Compare commits

..

23 Commits

Author SHA1 Message Date
Andrew Scott b2b903957a release: cut the v21.2.2 release 2026-03-09 11:11:43 -07:00
Matthieu Riegler bd72571583 refactor(devtools): prevent spamming the message bus
This commit adds some state on the bus to prevent sending unecessary messages to the main window.

(cherry picked from commit 263b819a75)
2026-03-09 10:46:08 -07:00
Kam 628f3f99db docs: add Turkish community translation of Angular documentation
add a link to the community-driven Turkish translation of Angular
documentation to make it easier for Turkish-speaking developers to
discover localized docs.

The translation project is maintained by the community and hosted at:
https://github.com/erkamyaman/angular-tr

Providing documentation in developers' native languages helps lower
the barrier to entry and supports wider Angular adoption in the
Turkish-speaking developer community.

(cherry picked from commit 03465360bd)
2026-03-09 10:29:25 -07:00
hawkgs 79ac570c26 refactor(devtools): fix browser-specific styles infra
Fix browser-specific styles infrastructure. PR #62786 cleans up part of the code, but there are still services that attempt to load these stylesheets on `main`.

(cherry picked from commit ed3dc10fea)
2026-03-09 10:22:02 -07:00
Angular Robot 276c12a349 build: update dependency node to v22.22.1
See associated pull request for more information.
2026-03-06 15:32:15 -08:00
Angular Robot 104ac117ea docs: update cross-repo adev docs
Updated Angular adev cross repo docs files.
2026-03-06 15:04:42 -08:00
Angular Robot c2a74548d9 build: update all github actions
See associated pull request for more information.
2026-03-06 14:36:16 -08:00
Kristiyan Kostadinov e7d1d8def3 refactor(core): add tracing for component creation
Adds tracing support for component creations.

(cherry picked from commit c19066c741)
2026-03-06 17:48:27 +00:00
Kristiyan Kostadinov 4a390b6c73 refactor(core): track the tracing service in the LView environment
Adds the `TracingService` to the `LView[ENVIRONMENT]` so we don't have to inject it everywhere.

(cherry picked from commit e433ba9a80)
2026-03-06 17:48:27 +00:00
Anushka Geeta Singh 5a486a65cf docs(router): Add example of relative navigation and multiple array values
Adds an example to docs with relative navigation and multiple values in
the commands array.

(cherry picked from commit 418cb61f2f)
2026-03-06 17:47:44 +00:00
Kristiyan Kostadinov c822bf8e76 fix(compiler-cli): always parenthesize object literals in TCB
This is a follow-up to #67381 which introduced a subtle bug where depending on the type checking configuration, we may put an object literal directly in the TCB body which the TS compiler ends up interpreting as a block. These changes resolve the issue by always wrapping the literal in parentheses.
2026-03-05 14:12:25 -08:00
Kristiyan Kostadinov 31e2c99007 refactor(compiler-cli): resolve presubmit issues
Resolves issues caught during the presubmit.
2026-03-05 14:12:25 -08:00
Kristiyan Kostadinov f8b6bd0cbe refactor(compiler-cli): escape quotes used in string expressions
TypeScript has functionality that automatically escapes quotes in string literals. These changes update the places where we may need to do the same ourselves.
2026-03-05 14:12:25 -08:00
Kristiyan Kostadinov 5349b027b8 refactor(compiler-cli): delete unused utilities
Deletes utilities that we no longer use.
2026-03-05 14:12:25 -08:00
Kristiyan Kostadinov 8f8d67dc57 refactor(compiler-cli): initial decoupling from TypeScript factory APIs
Initial pass to move usages of TS `factory` APIs to the new `TcbExpr`.
2026-03-05 14:12:25 -08:00
Kristiyan Kostadinov 23153a06dd refactor(compiler-cli): introduce new primitive for generating TCB code
Introduces the `TcbExpr` class that will be used to generate TCB code without going through TypeScript's factory APIs.
2026-03-05 14:12:25 -08:00
Alan Agius fd3b4413f3 docs: add a new section on nonces and caching considerations to the security guide.
(cherry picked from commit e5f99edda5)
2026-03-05 18:03:35 +00:00
Leon Senft 670d1660c4 feat(forms): add 'blur' option to debounce rule
Expands the `debounce` rule configuration to accept `'blur'`. When this option
is provided, the rule will delay model synchronization until the field loses
focus (is touched). This introduces a debouncer that defers resolution
until the framework automatically aborts pending debounces upon touch events.

(cherry picked from commit c767d678cf)
2026-03-05 17:55:18 +00:00
Angular Robot 8ca0237d99 build: update cross-repo angular dependencies
See associated pull request for more information.
2026-03-04 16:27:39 -08:00
Joey Perrott 3576019c0f build: no longer require release mode for releases
This commit updates the github.mjs configuration to set requireReleaseModeForRelease to false, removing the requirement to use release mode for releases in the angular/angular repository.

(cherry picked from commit a6941adce8)
2026-03-04 22:48:59 +00:00
Doug Parker 05d022d5e6 fix(compiler-cli): ignore generated ngDevMode signal branch for code coverage
The Angular compiler unconditionally adds a debug name transform for signals
which generates a conditional on `ngDevMode` (e.g., `ngDevMode ? { debugName: "xyz" } : []`).
During testing, `ngDevMode` is true, so the true branch executes but the
false branch is never executed. Consequently, coverage tools report the
false branch as an untested line/branch, preventing 100% test coverage.

This commit adds a synthetic `/* istanbul ignore next */` comment to the
generated false branch so that Istanbul ignores it. We only include the
istanbul comment (instead of additionally including c8) to focus on the
established standard for Angular CLI/Karma coverage while maintaining
compatibility with modern Vitest setups, since @vitest/coverage-v8 now
natively respects the fallback istanbul comment.

Fixes #64583

(cherry picked from commit dc4cf649b6)
2026-03-04 22:42:57 +00:00
Andrew Scott 1df1697c6e fix(compiler): prevent mutation of children array in RecursiveVisitor
RecursiveVisitor.visitIfBlockBranch was permanently mutating the children array by pushing the expressionAlias into it. This change clones the array before pushing to avoid this side effect.

(cherry picked from commit 72a17afaf3)
2026-03-04 22:41:36 +00:00
Andrew Scott 05476ea21b release: bump VSCode extension version to 21.2.2 (#67449) 2026-03-04 12:40:40 -08:00
106 changed files with 1918 additions and 1963 deletions
+2 -2
View File
@@ -33495,7 +33495,7 @@ tmp/lib/tmp.js:
(* v8 ignore next -- @preserve *)
(* v8 ignore else -- @preserve *)
@angular/ng-dev/bundles/chunk-WTYCKKHM.mjs:
@angular/ng-dev/bundles/chunk-YN3IWAKJ.mjs:
(*! Bundled license information:
yargs-parser/build/lib/string-utils.js:
@@ -33536,7 +33536,7 @@ tmp/lib/tmp.js:
*)
*)
@angular/ng-dev/bundles/chunk-PELL5AHH.mjs:
@angular/ng-dev/bundles/chunk-34WCNHIG.mjs:
(*! Bundled license information:
@octokit/request-error/dist-src/index.js:
+2 -2
View File
@@ -5,9 +5,9 @@ runs:
using: 'composite'
steps:
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Saucelabs Variables
uses: angular/dev-infra/github-actions/saucelabs@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/saucelabs@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Starting Saucelabs tunnel service
shell: bash
run: ./tools/saucelabs/sauce-service.sh run &
+4 -4
View File
@@ -21,17 +21,17 @@ 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@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Build adev
# `snapshot-build` config is used to stamp the exact version with sha in the footer.
run: pnpm bazel build //adev:build.production --config=snapshot-build
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
workflow-artifact-name: 'adev-preview'
pull-number: '${{github.event.pull_request.number}}'
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
github-token: '${{secrets.GITHUB_TOKEN}}'
workflow-artifact-name: 'adev-preview'
@@ -17,6 +17,6 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: angular/dev-infra/github-actions/branch-manager@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/branch-manager@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- run: pnpm install --frozen-lockfile
- uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
bazelrc: ./.bazelrc.user
+26 -26
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Check code lint
@@ -41,13 +41,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
disable-package-manager-cache: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -57,7 +57,7 @@ jobs:
- name: Test build
run: pnpm devtools:build:chrome
- name: Cypress run
uses: cypress-io/github-action@f9ffb60bba0c743f8c78dfed96873e8fdb2af34c # v7.1.4
uses: cypress-io/github-action@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5
with:
command: pnpm devtools:test:e2e
start: pnpm bazel run //devtools/src:devserver
@@ -69,11 +69,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -85,11 +85,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -102,11 +102,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -121,11 +121,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -138,11 +138,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
@@ -154,11 +154,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
- name: Install node modules
@@ -208,11 +208,11 @@ jobs:
runs-on: ubuntu-latest-8core
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Build adev
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: angular/dev-infra/github-actions/labeling/pull-request@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/labeling/pull-request@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
labels: '{"requires: TGP": ["packages/core/primitives/**/{*,.*}"]}'
@@ -25,14 +25,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: angular/dev-infra/github-actions/post-approval-changes@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/post-approval-changes@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
issue_labels:
if: github.event_name == 'issues'
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/labeling/issue@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/labeling/issue@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }}
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: angular/dev-infra/github-actions/google-internal-tests@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/google-internal-tests@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
run-tests-guide-url: http://go/angular-g3sync-start
github-token: ${{ secrets.GITHUB_TOKEN }}
+4 -4
View File
@@ -13,15 +13,15 @@ jobs:
JOBS: 2
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Saucelabs Variables
uses: angular/dev-infra/github-actions/saucelabs@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/saucelabs@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Set up Sauce Tunnel Daemon
run: pnpm bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
env:
+1 -1
View File
@@ -9,6 +9,6 @@ jobs:
status:
runs-on: ubuntu-latest
steps:
- uses: angular/dev-infra/github-actions/unified-status-check@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
- uses: angular/dev-infra/github-actions/unified-status-check@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
workflows: ${{ steps.workflows.outputs.workflows }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- id: workflows
@@ -36,9 +36,9 @@ jobs:
workflow: ${{ fromJSON(needs.list.outputs.workflows) }}
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
# We utilize the google-github-actions/auth action to allow us to get an active credential using workflow
+21 -21
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Check code lint
@@ -39,7 +39,7 @@ jobs:
- name: Check code format
run: pnpm ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
- name: Check Package Licenses
uses: angular/dev-infra/github-actions/linting/licenses@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/linting/licenses@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
allow-dependencies-licenses: 'pkg:npm/google-protobuf@'
@@ -47,13 +47,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
with:
disable-package-manager-cache: true
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run unit tests
@@ -61,7 +61,7 @@ jobs:
- name: Test build
run: pnpm devtools:build:chrome
- name: Cypress run
uses: cypress-io/github-action@f9ffb60bba0c743f8c78dfed96873e8fdb2af34c # v7.1.4
uses: cypress-io/github-action@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5
with:
command: pnpm devtools:test:e2e
start: pnpm bazel run //devtools/src:devserver
@@ -73,11 +73,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run CI tests for framework
@@ -97,11 +97,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel Remote Caching
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run integration CI tests for framework
@@ -112,11 +112,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run tests
@@ -129,11 +129,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- name: Run tests
@@ -144,11 +144,11 @@ jobs:
labels: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel
uses: angular/dev-infra/github-actions/bazel/setup@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Setup Bazel RBE
uses: angular/dev-infra/github-actions/bazel/configure-remote@c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
- name: Install node modules
run: pnpm install --frozen-lockfile
- run: |
+1 -1
View File
@@ -47,6 +47,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
sarif_file: results.sarif
+1 -1
View File
@@ -9,5 +9,5 @@ export const github = {
name: 'angular',
mainBranchName: 'main',
mergeMode: 'caretaker-only',
requireReleaseModeForRelease: true,
requireReleaseModeForRelease: false,
};
+1 -1
View File
@@ -1 +1 @@
22.22.0
22.22.1
+18
View File
@@ -1,3 +1,21 @@
<a name="21.2.2"></a>
# 21.2.2 (2026-03-09)
### compiler
| Commit | Type | Description |
| -- | -- | -- |
| [1df1697c6e](https://github.com/angular/angular/commit/1df1697c6e3a6b1d302f7692b495146943faa12f) | fix | prevent mutation of children array in RecursiveVisitor |
### compiler-cli
| Commit | Type | Description |
| -- | -- | -- |
| [c822bf8e76](https://github.com/angular/angular/commit/c822bf8e76611afde332b6625f5e7bae2fe9c3f3) | fix | always parenthesize object literals in TCB |
| [05d022d5e6](https://github.com/angular/angular/commit/05d022d5e61cca7ac90d5b2b2ba3fc738b364ad9) | fix | ignore generated ngDevMode signal branch for code coverage |
### forms
| Commit | Type | Description |
| -- | -- | -- |
| [670d1660c4](https://github.com/angular/angular/commit/670d1660c40504e3f55e094c3ebbcccad14163f3) | feat | add 'blur' option to debounce rule |
<!-- CHANGELOG SPLIT MARKER -->
<a name="21.2.1"></a>
# 21.2.1 (2026-03-04)
### core
+9 -9
View File
@@ -25,7 +25,7 @@ git_override(
bazel_dep(name = "devinfra")
git_override(
module_name = "devinfra",
commit = "c4344a4e20dbdf9cb8eeb7a7eb91431b98eabd99",
commit = "e4dcda13e469f50a6bd92667e2ded8b98173bf2c",
remote = "https://github.com/angular/dev-infra.git",
)
@@ -49,15 +49,15 @@ use_repo(yq, "yq_toolchains")
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(
node_repositories = {
"22.22.0-darwin_arm64": ("node-v22.22.0-darwin-arm64.tar.gz", "node-v22.22.0-darwin-arm64", "5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"),
"22.22.0-darwin_amd64": ("node-v22.22.0-darwin-x64.tar.gz", "node-v22.22.0-darwin-x64", "5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"),
"22.22.0-linux_arm64": ("node-v22.22.0-linux-arm64.tar.xz", "node-v22.22.0-linux-arm64", "1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"),
"22.22.0-linux_ppc64le": ("node-v22.22.0-linux-ppc64le.tar.xz", "node-v22.22.0-linux-ppc64le", "d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"),
"22.22.0-linux_s390x": ("node-v22.22.0-linux-s390x.tar.xz", "node-v22.22.0-linux-s390x", "5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"),
"22.22.0-linux_amd64": ("node-v22.22.0-linux-x64.tar.xz", "node-v22.22.0-linux-x64", "9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"),
"22.22.0-windows_amd64": ("node-v22.22.0-win-x64.zip", "node-v22.22.0-win-x64", "c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"),
"22.22.1-darwin_arm64": ("node-v22.22.1-darwin-arm64.tar.gz", "node-v22.22.1-darwin-arm64", "679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"),
"22.22.1-darwin_amd64": ("node-v22.22.1-darwin-x64.tar.gz", "node-v22.22.1-darwin-x64", "07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"),
"22.22.1-linux_arm64": ("node-v22.22.1-linux-arm64.tar.xz", "node-v22.22.1-linux-arm64", "0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"),
"22.22.1-linux_ppc64le": ("node-v22.22.1-linux-ppc64le.tar.xz", "node-v22.22.1-linux-ppc64le", "a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"),
"22.22.1-linux_s390x": ("node-v22.22.1-linux-s390x.tar.xz", "node-v22.22.1-linux-s390x", "e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"),
"22.22.1-linux_amd64": ("node-v22.22.1-linux-x64.tar.xz", "node-v22.22.1-linux-x64", "9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"),
"22.22.1-windows_amd64": ("node-v22.22.1-win-x64.zip", "node-v22.22.1-win-x64", "877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"),
},
node_version = "22.22.0",
node_version = "22.22.1",
)
use_repo(node, "nodejs_toolchains")
use_repo(node, "nodejs_darwin_amd64")
+233 -233
View File
@@ -1093,7 +1093,7 @@
"@@rules_nodejs+//nodejs:extensions.bzl%node": {
"general": {
"bzlTransitiveDigest": "4pUxCNc22K4I+6+4Nxu52Hur12tFRfa1JMsN5mdDv60=",
"usagesDigest": "SFGq+zUCbdSXGEjZH5ptl7y0cVwcw+BXOKjCi8YN22E=",
"usagesDigest": "pjXeuItYQffkhMcQv+aU/G0SB0QAmyOC9W7iq63eW08=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -1103,46 +1103,46 @@
"attributes": {
"node_download_auth": {},
"node_repositories": {
"22.22.0-darwin_arm64": [
"node-v22.22.0-darwin-arm64.tar.gz",
"node-v22.22.0-darwin-arm64",
"5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"
"22.22.1-darwin_arm64": [
"node-v22.22.1-darwin-arm64.tar.gz",
"node-v22.22.1-darwin-arm64",
"679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"
],
"22.22.0-darwin_amd64": [
"node-v22.22.0-darwin-x64.tar.gz",
"node-v22.22.0-darwin-x64",
"5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"
"22.22.1-darwin_amd64": [
"node-v22.22.1-darwin-x64.tar.gz",
"node-v22.22.1-darwin-x64",
"07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"
],
"22.22.0-linux_arm64": [
"node-v22.22.0-linux-arm64.tar.xz",
"node-v22.22.0-linux-arm64",
"1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"
"22.22.1-linux_arm64": [
"node-v22.22.1-linux-arm64.tar.xz",
"node-v22.22.1-linux-arm64",
"0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"
],
"22.22.0-linux_ppc64le": [
"node-v22.22.0-linux-ppc64le.tar.xz",
"node-v22.22.0-linux-ppc64le",
"d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"
"22.22.1-linux_ppc64le": [
"node-v22.22.1-linux-ppc64le.tar.xz",
"node-v22.22.1-linux-ppc64le",
"a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"
],
"22.22.0-linux_s390x": [
"node-v22.22.0-linux-s390x.tar.xz",
"node-v22.22.0-linux-s390x",
"5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"
"22.22.1-linux_s390x": [
"node-v22.22.1-linux-s390x.tar.xz",
"node-v22.22.1-linux-s390x",
"e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"
],
"22.22.0-linux_amd64": [
"node-v22.22.0-linux-x64.tar.xz",
"node-v22.22.0-linux-x64",
"9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"
"22.22.1-linux_amd64": [
"node-v22.22.1-linux-x64.tar.xz",
"node-v22.22.1-linux-x64",
"9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"
],
"22.22.0-windows_amd64": [
"node-v22.22.0-win-x64.zip",
"node-v22.22.0-win-x64",
"c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
"22.22.1-windows_amd64": [
"node-v22.22.1-win-x64.zip",
"node-v22.22.1-win-x64",
"877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"
]
},
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.22.0",
"node_version": "22.22.1",
"include_headers": false,
"platform": "linux_amd64"
}
@@ -1152,46 +1152,46 @@
"attributes": {
"node_download_auth": {},
"node_repositories": {
"22.22.0-darwin_arm64": [
"node-v22.22.0-darwin-arm64.tar.gz",
"node-v22.22.0-darwin-arm64",
"5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"
"22.22.1-darwin_arm64": [
"node-v22.22.1-darwin-arm64.tar.gz",
"node-v22.22.1-darwin-arm64",
"679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"
],
"22.22.0-darwin_amd64": [
"node-v22.22.0-darwin-x64.tar.gz",
"node-v22.22.0-darwin-x64",
"5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"
"22.22.1-darwin_amd64": [
"node-v22.22.1-darwin-x64.tar.gz",
"node-v22.22.1-darwin-x64",
"07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"
],
"22.22.0-linux_arm64": [
"node-v22.22.0-linux-arm64.tar.xz",
"node-v22.22.0-linux-arm64",
"1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"
"22.22.1-linux_arm64": [
"node-v22.22.1-linux-arm64.tar.xz",
"node-v22.22.1-linux-arm64",
"0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"
],
"22.22.0-linux_ppc64le": [
"node-v22.22.0-linux-ppc64le.tar.xz",
"node-v22.22.0-linux-ppc64le",
"d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"
"22.22.1-linux_ppc64le": [
"node-v22.22.1-linux-ppc64le.tar.xz",
"node-v22.22.1-linux-ppc64le",
"a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"
],
"22.22.0-linux_s390x": [
"node-v22.22.0-linux-s390x.tar.xz",
"node-v22.22.0-linux-s390x",
"5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"
"22.22.1-linux_s390x": [
"node-v22.22.1-linux-s390x.tar.xz",
"node-v22.22.1-linux-s390x",
"e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"
],
"22.22.0-linux_amd64": [
"node-v22.22.0-linux-x64.tar.xz",
"node-v22.22.0-linux-x64",
"9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"
"22.22.1-linux_amd64": [
"node-v22.22.1-linux-x64.tar.xz",
"node-v22.22.1-linux-x64",
"9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"
],
"22.22.0-windows_amd64": [
"node-v22.22.0-win-x64.zip",
"node-v22.22.0-win-x64",
"c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
"22.22.1-windows_amd64": [
"node-v22.22.1-win-x64.zip",
"node-v22.22.1-win-x64",
"877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"
]
},
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.22.0",
"node_version": "22.22.1",
"include_headers": false,
"platform": "linux_arm64"
}
@@ -1201,46 +1201,46 @@
"attributes": {
"node_download_auth": {},
"node_repositories": {
"22.22.0-darwin_arm64": [
"node-v22.22.0-darwin-arm64.tar.gz",
"node-v22.22.0-darwin-arm64",
"5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"
"22.22.1-darwin_arm64": [
"node-v22.22.1-darwin-arm64.tar.gz",
"node-v22.22.1-darwin-arm64",
"679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"
],
"22.22.0-darwin_amd64": [
"node-v22.22.0-darwin-x64.tar.gz",
"node-v22.22.0-darwin-x64",
"5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"
"22.22.1-darwin_amd64": [
"node-v22.22.1-darwin-x64.tar.gz",
"node-v22.22.1-darwin-x64",
"07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"
],
"22.22.0-linux_arm64": [
"node-v22.22.0-linux-arm64.tar.xz",
"node-v22.22.0-linux-arm64",
"1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"
"22.22.1-linux_arm64": [
"node-v22.22.1-linux-arm64.tar.xz",
"node-v22.22.1-linux-arm64",
"0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"
],
"22.22.0-linux_ppc64le": [
"node-v22.22.0-linux-ppc64le.tar.xz",
"node-v22.22.0-linux-ppc64le",
"d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"
"22.22.1-linux_ppc64le": [
"node-v22.22.1-linux-ppc64le.tar.xz",
"node-v22.22.1-linux-ppc64le",
"a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"
],
"22.22.0-linux_s390x": [
"node-v22.22.0-linux-s390x.tar.xz",
"node-v22.22.0-linux-s390x",
"5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"
"22.22.1-linux_s390x": [
"node-v22.22.1-linux-s390x.tar.xz",
"node-v22.22.1-linux-s390x",
"e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"
],
"22.22.0-linux_amd64": [
"node-v22.22.0-linux-x64.tar.xz",
"node-v22.22.0-linux-x64",
"9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"
"22.22.1-linux_amd64": [
"node-v22.22.1-linux-x64.tar.xz",
"node-v22.22.1-linux-x64",
"9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"
],
"22.22.0-windows_amd64": [
"node-v22.22.0-win-x64.zip",
"node-v22.22.0-win-x64",
"c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
"22.22.1-windows_amd64": [
"node-v22.22.1-win-x64.zip",
"node-v22.22.1-win-x64",
"877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"
]
},
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.22.0",
"node_version": "22.22.1",
"include_headers": false,
"platform": "linux_s390x"
}
@@ -1250,46 +1250,46 @@
"attributes": {
"node_download_auth": {},
"node_repositories": {
"22.22.0-darwin_arm64": [
"node-v22.22.0-darwin-arm64.tar.gz",
"node-v22.22.0-darwin-arm64",
"5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"
"22.22.1-darwin_arm64": [
"node-v22.22.1-darwin-arm64.tar.gz",
"node-v22.22.1-darwin-arm64",
"679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"
],
"22.22.0-darwin_amd64": [
"node-v22.22.0-darwin-x64.tar.gz",
"node-v22.22.0-darwin-x64",
"5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"
"22.22.1-darwin_amd64": [
"node-v22.22.1-darwin-x64.tar.gz",
"node-v22.22.1-darwin-x64",
"07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"
],
"22.22.0-linux_arm64": [
"node-v22.22.0-linux-arm64.tar.xz",
"node-v22.22.0-linux-arm64",
"1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"
"22.22.1-linux_arm64": [
"node-v22.22.1-linux-arm64.tar.xz",
"node-v22.22.1-linux-arm64",
"0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"
],
"22.22.0-linux_ppc64le": [
"node-v22.22.0-linux-ppc64le.tar.xz",
"node-v22.22.0-linux-ppc64le",
"d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"
"22.22.1-linux_ppc64le": [
"node-v22.22.1-linux-ppc64le.tar.xz",
"node-v22.22.1-linux-ppc64le",
"a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"
],
"22.22.0-linux_s390x": [
"node-v22.22.0-linux-s390x.tar.xz",
"node-v22.22.0-linux-s390x",
"5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"
"22.22.1-linux_s390x": [
"node-v22.22.1-linux-s390x.tar.xz",
"node-v22.22.1-linux-s390x",
"e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"
],
"22.22.0-linux_amd64": [
"node-v22.22.0-linux-x64.tar.xz",
"node-v22.22.0-linux-x64",
"9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"
"22.22.1-linux_amd64": [
"node-v22.22.1-linux-x64.tar.xz",
"node-v22.22.1-linux-x64",
"9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"
],
"22.22.0-windows_amd64": [
"node-v22.22.0-win-x64.zip",
"node-v22.22.0-win-x64",
"c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
"22.22.1-windows_amd64": [
"node-v22.22.1-win-x64.zip",
"node-v22.22.1-win-x64",
"877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"
]
},
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.22.0",
"node_version": "22.22.1",
"include_headers": false,
"platform": "linux_ppc64le"
}
@@ -1299,46 +1299,46 @@
"attributes": {
"node_download_auth": {},
"node_repositories": {
"22.22.0-darwin_arm64": [
"node-v22.22.0-darwin-arm64.tar.gz",
"node-v22.22.0-darwin-arm64",
"5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"
"22.22.1-darwin_arm64": [
"node-v22.22.1-darwin-arm64.tar.gz",
"node-v22.22.1-darwin-arm64",
"679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"
],
"22.22.0-darwin_amd64": [
"node-v22.22.0-darwin-x64.tar.gz",
"node-v22.22.0-darwin-x64",
"5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"
"22.22.1-darwin_amd64": [
"node-v22.22.1-darwin-x64.tar.gz",
"node-v22.22.1-darwin-x64",
"07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"
],
"22.22.0-linux_arm64": [
"node-v22.22.0-linux-arm64.tar.xz",
"node-v22.22.0-linux-arm64",
"1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"
"22.22.1-linux_arm64": [
"node-v22.22.1-linux-arm64.tar.xz",
"node-v22.22.1-linux-arm64",
"0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"
],
"22.22.0-linux_ppc64le": [
"node-v22.22.0-linux-ppc64le.tar.xz",
"node-v22.22.0-linux-ppc64le",
"d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"
"22.22.1-linux_ppc64le": [
"node-v22.22.1-linux-ppc64le.tar.xz",
"node-v22.22.1-linux-ppc64le",
"a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"
],
"22.22.0-linux_s390x": [
"node-v22.22.0-linux-s390x.tar.xz",
"node-v22.22.0-linux-s390x",
"5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"
"22.22.1-linux_s390x": [
"node-v22.22.1-linux-s390x.tar.xz",
"node-v22.22.1-linux-s390x",
"e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"
],
"22.22.0-linux_amd64": [
"node-v22.22.0-linux-x64.tar.xz",
"node-v22.22.0-linux-x64",
"9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"
"22.22.1-linux_amd64": [
"node-v22.22.1-linux-x64.tar.xz",
"node-v22.22.1-linux-x64",
"9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"
],
"22.22.0-windows_amd64": [
"node-v22.22.0-win-x64.zip",
"node-v22.22.0-win-x64",
"c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
"22.22.1-windows_amd64": [
"node-v22.22.1-win-x64.zip",
"node-v22.22.1-win-x64",
"877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"
]
},
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.22.0",
"node_version": "22.22.1",
"include_headers": false,
"platform": "darwin_amd64"
}
@@ -1348,46 +1348,46 @@
"attributes": {
"node_download_auth": {},
"node_repositories": {
"22.22.0-darwin_arm64": [
"node-v22.22.0-darwin-arm64.tar.gz",
"node-v22.22.0-darwin-arm64",
"5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"
"22.22.1-darwin_arm64": [
"node-v22.22.1-darwin-arm64.tar.gz",
"node-v22.22.1-darwin-arm64",
"679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"
],
"22.22.0-darwin_amd64": [
"node-v22.22.0-darwin-x64.tar.gz",
"node-v22.22.0-darwin-x64",
"5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"
"22.22.1-darwin_amd64": [
"node-v22.22.1-darwin-x64.tar.gz",
"node-v22.22.1-darwin-x64",
"07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"
],
"22.22.0-linux_arm64": [
"node-v22.22.0-linux-arm64.tar.xz",
"node-v22.22.0-linux-arm64",
"1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"
"22.22.1-linux_arm64": [
"node-v22.22.1-linux-arm64.tar.xz",
"node-v22.22.1-linux-arm64",
"0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"
],
"22.22.0-linux_ppc64le": [
"node-v22.22.0-linux-ppc64le.tar.xz",
"node-v22.22.0-linux-ppc64le",
"d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"
"22.22.1-linux_ppc64le": [
"node-v22.22.1-linux-ppc64le.tar.xz",
"node-v22.22.1-linux-ppc64le",
"a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"
],
"22.22.0-linux_s390x": [
"node-v22.22.0-linux-s390x.tar.xz",
"node-v22.22.0-linux-s390x",
"5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"
"22.22.1-linux_s390x": [
"node-v22.22.1-linux-s390x.tar.xz",
"node-v22.22.1-linux-s390x",
"e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"
],
"22.22.0-linux_amd64": [
"node-v22.22.0-linux-x64.tar.xz",
"node-v22.22.0-linux-x64",
"9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"
"22.22.1-linux_amd64": [
"node-v22.22.1-linux-x64.tar.xz",
"node-v22.22.1-linux-x64",
"9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"
],
"22.22.0-windows_amd64": [
"node-v22.22.0-win-x64.zip",
"node-v22.22.0-win-x64",
"c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
"22.22.1-windows_amd64": [
"node-v22.22.1-win-x64.zip",
"node-v22.22.1-win-x64",
"877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"
]
},
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.22.0",
"node_version": "22.22.1",
"include_headers": false,
"platform": "darwin_arm64"
}
@@ -1397,46 +1397,46 @@
"attributes": {
"node_download_auth": {},
"node_repositories": {
"22.22.0-darwin_arm64": [
"node-v22.22.0-darwin-arm64.tar.gz",
"node-v22.22.0-darwin-arm64",
"5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"
"22.22.1-darwin_arm64": [
"node-v22.22.1-darwin-arm64.tar.gz",
"node-v22.22.1-darwin-arm64",
"679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"
],
"22.22.0-darwin_amd64": [
"node-v22.22.0-darwin-x64.tar.gz",
"node-v22.22.0-darwin-x64",
"5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"
"22.22.1-darwin_amd64": [
"node-v22.22.1-darwin-x64.tar.gz",
"node-v22.22.1-darwin-x64",
"07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"
],
"22.22.0-linux_arm64": [
"node-v22.22.0-linux-arm64.tar.xz",
"node-v22.22.0-linux-arm64",
"1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"
"22.22.1-linux_arm64": [
"node-v22.22.1-linux-arm64.tar.xz",
"node-v22.22.1-linux-arm64",
"0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"
],
"22.22.0-linux_ppc64le": [
"node-v22.22.0-linux-ppc64le.tar.xz",
"node-v22.22.0-linux-ppc64le",
"d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"
"22.22.1-linux_ppc64le": [
"node-v22.22.1-linux-ppc64le.tar.xz",
"node-v22.22.1-linux-ppc64le",
"a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"
],
"22.22.0-linux_s390x": [
"node-v22.22.0-linux-s390x.tar.xz",
"node-v22.22.0-linux-s390x",
"5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"
"22.22.1-linux_s390x": [
"node-v22.22.1-linux-s390x.tar.xz",
"node-v22.22.1-linux-s390x",
"e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"
],
"22.22.0-linux_amd64": [
"node-v22.22.0-linux-x64.tar.xz",
"node-v22.22.0-linux-x64",
"9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"
"22.22.1-linux_amd64": [
"node-v22.22.1-linux-x64.tar.xz",
"node-v22.22.1-linux-x64",
"9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"
],
"22.22.0-windows_amd64": [
"node-v22.22.0-win-x64.zip",
"node-v22.22.0-win-x64",
"c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
"22.22.1-windows_amd64": [
"node-v22.22.1-win-x64.zip",
"node-v22.22.1-win-x64",
"877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"
]
},
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.22.0",
"node_version": "22.22.1",
"include_headers": false,
"platform": "windows_amd64"
}
@@ -1446,46 +1446,46 @@
"attributes": {
"node_download_auth": {},
"node_repositories": {
"22.22.0-darwin_arm64": [
"node-v22.22.0-darwin-arm64.tar.gz",
"node-v22.22.0-darwin-arm64",
"5ed4db0fcf1eaf84d91ad12462631d73bf4576c1377e192d222e48026a902640"
"22.22.1-darwin_arm64": [
"node-v22.22.1-darwin-arm64.tar.gz",
"node-v22.22.1-darwin-arm64",
"679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"
],
"22.22.0-darwin_amd64": [
"node-v22.22.0-darwin-x64.tar.gz",
"node-v22.22.0-darwin-x64",
"5ea50c9d6dea3dfa3abb66b2656f7a4e1c8cef23432b558d45fb538c7b5dedce"
"22.22.1-darwin_amd64": [
"node-v22.22.1-darwin-x64.tar.gz",
"node-v22.22.1-darwin-x64",
"07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"
],
"22.22.0-linux_arm64": [
"node-v22.22.0-linux-arm64.tar.xz",
"node-v22.22.0-linux-arm64",
"1bf1eb9ee63ffc4e5d324c0b9b62cf4a289f44332dfef9607cea1a0d9596ba6f"
"22.22.1-linux_arm64": [
"node-v22.22.1-linux-arm64.tar.xz",
"node-v22.22.1-linux-arm64",
"0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"
],
"22.22.0-linux_ppc64le": [
"node-v22.22.0-linux-ppc64le.tar.xz",
"node-v22.22.0-linux-ppc64le",
"d83b9957431cc18e1fc143a4b99f89cde7b8a18f53ef392231b4336afd058865"
"22.22.1-linux_ppc64le": [
"node-v22.22.1-linux-ppc64le.tar.xz",
"node-v22.22.1-linux-ppc64le",
"a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"
],
"22.22.0-linux_s390x": [
"node-v22.22.0-linux-s390x.tar.xz",
"node-v22.22.0-linux-s390x",
"5aa0e520689448c4233e8d73f284e8e0634fdcd32b479735698494be5641f3e4"
"22.22.1-linux_s390x": [
"node-v22.22.1-linux-s390x.tar.xz",
"node-v22.22.1-linux-s390x",
"e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"
],
"22.22.0-linux_amd64": [
"node-v22.22.0-linux-x64.tar.xz",
"node-v22.22.0-linux-x64",
"9aa8e9d2298ab68c600bd6fb86a6c13bce11a4eca1ba9b39d79fa021755d7c37"
"22.22.1-linux_amd64": [
"node-v22.22.1-linux-x64.tar.xz",
"node-v22.22.1-linux-x64",
"9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"
],
"22.22.0-windows_amd64": [
"node-v22.22.0-win-x64.zip",
"node-v22.22.0-win-x64",
"c97fa376d2becdc8863fcd3ca2dd9a83a9f3468ee7ccf7a6d076ec66a645c77a"
"22.22.1-windows_amd64": [
"node-v22.22.1-win-x64.zip",
"node-v22.22.1-win-x64",
"877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"
]
},
"node_urls": [
"https://nodejs.org/dist/v{version}/{filename}"
],
"node_version": "22.22.0",
"node_version": "22.22.1",
"include_headers": false,
"platform": "windows_arm64"
}
@@ -99,6 +99,9 @@
<li>
<a href="https://angular.az/" title="Azərbaycanca">Azərbaycanca</a>
</li>
<li>
<a href="https://angular-docs.tr/" title="Türkçe">Türkçe</a>
</li>
<li>
<a href="https://angular-docs.ru/" title="Русский">Русский</a>
</li>
+1 -1
View File
@@ -1,4 +1,4 @@
{
"branchName": "refs/heads/21.2.x",
"sha": "a68740532edcf94dafd4a6862a8985f506718016"
"sha": "a076ff7f05b678c4cdc6b85dbfe474a09f56022a"
}
+2 -2
View File
@@ -275,7 +275,7 @@
"source": {
"filePath": "src/aria/grid/grid-cell-widget.ts",
"startLine": 42,
"endLine": 134
"endLine": 135
}
},
{
@@ -446,7 +446,7 @@
"source": {
"filePath": "/src/aria/grid/grid.ts",
"startLine": 48,
"endLine": 177
"endLine": 178
}
},
{
@@ -137,6 +137,13 @@ export class UserDetail {
// To: /users
this.router.navigate(['..'], {relativeTo: this.route});
}
navigateToList() {
// Angular resolves the commands array as a single navigation path relative to the current route.
// From: /users/123
// Result: /users/list
this.router.navigate(['..', 'list'], {relativeTo: this.route});
}
}
```
+4
View File
@@ -173,6 +173,10 @@ bootstrapApplication(AppComponent, {
Always ensure that the nonces you provide are <strong>unique per request</strong> and that they are not predictable or guessable.
If an attacker can predict future nonces, they can circumvent the protections offered by CSP.
Generating a nonce at the origin server is generally discouraged when using a CDN, as responses are frequently cached. If the server generates a nonce and the CDN caches that HTML response, every subsequent visitor receives the same "unique" value, allowing an attacker to discover the static value and bypass CSP protections.
To maintain the "one-time-use" integrity of a nonce, it should ideally be generated at the Edge layer (e.g., CDN) just before the content is delivered to the user.
</docs-callout>
NOTE: If you want to [inline the critical CSS](/tools/cli/build#critical-css-inlining) of your application, you can not use the `CSP_NONCE` token, and should prefer the `autoCsp` option or set the `ngCspNonce` attribute on the root application element.
@@ -1,4 +1,4 @@
load("//devtools/tools:defaults.bzl", "npm_sass_library", "sass_library")
load("//devtools/tools:defaults.bzl", "npm_sass_library", "sass_binary", "sass_library")
package(default_visibility = ["//visibility:public"])
@@ -76,3 +76,13 @@ sass_library(
":typography",
],
)
sass_binary(
name = "firefox",
src = "firefox.scss",
)
sass_binary(
name = "chrome",
src = "chrome.scss",
)
@@ -0,0 +1 @@
/* Chrome-specific global styles */
@@ -0,0 +1 @@
/* Firefox-specific global styles */
@@ -1,4 +1,4 @@
load("//devtools/tools:defaults.bzl", "esbuild", "extension_package", "ng_project", "sass_binary", "string_flag", "ts_project")
load("//devtools/tools:defaults.bzl", "copy_to_directory", "esbuild", "extension_package", "ng_project", "sass_binary", "string_flag", "ts_project")
package(default_visibility = ["//visibility:public"])
@@ -10,6 +10,18 @@ sass_binary(
deps = ["//devtools/projects/ng-devtools/src/styles:global"],
)
copy_to_directory(
name = "browser_specific_styles",
srcs = [
"//devtools/projects/ng-devtools/src/styles:chrome",
"//devtools/projects/ng-devtools/src/styles:firefox",
],
out = "styles",
replace_prefixes = {
"devtools/projects/ng-devtools/src/styles": "",
},
)
ts_project(
name = "devtools",
srcs = [
@@ -74,6 +86,7 @@ exports_files(["index.html"])
filegroup(
name = "prod_app_static_files",
srcs = [
":browser_specific_styles",
":index.html",
":shell_common_styles",
],
@@ -14,7 +14,11 @@ import {initializeExtendedWindowOperations} from './chrome-window-extensions';
import {getBackendUri, getContentScriptUri} from './comm-utils';
import {SamePageMessageBus} from './same-page-message-bus';
const messageBus = new SamePageMessageBus(getBackendUri(), getContentScriptUri());
const messageBus = new SamePageMessageBus(
'[Backend=>ContentScript]',
getBackendUri(),
getContentScriptUri(),
);
let initialized = false;
messageBus.on('handshake', () => {
@@ -56,6 +56,7 @@ function attemptBackendHandshake() {
port.onDisconnect.addListener(handleDisconnect);
const detectAngularMessageBus = new SamePageMessageBus(
'[ContentScript=>DetectAngular]',
getContentScriptUri(),
getDetectAngularScriptUri(),
);
@@ -87,7 +88,11 @@ detectAngularMessageBus.on('detectAngular', (detectionResult) => {
attemptBackendHandshake();
});
const localMessageBus = new SamePageMessageBus(getContentScriptUri(), getBackendUri());
const localMessageBus = new SamePageMessageBus(
'[ConstentScript=>BackEnd]',
getContentScriptUri(),
getBackendUri(),
);
const chromeMessageBus = new ChromeMessageBus(port);
const handshakeWithBackend = (): void => {
@@ -18,6 +18,7 @@ import {getDetectAngularScriptUri} from './comm-utils';
import {SamePageMessageBus} from './same-page-message-bus';
const detectAngularMessageBus = new SamePageMessageBus(
'[DetectAngular=>CS]',
getDetectAngularScriptUri(),
getDetectAngularScriptUri(),
);
@@ -12,10 +12,22 @@ type AnyEventCallback<Ev> = <E extends keyof Ev>(topic: E, args: Parameters<Ev[E
type ListenerFn = (e: MessageEvent) => void;
// We use a bus status to know if we should send the message between the context
// eg: We don't want to send events until the devtools tab has been focused to present spaming the message bus
// init: initial state, until the backend is ready
// waiting: blocking message, until we see the 'enableFrameConnection' event
// ready: we send every message through the bus
export type BusStatus = 'init' | 'waiting' | 'ready';
/**
* Note: This Bus implementation implements a filter based of topic. It might not be suitable for others messages than the ones currently used.
*/
export class SamePageMessageBus extends MessageBus<Events> {
private status: BusStatus = 'init';
private _listeners: ListenerFn[] = [];
constructor(
private debugName: string,
private _source: string,
private _destination: string,
) {
@@ -24,6 +36,8 @@ export class SamePageMessageBus extends MessageBus<Events> {
onAny(cb: AnyEventCallback<Events>): () => void {
const listener: ListenerFn = (e) => {
this.updateStatus(e.data?.topic);
if (e.source !== window || !e.data || !e.data.topic || e.data.source !== this._destination) {
return;
}
@@ -39,6 +53,8 @@ export class SamePageMessageBus extends MessageBus<Events> {
override on<E extends keyof Events>(topic: E, cb: Events[E]): () => void {
const listener: ListenerFn = (e) => {
this.updateStatus(e.data?.topic);
if (e.source !== window || !e.data || e.data.source !== this._destination || !e.data.topic) {
return;
}
@@ -68,6 +84,10 @@ export class SamePageMessageBus extends MessageBus<Events> {
}
override emit<E extends keyof Events>(topic: E, args?: Parameters<Events[E]>): boolean {
if (this.shouldSkipMessage(topic)) {
return false;
}
window.postMessage(
{
source: this._source,
@@ -85,4 +105,16 @@ export class SamePageMessageBus extends MessageBus<Events> {
this._listeners.forEach((l) => window.removeEventListener('message', l));
this._listeners = [];
}
private shouldSkipMessage(topic: string): boolean {
return this.status === 'waiting' && topic !== 'enableFrameConnection';
}
private updateStatus(topic: string): void {
if (topic === 'enableFrameConnection') {
this.status = 'ready';
} else if (topic === 'backendReady') {
this.status = 'waiting';
}
}
}
+14 -1
View File
@@ -1,4 +1,4 @@
load("//devtools/tools:defaults.bzl", "esbuild", "http_server", "ng_project", "sass_binary")
load("//devtools/tools:defaults.bzl", "copy_to_directory", "esbuild", "http_server", "ng_project", "sass_binary")
package(default_visibility = ["//visibility:public"])
@@ -9,6 +9,18 @@ sass_binary(
deps = ["//devtools/projects/ng-devtools/src/styles:global"],
)
copy_to_directory(
name = "browser_specific_styles",
srcs = [
"//devtools/projects/ng-devtools/src/styles:chrome",
"//devtools/projects/ng-devtools/src/styles:firefox",
],
out = "styles",
replace_prefixes = {
"devtools/projects/ng-devtools/src/styles": "",
},
)
ng_project(
name = "demo",
srcs = ["main.ts"],
@@ -43,6 +55,7 @@ exports_files(["index.html"])
filegroup(
name = "dev_app_static_files",
srcs = [
":browser_specific_styles",
":demo_styles",
":index.html",
"//devtools/src/assets",
@@ -91,7 +91,7 @@ export function createMetadataKey<TWrite>(): MetadataKey<Signal<TWrite | undefin
export function createMetadataKey<TWrite, TAcc>(reducer: MetadataReducer<TAcc, TWrite>): MetadataKey<Signal<TAcc>, TWrite, TAcc>;
// @public
export function debounce<TValue, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, durationOrDebouncer: number | Debouncer<TValue, TPathKind>): void;
export function debounce<TValue, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, config: number | 'blur' | Debouncer<TValue, TPathKind>): void;
// @public
export type Debouncer<TValue, TPathKind extends PathKind = PathKind.Root> = (context: FieldContext<TValue, TPathKind>, abortSignal: AbortSignal) => Promise<void> | void;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "21.2.1",
"version": "21.2.2",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",
@@ -161,7 +161,7 @@
"devDependencies": {
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#5a0a8a72a00865eb0d787908876eacb51138113a",
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#610c7c10ab750bfe5688581d6d5ec11114048f35",
"@babel/plugin-proposal-async-generator-functions": "7.20.7",
"@babel/plugin-transform-async-generator-functions": "^7.27.1",
"@bazel/bazelisk": "^1.7.5",
@@ -89,6 +89,12 @@ function createNgDevModeConditional(
devModeExpression: ts.Expression,
prodModeExpression: ts.Expression,
): ts.ParenthesizedExpression {
ts.addSyntheticLeadingComment(
prodModeExpression,
ts.SyntaxKind.MultiLineCommentTrivia,
' istanbul ignore next ',
false,
);
return ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(
ts.factory.createIdentifier('ngDevMode'),
@@ -51,31 +51,8 @@ export enum ExpressionIdentifier {
VARIABLE_AS_EXPRESSION = 'VAE',
}
/** Tags the node with the given expression identifier. */
export function addExpressionIdentifier(node: ts.Node, identifier: ExpressionIdentifier) {
ts.addSyntheticTrailingComment(
node,
ts.SyntaxKind.MultiLineCommentTrivia,
`${CommentTriviaType.EXPRESSION_TYPE_IDENTIFIER}:${identifier}`,
/* hasTrailingNewLine */ false,
);
}
const IGNORE_FOR_DIAGNOSTICS_MARKER = `${CommentTriviaType.DIAGNOSTIC}:ignore`;
/**
* Tag the `ts.Node` with an indication that any errors arising from the evaluation of the node
* should be ignored.
*/
export function markIgnoreDiagnostics(node: ts.Node): void {
ts.addSyntheticTrailingComment(
node,
ts.SyntaxKind.MultiLineCommentTrivia,
IGNORE_FOR_DIAGNOSTICS_MARKER,
/* hasTrailingNewLine */ false,
);
}
/** Returns true if the node has a marker that indicates diagnostics errors should be ignored. */
export function hasIgnoreForDiagnosticsMarker(node: ts.Node, sourceFile: ts.SourceFile): boolean {
return (
@@ -461,7 +461,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
.map((op) => {
return {
pos: op.splitPoint,
text: op.execute(importManager, sf, this.refEmitter, printer),
text: op.execute(importManager, sf, this.refEmitter),
};
});
@@ -525,7 +525,7 @@ export class TypeCheckContextImpl implements TypeCheckContext {
path: pendingShimData.file.fileName,
data: pendingShimData.data,
});
const sfText = pendingShimData.file.render(false /* removeComments */);
const sfText = pendingShimData.file.render();
updates.set(pendingShimData.file.fileName, {
newText: sfText,
@@ -642,12 +642,7 @@ interface Op {
/**
* Execute the operation and return the generated code as text.
*/
execute(
im: ImportManager,
sf: ts.SourceFile,
refEmitter: ReferenceEmitter,
printer: ts.Printer,
): string;
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter): string;
}
/**
@@ -670,12 +665,7 @@ class InlineTcbOp implements Op {
return this.ref.node.end + 1;
}
execute(
im: ImportManager,
sf: ts.SourceFile,
refEmitter: ReferenceEmitter,
printer: ts.Printer,
): string {
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter): string {
const env = new Environment(this.config, im, refEmitter, this.reflector, sf);
const fnName = ts.factory.createIdentifier(`_tcb_${this.ref.node.pos}`);
@@ -691,7 +681,7 @@ class InlineTcbOp implements Op {
TcbGenericContextBehavior.CopyClassNodes,
);
return printer.printNode(ts.EmitHint.Unspecified, fn, sf);
return fn;
}
}
@@ -712,36 +702,8 @@ class TypeCtorOp implements Op {
return this.ref.node.end - 1;
}
execute(
im: ImportManager,
sf: ts.SourceFile,
refEmitter: ReferenceEmitter,
printer: ts.Printer,
): string {
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter): string {
const emitEnv = new ReferenceEmitEnvironment(im, refEmitter, this.reflector, sf);
const tcb = generateInlineTypeCtor(emitEnv, this.ref.node, this.meta);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
return generateInlineTypeCtor(emitEnv, this.ref.node, this.meta);
}
}
/**
* Compare two operations and return their split point ordering.
*/
function orderOps(op1: Op, op2: Op): number {
return op1.splitPoint - op2.splitPoint;
}
/**
* Split a string into chunks at any number of split points.
*/
function splitStringAtPoints(str: string, points: number[]): string[] {
const splits: string[] = [];
let start = 0;
for (let i = 0; i < points.length; i++) {
const point = points[i];
splits.push(str.substring(start, point));
start = point;
}
splits.push(str.substring(start));
return splits;
}
@@ -19,9 +19,9 @@ import {ImportManager, translateExpression} from '../../translator';
import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from '../api';
import {ReferenceEmitEnvironment} from './reference_emit_environment';
import {tsDeclareVariable} from './ts_util';
import {generateTypeCtorDeclarationFn, requiresInlineTypeCtor} from './type_constructor';
import {TypeParameterEmitter} from './type_parameter_emitter';
import {declareVariable, TcbExpr, tempPrint} from './ops/codegen';
/**
* A context which hosts one or more Type Check Blocks (TCBs).
@@ -40,11 +40,11 @@ export class Environment extends ReferenceEmitEnvironment {
typeCtor: 1,
};
private typeCtors = new Map<ClassDeclaration, ts.Expression>();
protected typeCtorStatements: ts.Statement[] = [];
private typeCtors = new Map<ClassDeclaration, string>();
protected typeCtorStatements: TcbExpr[] = [];
private pipeInsts = new Map<ClassDeclaration, ts.Expression>();
protected pipeInstStatements: ts.Statement[] = [];
private pipeInsts = new Map<ClassDeclaration, string>();
protected pipeInstStatements: TcbExpr[] = [];
constructor(
readonly config: TypeCheckingConfig,
@@ -62,20 +62,20 @@ export class Environment extends ReferenceEmitEnvironment {
* Depending on the shape of the directive itself, this could be either a reference to a declared
* type constructor, or to an inline type constructor.
*/
typeCtorFor(dir: TypeCheckableDirectiveMeta): ts.Expression {
typeCtorFor(dir: TypeCheckableDirectiveMeta): TcbExpr {
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
const node = dirRef.node;
if (this.typeCtors.has(node)) {
return this.typeCtors.get(node)!;
return new TcbExpr(this.typeCtors.get(node)!);
}
if (requiresInlineTypeCtor(node, this.reflector, this)) {
// The constructor has already been created inline, we just need to construct a reference to
// it.
const ref = this.reference(dirRef);
const typeCtorExpr = ts.factory.createPropertyAccessExpression(ref, 'ngTypeCtor');
const typeCtorExpr = `${ref.print()}.ngTypeCtor`;
this.typeCtors.set(node, typeCtorExpr);
return typeCtorExpr;
return new TcbExpr(typeCtorExpr);
} else {
const fnName = `_ctor${this.nextIds.typeCtor++}`;
const nodeTypeRef = this.referenceType(dirRef);
@@ -95,27 +95,27 @@ export class Environment extends ReferenceEmitEnvironment {
const typeParams = this.emitTypeParameters(node);
const typeCtor = generateTypeCtorDeclarationFn(this, meta, nodeTypeRef.typeName, typeParams);
this.typeCtorStatements.push(typeCtor);
const fnId = ts.factory.createIdentifier(fnName);
this.typeCtors.set(node, fnId);
return fnId;
this.typeCtors.set(node, fnName);
return new TcbExpr(fnName);
}
}
/*
* Get an expression referring to an instance of the given pipe.
*/
pipeInst(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
pipeInst(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): TcbExpr {
if (this.pipeInsts.has(ref.node)) {
return this.pipeInsts.get(ref.node)!;
return new TcbExpr(this.pipeInsts.get(ref.node)!);
}
const pipeType = this.referenceType(ref);
const pipeInstId = ts.factory.createIdentifier(`_pipe${this.nextIds.pipeInst++}`);
const pipeInstId = `_pipe${this.nextIds.pipeInst++}`;
this.pipeInstStatements.push(tsDeclareVariable(pipeInstId, pipeType));
this.pipeInsts.set(ref.node, pipeInstId);
return pipeInstId;
this.pipeInstStatements.push(
declareVariable(new TcbExpr(pipeInstId), new TcbExpr(tempPrint(pipeType, this.contextFile))),
);
return new TcbExpr(pipeInstId);
}
/**
@@ -123,7 +123,7 @@ export class Environment extends ReferenceEmitEnvironment {
*
* This may involve importing the node into the file if it's not declared there already.
*/
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): TcbExpr {
// Disable aliasing for imports generated in a template type-checking context, as there is no
// guarantee that any alias re-exports exist in the .d.ts files. It's safe to use direct imports
// in these cases as there is no strict dependency checking during the template type-checking
@@ -132,7 +132,13 @@ export class Environment extends ReferenceEmitEnvironment {
assertSuccessfulReferenceEmit(ngExpr, this.contextFile, 'class');
// Use `translateExpression` to convert the `Expression` into a `ts.Expression`.
return translateExpression(this.contextFile, ngExpr.expression, this.importManager);
const tsExpression = translateExpression(
this.contextFile,
ngExpr.expression,
this.importManager,
);
return new TcbExpr(tempPrint(tsExpression, this.contextFile));
}
private emitTypeParameters(
@@ -142,7 +148,7 @@ export class Environment extends ReferenceEmitEnvironment {
return emitter.emit((ref) => this.referenceType(ref));
}
getPreludeStatements(): ts.Statement[] {
getPreludeStatements(): TcbExpr[] {
return [...this.pipeInstStatements, ...this.typeCtorStatements];
}
}
@@ -34,100 +34,34 @@ import {
SpreadElement,
TaggedTemplateLiteral,
TemplateLiteral,
TemplateLiteralElement,
ThisReceiver,
TypeofExpression,
Unary,
VoidExpression,
} from '@angular/compiler';
import ts from 'typescript';
import {quoteAndEscape, TcbExpr} from './ops/codegen';
import {TypeCheckingConfig} from '../api';
import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics';
import {tsCastToAny, tsNumericExpression} from './ts_util';
import {markIgnoreDiagnostics} from './comments';
/**
* Gets an expression that is cast to any. Currently represented as `0 as any`.
*
* Historically this expression was using `null as any`, but a newly-added check in TypeScript 5.6
* (https://devblogs.microsoft.com/typescript/announcing-typescript-5-6-beta/#disallowed-nullish-and-truthy-checks)
* started flagging it as always being nullish. Other options that were considered:
* - `NaN as any` or `Infinity as any` - not used, because they don't work if the `noLib` compiler
* option is enabled. Also they require more characters.
* - Some flavor of function call, like `isNan(0) as any` - requires even more characters than the
* NaN option and has the same issue with `noLib`.
*/
export function getAnyExpression(): ts.AsExpression {
return ts.factory.createAsExpression(
ts.factory.createNumericLiteral('0'),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
}
/**
* Convert an `AST` to TypeScript code directly, without going through an intermediate `Expression`
* Convert an `AST` to a `TcbExpr` directly, without going through an intermediate `Expression`
* AST.
*/
export function astToTypescript(
export function astToTcbExpr(
ast: AST,
maybeResolve: (ast: AST) => ts.Expression | null,
maybeResolve: (ast: AST) => TcbExpr | null,
config: TypeCheckingConfig,
): ts.Expression {
const translator = new AstTranslator(maybeResolve, config);
): TcbExpr {
const translator = new TcbExprTranslator(maybeResolve, config);
return translator.translate(ast);
}
class AstTranslator implements AstVisitor {
private readonly UNDEFINED = ts.factory.createIdentifier('undefined');
private readonly UNARY_OPS = new Map<string, ts.PrefixUnaryOperator>([
['+', ts.SyntaxKind.PlusToken],
['-', ts.SyntaxKind.MinusToken],
]);
private readonly BINARY_OPS = new Map<string, ts.BinaryOperator>([
['+', ts.SyntaxKind.PlusToken],
['-', ts.SyntaxKind.MinusToken],
['<', ts.SyntaxKind.LessThanToken],
['>', ts.SyntaxKind.GreaterThanToken],
['<=', ts.SyntaxKind.LessThanEqualsToken],
['>=', ts.SyntaxKind.GreaterThanEqualsToken],
['=', ts.SyntaxKind.EqualsToken],
['==', ts.SyntaxKind.EqualsEqualsToken],
['===', ts.SyntaxKind.EqualsEqualsEqualsToken],
['*', ts.SyntaxKind.AsteriskToken],
['**', ts.SyntaxKind.AsteriskAsteriskToken],
['/', ts.SyntaxKind.SlashToken],
['%', ts.SyntaxKind.PercentToken],
['!=', ts.SyntaxKind.ExclamationEqualsToken],
['!==', ts.SyntaxKind.ExclamationEqualsEqualsToken],
['||', ts.SyntaxKind.BarBarToken],
['&&', ts.SyntaxKind.AmpersandAmpersandToken],
['&', ts.SyntaxKind.AmpersandToken],
['|', ts.SyntaxKind.BarToken],
['??', ts.SyntaxKind.QuestionQuestionToken],
['=', ts.SyntaxKind.EqualsToken],
['+=', ts.SyntaxKind.PlusEqualsToken],
['-=', ts.SyntaxKind.MinusEqualsToken],
['*=', ts.SyntaxKind.AsteriskEqualsToken],
['/=', ts.SyntaxKind.SlashEqualsToken],
['%=', ts.SyntaxKind.PercentEqualsToken],
['**=', ts.SyntaxKind.AsteriskAsteriskEqualsToken],
['&&=', ts.SyntaxKind.AmpersandAmpersandEqualsToken],
['||=', ts.SyntaxKind.BarBarEqualsToken],
['??=', ts.SyntaxKind.QuestionQuestionEqualsToken],
['in', ts.SyntaxKind.InKeyword],
['instanceof', ts.SyntaxKind.InstanceOfKeyword],
]);
class TcbExprTranslator implements AstVisitor {
constructor(
private maybeResolve: (ast: AST) => ts.Expression | null,
private maybeResolve: (ast: AST) => TcbExpr | null,
private config: TypeCheckingConfig,
) {}
translate(ast: AST): ts.Expression {
// Skip over an `ASTWithSource` as its `visit` method calls directly into its ast's `visit`,
// which would prevent any custom resolution through `maybeResolve` for that node.
translate(ast: AST): TcbExpr {
if (ast instanceof ASTWithSource) {
ast = ast.ast;
}
@@ -141,37 +75,34 @@ class AstTranslator implements AstVisitor {
return ast.visit(this);
}
visitUnary(ast: Unary): ts.Expression {
visitUnary(ast: Unary): TcbExpr {
const expr = this.translate(ast.expr);
const op = this.UNARY_OPS.get(ast.operator);
if (op === undefined) {
throw new Error(`Unsupported Unary.operator: ${ast.operator}`);
}
const node = wrapForDiagnostics(ts.factory.createPrefixUnaryExpression(op, expr));
addParseSpanInfo(node, ast.sourceSpan);
const node = new TcbExpr(`${ast.operator}${expr.print()}`);
return node.wrapForTypeChecker().addParseSpanInfo(ast.sourceSpan);
}
visitBinary(ast: Binary): TcbExpr {
const lhs = this.translate(ast.left);
const rhs = this.translate(ast.right);
lhs.wrapForTypeChecker();
rhs.wrapForTypeChecker();
const expression = `${lhs.print()} ${ast.operation} ${rhs.print()}`;
const node = new TcbExpr(
ast.operation === '??' || ast.operation === '**' ? `(${expression})` : expression,
);
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitBinary(ast: Binary): ts.Expression {
const lhs = wrapForDiagnostics(this.translate(ast.left));
const rhs = wrapForDiagnostics(this.translate(ast.right));
const op = this.BINARY_OPS.get(ast.operation);
if (op === undefined) {
throw new Error(`Unsupported Binary.operation: ${ast.operation}`);
}
const node = ts.factory.createBinaryExpression(lhs, op, rhs);
addParseSpanInfo(node, ast.sourceSpan);
visitChain(ast: Chain): TcbExpr {
const elements = ast.expressions.map((expr) => this.translate(expr).print());
const node = new TcbExpr(elements.join(', '));
node.wrapForTypeChecker();
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitChain(ast: Chain): ts.Expression {
const elements = ast.expressions.map((expr) => this.translate(expr));
const node = wrapForDiagnostics(ts.factory.createCommaListExpression(elements));
addParseSpanInfo(node, ast.sourceSpan);
return node;
}
visitConditional(ast: Conditional): ts.Expression {
visitConditional(ast: Conditional): TcbExpr {
const condExpr = this.translate(ast.condition);
const trueExpr = this.translate(ast.trueExp);
// Wrap `falseExpr` in parens so that the trailing parse span info is not attributed to the
@@ -181,11 +112,10 @@ class AstTranslator implements AstVisitor {
// is immediately before it:
// `conditional /*1,2*/ ? trueExpr /*3,4*/ : falseExpr /*5,6*/`
// This should be instead be `conditional /*1,2*/ ? trueExpr /*3,4*/ : (falseExpr /*5,6*/)`
const falseExpr = wrapForTypeChecker(this.translate(ast.falseExp));
const node = ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(condExpr, undefined, trueExpr, undefined, falseExpr),
);
addParseSpanInfo(node, ast.sourceSpan);
const falseExpr = this.translate(ast.falseExp).wrapForTypeChecker();
const node = new TcbExpr(
`(${condExpr.print()} ? ${trueExpr.print()} : ${falseExpr.print()})`,
).addParseSpanInfo(ast.sourceSpan);
return node;
}
@@ -197,213 +127,187 @@ class AstTranslator implements AstVisitor {
throw new Error('Method not implemented.');
}
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any) {
return wrapForTypeChecker(
ts.factory.createRegularExpressionLiteral(`/${ast.body}/${ast.flags ?? ''}`),
);
visitRegularExpressionLiteral(ast: RegularExpressionLiteral, context: any): TcbExpr {
const node = new TcbExpr(`/${ast.body}/${ast.flags ?? ''}`);
node.wrapForTypeChecker();
return node;
}
visitInterpolation(ast: Interpolation): ts.Expression {
visitInterpolation(ast: Interpolation): TcbExpr {
// Build up a chain of binary + operations to simulate the string concatenation of the
// interpolation's expressions. The chain is started using an actual string literal to ensure
// the type is inferred as 'string'.
return ast.expressions.reduce(
(lhs: ts.Expression, ast: AST) =>
ts.factory.createBinaryExpression(
lhs,
ts.SyntaxKind.PlusToken,
wrapForTypeChecker(this.translate(ast)),
),
ts.factory.createStringLiteral(''),
);
const exprs = ast.expressions.map((e) => {
const node = this.translate(e);
node.wrapForTypeChecker();
return node.print();
});
return new TcbExpr(`"" + ${exprs.join(' + ')}`);
}
visitKeyedRead(ast: KeyedRead): ts.Expression {
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
visitKeyedRead(ast: KeyedRead): TcbExpr {
const receiver = this.translate(ast.receiver).wrapForTypeChecker();
const key = this.translate(ast.key);
const node = ts.factory.createElementAccessExpression(receiver, key);
addParseSpanInfo(node, ast.sourceSpan);
return node;
return new TcbExpr(`${receiver.print()}[${key.print()}]`).addParseSpanInfo(ast.sourceSpan);
}
visitLiteralArray(ast: LiteralArray): ts.Expression {
visitLiteralArray(ast: LiteralArray): TcbExpr {
const elements = ast.expressions.map((expr) => this.translate(expr));
const literal = ts.factory.createArrayLiteralExpression(elements);
let literal = `[${elements.map((el) => el.print()).join(', ')}]`;
// If strictLiteralTypes is disabled, array literals are cast to `any`.
const node = this.config.strictLiteralTypes ? literal : tsCastToAny(literal);
addParseSpanInfo(node, ast.sourceSpan);
return node;
if (!this.config.strictLiteralTypes) {
literal = `(${literal} as any)`;
}
return new TcbExpr(literal).addParseSpanInfo(ast.sourceSpan);
}
visitLiteralMap(ast: LiteralMap): ts.Expression {
visitLiteralMap(ast: LiteralMap): TcbExpr {
const properties = ast.keys.map((key, idx) => {
const value = this.translate(ast.values[idx]);
if (key.kind === 'property') {
const keyNode = ts.factory.createStringLiteral(key.key);
addParseSpanInfo(keyNode, key.sourceSpan);
return ts.factory.createPropertyAssignment(keyNode, value);
const keyNode = new TcbExpr(quoteAndEscape(key.key));
keyNode.addParseSpanInfo(key.sourceSpan);
return `${keyNode.print()}: ${value.print()}`;
} else {
return ts.factory.createSpreadAssignment(value);
return `...${value.print()}`;
}
});
const literal = ts.factory.createObjectLiteralExpression(properties, true);
// If strictLiteralTypes is disabled, object literals are cast to `any`.
const node = this.config.strictLiteralTypes ? literal : tsCastToAny(literal);
addParseSpanInfo(node, ast.sourceSpan);
return node;
let literal = `{ ${properties.join(', ')} }`;
if (!this.config.strictLiteralTypes) {
// If strictLiteralTypes is disabled, array literals are cast to `any`.
literal = `${literal} as any`;
}
const expression = new TcbExpr(literal).addParseSpanInfo(ast.sourceSpan);
// Always parenthesize the literal, because for DOM bindings we may put it
// directly in the function body at which point TS might parse it as a block.
expression.wrapForTypeChecker();
return expression;
}
visitLiteralPrimitive(ast: LiteralPrimitive): ts.Expression {
let node: ts.Expression;
visitLiteralPrimitive(ast: LiteralPrimitive): TcbExpr {
let node: TcbExpr;
if (ast.value === undefined) {
node = ts.factory.createIdentifier('undefined');
node = new TcbExpr('undefined');
} else if (ast.value === null) {
node = ts.factory.createNull();
node = new TcbExpr('null');
} else if (typeof ast.value === 'string') {
node = ts.factory.createStringLiteral(ast.value);
node = new TcbExpr(quoteAndEscape(ast.value));
} else if (typeof ast.value === 'number') {
node = tsNumericExpression(ast.value);
if (Number.isNaN(ast.value)) {
node = new TcbExpr('NaN');
} else if (!Number.isFinite(ast.value)) {
node = new TcbExpr(ast.value > 0 ? 'Infinity' : '-Infinity');
} else {
node = new TcbExpr(ast.value.toString());
}
} else if (typeof ast.value === 'boolean') {
node = ast.value ? ts.factory.createTrue() : ts.factory.createFalse();
node = new TcbExpr(ast.value + '');
} else {
throw Error(`Unsupported AST value of type ${typeof ast.value}`);
}
addParseSpanInfo(node, ast.sourceSpan);
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitNonNullAssert(ast: NonNullAssert): ts.Expression {
const expr = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createNonNullExpression(expr);
addParseSpanInfo(node, ast.sourceSpan);
return node;
visitNonNullAssert(ast: NonNullAssert): TcbExpr {
const expr = this.translate(ast.expression).wrapForTypeChecker();
return new TcbExpr(`${expr.print()}!`).addParseSpanInfo(ast.sourceSpan);
}
visitPipe(ast: BindingPipe): never {
throw new Error('Method not implemented.');
}
visitPrefixNot(ast: PrefixNot): ts.Expression {
const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createLogicalNot(expression);
addParseSpanInfo(node, ast.sourceSpan);
return node;
visitPrefixNot(ast: PrefixNot): TcbExpr {
const expression = this.translate(ast.expression).wrapForTypeChecker();
return new TcbExpr(`!${expression.print()}`).addParseSpanInfo(ast.sourceSpan);
}
visitTypeofExpression(ast: TypeofExpression): ts.Expression {
const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createTypeOfExpression(expression);
addParseSpanInfo(node, ast.sourceSpan);
return node;
visitTypeofExpression(ast: TypeofExpression): TcbExpr {
const expression = this.translate(ast.expression).wrapForTypeChecker();
return new TcbExpr(`typeof ${expression.print()}`).addParseSpanInfo(ast.sourceSpan);
}
visitVoidExpression(ast: VoidExpression): ts.Expression {
const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createVoidExpression(expression);
addParseSpanInfo(node, ast.sourceSpan);
return node;
visitVoidExpression(ast: VoidExpression): TcbExpr {
const expression = this.translate(ast.expression).wrapForTypeChecker();
return new TcbExpr(`void ${expression.print()}`).addParseSpanInfo(ast.sourceSpan);
}
visitPropertyRead(ast: PropertyRead): ts.Expression {
visitPropertyRead(ast: PropertyRead): TcbExpr {
// This is a normal property read - convert the receiver to an expression and emit the correct
// TypeScript expression to read the property.
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
const name = ts.factory.createPropertyAccessExpression(receiver, ast.name);
addParseSpanInfo(name, ast.nameSpan);
const node = wrapForDiagnostics(name);
addParseSpanInfo(node, ast.sourceSpan);
return node;
const receiver = this.translate(ast.receiver).wrapForTypeChecker();
return new TcbExpr(`${receiver.print()}.${ast.name}`)
.addParseSpanInfo(ast.nameSpan)
.wrapForTypeChecker()
.addParseSpanInfo(ast.sourceSpan);
}
visitSafePropertyRead(ast: SafePropertyRead): ts.Expression {
let node: ts.Expression;
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
visitSafePropertyRead(ast: SafePropertyRead): TcbExpr {
let node: TcbExpr;
const receiver = this.translate(ast.receiver).wrapForTypeChecker();
const name = new TcbExpr(ast.name).addParseSpanInfo(ast.nameSpan);
// The form of safe property reads depends on whether strictness is in use.
if (this.config.strictSafeNavigationTypes) {
// Basically, the return here is either the type of the complete expression with a null-safe
// property read, or `undefined`. So a ternary is used to create an "or" type:
// "a?.b" becomes (0 as any ? a!.b : undefined)
// The type of this expression is (typeof a!.b) | undefined, which is exactly as desired.
const expr = ts.factory.createPropertyAccessExpression(
ts.factory.createNonNullExpression(receiver),
ast.name,
);
addParseSpanInfo(expr, ast.nameSpan);
node = ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(
getAnyExpression(),
undefined,
expr,
undefined,
this.UNDEFINED,
),
);
node = new TcbExpr(`(0 as any ? ${receiver.print()}!.${name.print()} : undefined)`);
} else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// Emulate a View Engine bug where 'any' is inferred for the left-hand side of the safe
// navigation operation. With this bug, the type of the left-hand side is regarded as any.
// Therefore, the left-hand side only needs repeating in the output (to validate it), and then
// 'any' is used for the rest of the expression. This is done using a comma operator:
// "a?.b" becomes (a as any).b, which will of course have type 'any'.
node = ts.factory.createPropertyAccessExpression(tsCastToAny(receiver), ast.name);
node = new TcbExpr(`(${receiver.print()} as any).${name.print()}`);
} else {
// The View Engine bug isn't active, so check the entire type of the expression, but the final
// result is still inferred as `any`.
// "a?.b" becomes (a!.b as any)
const expr = ts.factory.createPropertyAccessExpression(
ts.factory.createNonNullExpression(receiver),
ast.name,
);
addParseSpanInfo(expr, ast.nameSpan);
node = tsCastToAny(expr);
node = new TcbExpr(`(${receiver.print()}!.${name.print()} as any)`);
}
addParseSpanInfo(node, ast.sourceSpan);
return node;
return node.addParseSpanInfo(ast.sourceSpan);
}
visitSafeKeyedRead(ast: SafeKeyedRead): ts.Expression {
const receiver = wrapForDiagnostics(this.translate(ast.receiver));
visitSafeKeyedRead(ast: SafeKeyedRead): TcbExpr {
const receiver = this.translate(ast.receiver).wrapForTypeChecker();
const key = this.translate(ast.key);
let node: ts.Expression;
let node: TcbExpr;
// The form of safe property reads depends on whether strictness is in use.
if (this.config.strictSafeNavigationTypes) {
// "a?.[...]" becomes (0 as any ? a![...] : undefined)
const expr = ts.factory.createElementAccessExpression(
ts.factory.createNonNullExpression(receiver),
key,
);
addParseSpanInfo(expr, ast.sourceSpan);
node = ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(
getAnyExpression(),
undefined,
expr,
undefined,
this.UNDEFINED,
),
const elementAccess = new TcbExpr(`${receiver.print()}![${key.print()}]`).addParseSpanInfo(
ast.sourceSpan,
);
node = new TcbExpr(`(0 as any ? ${elementAccess.print()} : undefined)`);
} else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// "a?.[...]" becomes (a as any)[...]
node = ts.factory.createElementAccessExpression(tsCastToAny(receiver), key);
node = new TcbExpr(`(${receiver.print()} as any)[${key.print()}]`);
} else {
// "a?.[...]" becomes (a!.[...] as any)
const expr = ts.factory.createElementAccessExpression(
ts.factory.createNonNullExpression(receiver),
key,
const elementAccess = new TcbExpr(`${receiver.print()}![${key.print()}]`).addParseSpanInfo(
ast.sourceSpan,
);
addParseSpanInfo(expr, ast.sourceSpan);
node = tsCastToAny(expr);
node = new TcbExpr(`(${elementAccess.print()} as any)`);
}
addParseSpanInfo(node, ast.sourceSpan);
return node;
return node.addParseSpanInfo(ast.sourceSpan);
}
visitCall(ast: Call): ts.Expression {
visitCall(ast: Call): TcbExpr {
const args = ast.args.map((expr) => this.translate(expr));
let expr: ts.Expression;
const receiver = ast.receiver;
let expr: TcbExpr;
// For calls that have a property read as receiver, we have to special-case their emit to avoid
// inserting superfluous parenthesis as they prevent TypeScript from applying a narrowing effect
@@ -413,101 +317,90 @@ class AstTranslator implements AstVisitor {
if (resolved !== null) {
expr = resolved;
} else {
const propertyReceiver = wrapForDiagnostics(this.translate(receiver.receiver));
expr = ts.factory.createPropertyAccessExpression(propertyReceiver, receiver.name);
addParseSpanInfo(expr, receiver.nameSpan);
const propertyReceiver = this.translate(receiver.receiver).wrapForTypeChecker();
expr = new TcbExpr(`${propertyReceiver.print()}.${receiver.name}`).addParseSpanInfo(
receiver.nameSpan,
);
}
} else {
expr = this.translate(receiver);
}
let node: ts.Expression;
let node: TcbExpr;
// Safe property/keyed reads will produce a ternary whose value is nullable.
// We have to generate a similar ternary around the call.
if (ast.receiver instanceof SafePropertyRead || ast.receiver instanceof SafeKeyedRead) {
node = this.convertToSafeCall(ast, expr, args);
} else {
node = ts.factory.createCallExpression(expr, undefined, args);
node = new TcbExpr(`${expr.print()}(${args.map((arg) => arg.print()).join(', ')})`);
}
addParseSpanInfo(node, ast.sourceSpan);
return node;
return node.addParseSpanInfo(ast.sourceSpan);
}
visitSafeCall(ast: SafeCall): ts.Expression {
visitSafeCall(ast: SafeCall): TcbExpr {
const args = ast.args.map((expr) => this.translate(expr));
const expr = wrapForDiagnostics(this.translate(ast.receiver));
const node = this.convertToSafeCall(ast, expr, args);
addParseSpanInfo(node, ast.sourceSpan);
return node;
const expr = this.translate(ast.receiver).wrapForTypeChecker();
return this.convertToSafeCall(ast, expr, args).addParseSpanInfo(ast.sourceSpan);
}
visitTemplateLiteral(ast: TemplateLiteral): ts.TemplateLiteral {
visitTemplateLiteral(ast: TemplateLiteral): TcbExpr {
const length = ast.elements.length;
const head = ast.elements[0];
let result: ts.TemplateLiteral;
let result: string;
if (length === 1) {
result = ts.factory.createNoSubstitutionTemplateLiteral(head.text);
result = `\`${head.text}\``;
} else {
const spans: ts.TemplateSpan[] = [];
let parts = [`\`${head.text}`];
const tailIndex = length - 1;
for (let i = 1; i < tailIndex; i++) {
const middle = ts.factory.createTemplateMiddle(ast.elements[i].text);
spans.push(ts.factory.createTemplateSpan(this.translate(ast.expressions[i - 1]), middle));
const expr = this.translate(ast.expressions[i - 1]);
parts.push(`\${${expr.print()}}${ast.elements[i].text}`);
}
const resolvedExpression = this.translate(ast.expressions[tailIndex - 1]);
const templateTail = ts.factory.createTemplateTail(ast.elements[tailIndex].text);
spans.push(ts.factory.createTemplateSpan(resolvedExpression, templateTail));
result = ts.factory.createTemplateExpression(ts.factory.createTemplateHead(head.text), spans);
parts.push(`\${${resolvedExpression.print()}}${ast.elements[tailIndex].text}\``);
result = parts.join('');
}
return result;
return new TcbExpr(result);
}
visitTemplateLiteralElement(ast: TemplateLiteralElement, context: any) {
visitTemplateLiteralElement() {
throw new Error('Method not implemented');
}
visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral): ts.TaggedTemplateExpression {
return ts.factory.createTaggedTemplateExpression(
this.translate(ast.tag),
undefined,
this.visitTemplateLiteral(ast.template),
);
visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral): TcbExpr {
const tag = this.translate(ast.tag);
const template = this.visitTemplateLiteral(ast.template);
return new TcbExpr(`${tag.print()}${template.print()}`);
}
visitParenthesizedExpression(ast: ParenthesizedExpression): ts.ParenthesizedExpression {
return ts.factory.createParenthesizedExpression(this.translate(ast.expression));
visitParenthesizedExpression(ast: ParenthesizedExpression): TcbExpr {
const expr = this.translate(ast.expression);
return new TcbExpr(`(${expr.print()})`);
}
visitSpreadElement(ast: SpreadElement) {
const expression = wrapForDiagnostics(this.translate(ast.expression));
const node = ts.factory.createSpreadElement(expression);
addParseSpanInfo(node, ast.sourceSpan);
const expression = this.translate(ast.expression);
expression.wrapForTypeChecker();
const node = new TcbExpr(`...${expression.print()}`);
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitEmptyExpr(ast: EmptyExpr, context: any) {
const node = ts.factory.createIdentifier('undefined');
addParseSpanInfo(node, ast.sourceSpan);
visitEmptyExpr(ast: EmptyExpr) {
const node = new TcbExpr('undefined');
node.addParseSpanInfo(ast.sourceSpan);
return node;
}
visitArrowFunction(ast: ArrowFunction): ts.ArrowFunction {
const params = ast.parameters.map((param) => {
const paramNode = ts.factory.createParameterDeclaration(undefined, undefined, param.name);
// Ignore diagnostics on the node to skip diagnostics from `noImplicitAny` since
// users aren't able to set types on the parameters. Note that this is preferable
// to setting their types to `any`, because it allows us to infer the types when
// the arrow function is passed as a callback.
markIgnoreDiagnostics(paramNode);
return paramNode;
});
const body = astToTypescript(
visitArrowFunction(ast: ArrowFunction): TcbExpr {
const params = ast.parameters
.map((param) => new TcbExpr(param.name).markIgnoreDiagnostics().print())
.join(', ');
const body = astToTcbExpr(
ast.body,
(innerAst) => {
if (
@@ -521,8 +414,8 @@ class AstTranslator implements AstVisitor {
const correspondingParam = ast.parameters.find((arg) => arg.name === innerAst.name);
if (correspondingParam) {
const node = ts.factory.createIdentifier(innerAst.name);
addParseSpanInfo(node, innerAst.sourceSpan);
const node = new TcbExpr(innerAst.name);
node.addParseSpanInfo(innerAst.sourceSpan);
return node;
}
@@ -531,41 +424,27 @@ class AstTranslator implements AstVisitor {
this.config,
);
return ts.factory.createArrowFunction(undefined, undefined, params, undefined, undefined, body);
return new TcbExpr(
`${ast.parameters.length === 1 ? params : `(${params})`} => ${body.print()}`,
);
}
private convertToSafeCall(
ast: Call | SafeCall,
expr: ts.Expression,
args: ts.Expression[],
): ts.Expression {
private convertToSafeCall(ast: Call | SafeCall, exprNode: TcbExpr, argNodes: TcbExpr[]): TcbExpr {
const expr = exprNode.print();
const args = argNodes.map((node) => node.print()).join(', ');
if (this.config.strictSafeNavigationTypes) {
// "a?.method(...)" becomes (0 as any ? a!.method(...) : undefined)
const call = ts.factory.createCallExpression(
ts.factory.createNonNullExpression(expr),
undefined,
args,
);
return ts.factory.createParenthesizedExpression(
ts.factory.createConditionalExpression(
getAnyExpression(),
undefined,
call,
undefined,
this.UNDEFINED,
),
);
// (0 as any ? a!.method(...) : undefined)
return new TcbExpr(`(0 as any ? ${expr}!(${args}) : undefined)`);
}
if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) {
// "a?.method(...)" becomes (a as any).method(...)
return ts.factory.createCallExpression(tsCastToAny(expr), undefined, args);
// (a as any).method(...)
return new TcbExpr(`(${expr} as any)(${args})`);
}
// "a?.method(...)" becomes (a!.method(...) as any)
return tsCastToAny(
ts.factory.createCallExpression(ts.factory.createNonNullExpression(expr), undefined, args),
);
// (a!.method(...) as any)
return new TcbExpr(`(${expr}!(${args}) as any)`);
}
}
@@ -129,17 +129,12 @@ export function createHostElement(
* Creates an AST node that can be used as a guard in `if` statements to distinguish TypeScript
* nodes used for checking host bindings from ones used for checking templates.
*/
export function createHostBindingsBlockGuard(): ts.Expression {
export function createHostBindingsBlockGuard(): string {
// Note that the comment text is quite generic. This doesn't really matter, because it is
// used only inside a TCB and there's no way for users to produce a comment there.
// `true /*hostBindings*/`.
const trueExpr = ts.addSyntheticTrailingComment(
ts.factory.createTrue(),
ts.SyntaxKind.MultiLineCommentTrivia,
GUARD_COMMENT_TEXT,
);
// `true /*hostBindingsBlockGuard*/`.
// Wrap the expression in parentheses to ensure that the comment is attached to the correct node.
return ts.factory.createParenthesizedExpression(trueExpr);
return `(true /*${GUARD_COMMENT_TEXT}*/)`;
}
/**
@@ -5,7 +5,8 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import ts from 'typescript';
import {TcbExpr} from './codegen';
/**
* A code generation operation that's involved in the construction of a Type Check Block.
@@ -32,7 +33,7 @@ export abstract class TcbOp {
*/
abstract readonly optional: boolean;
abstract execute(): ts.Expression | null;
abstract execute(): TcbExpr | null;
/**
* Replacement value or operation used while this `TcbOp` is executing (i.e. to resolve circular
@@ -42,13 +43,13 @@ export abstract class TcbOp {
* `TcbOp` can be returned in cases where additional code generation is necessary to deal with
* circular references.
*/
circularFallback(): TcbOp | ts.Expression {
circularFallback(): TcbOp | TcbExpr {
// Value used to break a circular reference between `TcbOp`s.
//
// This value is returned whenever `TcbOp`s have a circular dependency. The
// expression is a non-null assertion of the null value (in TypeScript, the
// expression `null!`). This construction will infer the least narrow type
// for whatever it's assigned to.
return ts.factory.createNonNullExpression(ts.factory.createNull());
return new TcbExpr('null!');
}
}
@@ -9,6 +9,8 @@
import {
AST,
BindingType,
LiteralArray,
LiteralMap,
ParseSourceSpan,
TmplAstBoundAttribute,
TmplAstBoundEvent,
@@ -23,7 +25,7 @@ import {TypeCheckableDirectiveMeta} from '../../api';
import {ClassPropertyName} from '../../../metadata';
import {Reference} from '../../../imports';
import {Context} from './context';
import {tsCastToAny} from '../ts_util';
import {TcbExpr} from './codegen';
export interface TcbBoundAttribute {
value: AST | string;
@@ -50,9 +52,14 @@ export interface TcbDirectiveBoundInput {
field: string;
/**
* The `ts.Expression` corresponding with the input binding expression.
* The `TcbExpr` corresponding with the input binding expression.
*/
expression: ts.Expression;
expression: TcbExpr;
/**
* The input's original value expression.
*/
originalExpression: AST | string;
/**
* The source span of the full attribute binding.
@@ -177,13 +184,13 @@ export function checkSplitTwoWayBinding(
/**
* Potentially widens the type of `expr` according to the type-checking configuration.
*/
export function widenBinding(expr: ts.Expression, tcb: Context): ts.Expression {
export function widenBinding(expr: TcbExpr, tcb: Context, originalValue: string | AST): TcbExpr {
if (!tcb.env.config.checkTypeOfInputBindings) {
// If checking the type of bindings is disabled, cast the resulting expression to 'any'
// before the assignment.
return tsCastToAny(expr);
return new TcbExpr(`((${expr.print()}) as any)`);
} else if (!tcb.env.config.strictNullInputBindings) {
if (ts.isObjectLiteralExpression(expr) || ts.isArrayLiteralExpression(expr)) {
if (originalValue instanceof LiteralMap || originalValue instanceof LiteralArray) {
// Object literals and array literals should not be wrapped in non-null assertions as that
// would cause literals to be prematurely widened, resulting in type errors when assigning
// into a literal type.
@@ -191,7 +198,7 @@ export function widenBinding(expr: ts.Expression, tcb: Context): ts.Expression {
} else {
// If strict null checks are disabled, erase `null` and `undefined` from the type by
// wrapping the expression in a non-null assertion.
return ts.factory.createNonNullExpression(expr);
return new TcbExpr(`(${expr.print()})!`);
}
} else {
// No widening is requested, use the expression as is.
@@ -0,0 +1,149 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler';
import ts from 'typescript';
import {CommentTriviaType, ExpressionIdentifier} from '../comments';
/** Represents an expression generated within a type check block. */
export class TcbExpr {
/** Text for the content containing the expression's location information. */
private spanComment: string | null = null;
/** Text for the content containing the expression's identifier. */
private identifierComment: string | null = null;
/**
* Text of the comment instructing the type checker to
* ignore diagnostics coming from this expression.
*/
private ignoreComment: string | null = null;
constructor(private source: string) {}
/**
* Converts the node's current state to a string.
* @param ignoreComments Whether the comments associated with the expression should be skipped.
*/
print(ignoreComments = false): string {
if (ignoreComments) {
return this.source;
}
return (
this.source +
this.formatComment(this.identifierComment) +
this.formatComment(this.ignoreComment) +
this.formatComment(this.spanComment)
);
}
/**
* Adds a synthetic comment to the expression that represents the parse span of the provided node.
* This comment can later be retrieved as trivia of a node to recover original source locations.
* @param span Span from the parser containing the location information.
*/
addParseSpanInfo(span: AbsoluteSourceSpan | ParseSourceSpan): this {
let start: number;
let end: number;
if (span instanceof AbsoluteSourceSpan) {
start = span.start;
end = span.end;
} else {
start = span.start.offset;
end = span.end.offset;
}
this.spanComment = `${start},${end}`;
return this;
}
/** Marks the expression to be ignored for diagnostics. */
markIgnoreDiagnostics(): this {
this.ignoreComment = `${CommentTriviaType.DIAGNOSTIC}:ignore`;
return this;
}
/**
* Wraps the expression in parenthesis such that inserted
* span comments become attached to the proper node.
*/
wrapForTypeChecker(): this {
this.source = `(${this.print()})`;
this.spanComment = this.identifierComment = this.ignoreComment = null;
return this;
}
/**
* Tags the expression with an identifier.
* @param identifier Identifier to apply to the expression.
*/
addExpressionIdentifier(identifier: ExpressionIdentifier): this {
this.identifierComment = `${CommentTriviaType.EXPRESSION_TYPE_IDENTIFIER}:${identifier}`;
return this;
}
/**
* `toString` implementation meant to catch errors like accidentally
* writing `foo ${expr} bar` instead of `foo ${expr.print()} bar`.
*/
toString(): never {
throw new Error(
'Assertion error: TcbExpr should not be converted to a string through concatenation. ' +
'Use the `print` method instead.',
);
}
/** Format a comment string as a TypeScript comment. */
private formatComment(content: string | null): string {
return content === null || content.length === 0 ? '' : ` /*${content}*/`;
}
}
/**
* Declares a variable with a specific type.
* @param identifier Identifier used to refer to the variable.
* @param type Type that the variable should be initialized to.
*/
export function declareVariable(identifier: TcbExpr, type: TcbExpr): TcbExpr {
type.addExpressionIdentifier(ExpressionIdentifier.VARIABLE_AS_EXPRESSION);
return new TcbExpr(`var ${identifier.print()} = null! as ${type.print()}`);
}
/**
* Formats an array of `TcbExpr` as a block of single statements.
* @param expressions Expressions to format.
* @param singleLine Whether to print them on a single line or across multiple. Defaults to multiple.
*/
export function getStatementsBlock(expressions: TcbExpr[], singleLine = false): string {
let result = '';
for (const expr of expressions) {
result += `${expr.print()};${singleLine ? ' ' : '\n'}`;
}
return result;
}
/** Wraps a string value in quotes and escapes relevant characters. */
export function quoteAndEscape(value: string): string {
// Passing the value through `JSON.stringify` automatically
// escapes quotes and allows us to handle line breaks.
return JSON.stringify(value);
}
let tempPrinter: ts.Printer | null = null;
/**
* Prints a TypeScript node as a string.
*
* @deprecated This is a temporary method until all code generation code has been migrated.
*/
export function tempPrint(node: ts.Node, sourceFile: ts.SourceFile): string {
tempPrinter ??= ts.createPrinter();
return tempPrinter.printNode(ts.EmitHint.Unspecified, node, sourceFile);
}
@@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.dev/license
*/
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Scope} from './scope';
import {addExpressionIdentifier, ExpressionIdentifier, markIgnoreDiagnostics} from '../comments';
import {ExpressionIdentifier} from '../comments';
/**
* A `TcbOp` which generates a completion point for the component context.
@@ -26,11 +26,10 @@ export class TcbComponentContextCompletionOp extends TcbOp {
override readonly optional = false;
override execute(): null {
const ctx = ts.factory.createThis();
const ctxDot = ts.factory.createPropertyAccessExpression(ctx, '');
markIgnoreDiagnostics(ctxDot);
addExpressionIdentifier(ctxDot, ExpressionIdentifier.COMPONENT_COMPLETION);
this.scope.addStatement(ts.factory.createExpressionStatement(ctxDot));
const ctx = new TcbExpr('this.');
ctx.markIgnoreDiagnostics();
ctx.addExpressionIdentifier(ExpressionIdentifier.COMPONENT_COMPLETION);
this.scope.addStatement(ctx);
return null;
}
}
@@ -7,7 +7,6 @@
*/
import {BoundTarget, SchemaMetadata} from '@angular/compiler';
import ts from 'typescript';
import {DomSchemaChecker} from '../dom';
import {OutOfBandDiagnosticRecorder} from '../oob';
import {TypeCheckableDirectiveMeta, TypeCheckId} from '../../api';
@@ -70,8 +69,8 @@ export class Context {
* Currently this uses a monotonically increasing counter, but in the future the variable name
* might change depending on the type of data being stored.
*/
allocateId(): ts.Identifier {
return ts.factory.createIdentifier(`_t${this.nextId++}`);
allocateId(): string {
return `_t${this.nextId++}`;
}
getPipeByName(name: string): PipeMeta | null {
@@ -7,16 +7,13 @@
*/
import {DirectiveOwner, ParseSourceSpan, TmplAstHostElement} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {quoteAndEscape, TcbExpr} from './codegen';
import {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {addExpressionIdentifier, ExpressionIdentifier, markIgnoreDiagnostics} from '../comments';
import {addParseSpanInfo, wrapForDiagnostics} from '../diagnostics';
import {tsCreateVariable} from '../ts_util';
import {ExpressionIdentifier} from '../comments';
import {unwrapWritableSignal} from './expression';
import {getAnyExpression} from '../expression';
import {CustomFormControlType, expandBoundAttributesForField} from './signal_forms';
import {getBoundAttributes, TcbBoundAttribute, TcbDirectiveInput, widenBinding} from './bindings';
import {translateInput} from './inputs';
@@ -50,9 +47,9 @@ export class TcbDirectiveCtorOp extends TcbOp {
return true;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const genericInputs = new Map<string, TcbDirectiveInput>();
const id = this.tcb.allocateId();
const id = new TcbExpr(this.tcb.allocateId());
let boundAttrs: TcbBoundAttribute[];
let span: ParseSourceSpan;
@@ -77,8 +74,7 @@ export class TcbDirectiveCtorOp extends TcbOp {
}
}
addExpressionIdentifier(id, ExpressionIdentifier.DIRECTIVE);
addParseSpanInfo(id, span);
id.addExpressionIdentifier(ExpressionIdentifier.DIRECTIVE).addParseSpanInfo(span);
for (const attr of boundAttrs) {
// Skip text attributes if configured to do so.
@@ -98,6 +94,7 @@ export class TcbDirectiveCtorOp extends TcbOp {
type: 'binding',
field: fieldName,
expression,
originalExpression: attr.value,
sourceSpan: attr.sourceSpan,
isTwoWayBinding,
});
@@ -114,8 +111,8 @@ export class TcbDirectiveCtorOp extends TcbOp {
// Call the type constructor of the directive to infer a type, and assign the directive
// instance.
const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, Array.from(genericInputs.values()));
markIgnoreDiagnostics(typeCtor);
this.scope.addStatement(tsCreateVariable(id, typeCtor));
typeCtor.markIgnoreDiagnostics();
this.scope.addStatement(new TcbExpr(`var ${id.print()} = ${typeCtor.print()}`));
return id;
}
@@ -151,16 +148,11 @@ export class TcbDirectiveCtorCircularFallbackOp extends TcbOp {
return false;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const id = this.tcb.allocateId();
const typeCtor = this.tcb.env.typeCtorFor(this.dir);
const circularPlaceholder = ts.factory.createCallExpression(
typeCtor,
/* typeArguments */ undefined,
[ts.factory.createNonNullExpression(ts.factory.createNull())],
);
this.scope.addStatement(tsCreateVariable(id, circularPlaceholder));
return id;
this.scope.addStatement(new TcbExpr(`var ${id} = ${typeCtor.print()}(null!)`));
return new TcbExpr(id);
}
}
@@ -172,39 +164,39 @@ function tcbCallTypeCtor(
dir: TypeCheckableDirectiveMeta,
tcb: Context,
inputs: TcbDirectiveInput[],
): ts.Expression {
): TcbExpr {
const typeCtor = tcb.env.typeCtorFor(dir);
let literal = '{ ';
// Construct an array of `ts.PropertyAssignment`s for each of the directive's inputs.
const members = inputs.map((input) => {
const propertyName = ts.factory.createStringLiteral(input.field);
// Construct an object literal containing each directive input.
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
const propertyName = quoteAndEscape(input.field);
const isLast = i === inputs.length - 1;
if (input.type === 'binding') {
// For bound inputs, the property is assigned the binding expression.
let expr = widenBinding(input.expression, tcb);
let expr = widenBinding(input.expression, tcb, input.originalExpression);
if (input.isTwoWayBinding && tcb.env.config.allowSignalsInTwoWayBindings) {
expr = unwrapWritableSignal(expr, tcb);
}
const assignment = ts.factory.createPropertyAssignment(
propertyName,
wrapForDiagnostics(expr),
);
addParseSpanInfo(assignment, input.sourceSpan);
return assignment;
const assignment = new TcbExpr(`${propertyName}: ${expr.wrapForTypeChecker().print()}`);
assignment.addParseSpanInfo(input.sourceSpan);
literal += assignment.print();
} else {
// A type constructor is required to be called with all input properties, so any unset
// inputs are simply assigned a value of type `any` to ignore them.
return ts.factory.createPropertyAssignment(propertyName, getAnyExpression());
literal += `${propertyName}: 0 as any`;
}
});
literal += `${isLast ? '' : ','} `;
}
literal += '}';
// Call the `ngTypeCtor` method on the directive class, with an object literal argument created
// from the matched inputs.
return ts.factory.createCallExpression(
/* expression */ typeCtor,
/* typeArguments */ undefined,
/* argumentsArray */ [ts.factory.createObjectLiteralExpression(members)],
);
return new TcbExpr(`${typeCtor.print()}(${literal})`);
}
@@ -11,12 +11,11 @@ import ts from 'typescript';
import type {Context} from './context';
import type {Scope} from './scope';
import {TcbOp} from './base';
import {declareVariable, TcbExpr, tempPrint} from './codegen';
import {TypeCheckableDirectiveMeta} from '../../api';
import {Reference} from '../../../imports';
import {ClassDeclaration} from '../../../reflection';
import {addExpressionIdentifier, ExpressionIdentifier} from '../comments';
import {addParseSpanInfo} from '../diagnostics';
import {tsDeclareVariable} from '../ts_util';
import {ExpressionIdentifier} from '../comments';
/**
* A `TcbOp` which constructs an instance of a directive. For generic directives, generic
@@ -39,15 +38,14 @@ export abstract class TcbDirectiveTypeOpBase extends TcbOp {
return true;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
const rawType = this.tcb.env.referenceType(this.dir.ref);
let type: ts.TypeNode;
let type: TcbExpr;
let span: ParseSourceSpan;
if (this.dir.isGeneric === false || dirRef.node.typeParameters === undefined) {
type = rawType;
type = new TcbExpr(tempPrint(rawType, dirRef.node.getSourceFile()));
} else {
if (!ts.isTypeReferenceNode(rawType)) {
throw new Error(
@@ -57,7 +55,13 @@ export abstract class TcbDirectiveTypeOpBase extends TcbOp {
const typeArguments = dirRef.node.typeParameters.map(() =>
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
type = ts.factory.createTypeReferenceNode(rawType.typeName, typeArguments);
type = new TcbExpr(
tempPrint(
ts.factory.createTypeReferenceNode(rawType.typeName, typeArguments),
dirRef.node.getSourceFile(),
),
);
}
if (this.node instanceof TmplAstHostElement) {
@@ -66,10 +70,10 @@ export abstract class TcbDirectiveTypeOpBase extends TcbOp {
span = this.node.startSourceSpan || this.node.sourceSpan;
}
const id = this.tcb.allocateId();
addExpressionIdentifier(id, ExpressionIdentifier.DIRECTIVE);
addParseSpanInfo(id, span);
this.scope.addStatement(tsDeclareVariable(id, type));
const id = new TcbExpr(this.tcb.allocateId())
.addExpressionIdentifier(ExpressionIdentifier.DIRECTIVE)
.addParseSpanInfo(span);
this.scope.addStatement(declareVariable(id, type));
return id;
}
}
@@ -88,7 +92,7 @@ export class TcbNonGenericDirectiveTypeOp extends TcbDirectiveTypeOpBase {
* Creates a variable declaration for this op's directive of the argument type. Returns the id of
* the newly created variable.
*/
override execute(): ts.Identifier {
override execute(): TcbExpr {
const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
if (this.dir.isGeneric) {
throw new Error(`Assertion Error: expected ${dirRef.debugName} not to be generic.`);
@@ -106,7 +110,7 @@ export class TcbNonGenericDirectiveTypeOp extends TcbDirectiveTypeOpBase {
* type parameters set to `any`.
*/
export class TcbGenericDirectiveTypeWithAnyParamsOp extends TcbDirectiveTypeOpBase {
override execute(): ts.Identifier {
override execute(): TcbExpr {
const dirRef = this.dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
if (dirRef.node.typeParameters === undefined) {
throw new Error(
@@ -7,12 +7,10 @@
*/
import {TmplAstElement} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {tsCreateElement, tsCreateVariable} from '../ts_util';
import {addParseSpanInfo} from '../diagnostics';
/**
* A `TcbOp` which creates an expression for a native DOM element (or web component) from a
@@ -36,12 +34,17 @@ export class TcbElementOp extends TcbOp {
return true;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const id = this.tcb.allocateId();
const idNode = new TcbExpr(id);
idNode.addParseSpanInfo(this.element.startSourceSpan || this.element.sourceSpan);
// Add the declaration of the element using document.createElement.
const initializer = tsCreateElement(this.element.name);
addParseSpanInfo(initializer, this.element.startSourceSpan || this.element.sourceSpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
return id;
const initializer = new TcbExpr(`document.createElement("${this.element.name}")`);
initializer.addParseSpanInfo(this.element.startSourceSpan || this.element.sourceSpan);
const stmt = new TcbExpr(`var ${idNode.print()} = ${initializer.print()}`);
stmt.addParseSpanInfo(this.element.startSourceSpan || this.element.sourceSpan);
this.scope.addStatement(stmt);
return idNode;
}
}
@@ -12,20 +12,17 @@ import {
ImplicitReceiver,
ParsedEventType,
PropertyRead,
ThisReceiver,
TmplAstBoundAttribute,
TmplAstBoundEvent,
TmplAstElement,
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {quoteAndEscape, getStatementsBlock, TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {addParseSpanInfo} from '../diagnostics';
import {TcbExpressionTranslator, unwrapWritableSignal} from './expression';
import {tsCreateVariable} from '../ts_util';
import {addExpressionIdentifier, ExpressionIdentifier} from '../comments';
import {ExpressionIdentifier} from '../comments';
import {checkSplitTwoWayBinding} from './bindings';
import {LocalSymbol} from './references';
@@ -44,7 +41,7 @@ const enum EventParamType {
* `ts.Expression`, with special handling of the `$event` variable that can be used within event
* bindings.
*/
export function tcbEventHandlerExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
export function tcbEventHandlerExpression(ast: AST, tcb: Context, scope: Scope): TcbExpr {
const translator = new TcbEventHandlerTranslator(tcb, scope);
return translator.translate(ast);
}
@@ -72,7 +69,7 @@ export class TcbDirectiveOutputsOp extends TcbOp {
}
override execute(): null {
let dirId: ts.Expression | null = null;
let dirId: TcbExpr | null = null;
const outputs = this.dir.outputs;
for (const output of this.outputs) {
@@ -97,22 +94,17 @@ export class TcbDirectiveOutputsOp extends TcbOp {
if (dirId === null) {
dirId = this.scope.resolve(this.node, this.dir);
}
const outputField = ts.factory.createElementAccessExpression(
dirId,
ts.factory.createStringLiteral(field),
);
addParseSpanInfo(outputField, output.keySpan);
const outputField = new TcbExpr(`${dirId.print()}[${quoteAndEscape(field)}]`);
outputField.addParseSpanInfo(output.keySpan);
if (this.tcb.env.config.checkTypeOfOutputEvents) {
// For strict checking of directive events, generate a call to the `subscribe` method
// on the directive's output field to let type information flow into the handler function's
// `$event` parameter.
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
const subscribeFn = ts.factory.createPropertyAccessExpression(outputField, 'subscribe');
const call = ts.factory.createCallExpression(subscribeFn, /* typeArguments */ undefined, [
handler,
]);
addParseSpanInfo(call, output.sourceSpan);
this.scope.addStatement(ts.factory.createExpressionStatement(call));
const call = new TcbExpr(`${outputField.print()}.subscribe(${handler.print()})`);
call.addParseSpanInfo(output.sourceSpan);
this.scope.addStatement(call);
} else {
// If strict checking of directive events is disabled:
//
@@ -120,9 +112,9 @@ export class TcbDirectiveOutputsOp extends TcbOp {
// of the `TemplateTypeChecker` can still find the node for the class member for the
// output.
// * Emit a handler function where the `$event` parameter has an explicit `any` type.
this.scope.addStatement(ts.factory.createExpressionStatement(outputField));
this.scope.addStatement(outputField);
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
this.scope.addStatement(handler);
}
}
@@ -154,7 +146,7 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
}
override execute(): null {
let elId: ts.Expression | null = null;
let elId: TcbExpr | null = null;
// TODO(alxhub): this could be more efficient.
for (const output of this.outputs) {
@@ -178,31 +170,31 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
if (output.type === ParsedEventType.LegacyAnimation) {
// Animation output bindings always have an `$event` parameter of type `AnimationEvent`.
const eventType = this.tcb.env.config.checkTypeOfAnimationEvents
? this.tcb.env.referenceExternalType('@angular/animations', 'AnimationEvent')
? this.tcb.env.referenceExternalSymbol('@angular/animations', 'AnimationEvent').print()
: EventParamType.Any;
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
this.scope.addStatement(handler);
} else if (output.type === ParsedEventType.Animation) {
const eventType = this.tcb.env.referenceExternalType(
const eventType = this.tcb.env.referenceExternalSymbol(
'@angular/core',
'AnimationCallbackEvent',
);
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType.print());
this.scope.addStatement(handler);
} else if (this.tcb.env.config.checkTypeOfDomEvents) {
// If strict checking of DOM events is enabled, generate a call to `addEventListener` on
// the element instance so that TypeScript's type inference for
// `HTMLElement.addEventListener` using `HTMLElementEventMap` to infer an accurate type for
// `$event` depending on the event name. For unknown event names, TypeScript resorts to the
// base `Event` type.
let target: ts.Expression;
let domEventAssertion: ts.Expression | undefined;
let target: TcbExpr;
let domEventAssertion: TcbExpr | undefined;
// Only check for `window` and `document` since in theory any target can be passed.
if (output.target === 'window' || output.target === 'document') {
target = ts.factory.createIdentifier(output.target);
target = new TcbExpr(output.target);
} else if (elId === null) {
target = elId = this.scope.resolve(this.target);
} else {
@@ -228,26 +220,17 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
if (
this.target instanceof TmplAstElement &&
this.target.isVoid &&
ts.isIdentifier(target) &&
this.tcb.env.config.allowDomEventAssertion
) {
domEventAssertion = ts.factory.createCallExpression(
this.tcb.env.referenceExternalSymbol('@angular/core', 'ɵassertType'),
[ts.factory.createTypeQueryNode(target)],
[
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier(EVENT_PARAMETER),
'target',
),
],
const assertUtil = this.tcb.env.referenceExternalSymbol('@angular/core', 'ɵassertType');
domEventAssertion = new TcbExpr(
`${assertUtil.print()}<typeof ${target.print()}>(${EVENT_PARAMETER}.target)`,
);
}
const propertyAccess = ts.factory.createPropertyAccessExpression(
target,
'addEventListener',
const propertyAccess = new TcbExpr(`${target.print()}.addEventListener`).addParseSpanInfo(
output.keySpan,
);
addParseSpanInfo(propertyAccess, output.keySpan);
const handler = tcbCreateEventHandler(
output,
this.tcb,
@@ -255,18 +238,16 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
EventParamType.Infer,
domEventAssertion,
);
const call = ts.factory.createCallExpression(
/* expression */ propertyAccess,
/* typeArguments */ undefined,
/* arguments */ [ts.factory.createStringLiteral(output.name), handler],
const call = new TcbExpr(
`${propertyAccess.print()}(${quoteAndEscape(output.name)}, ${handler.print()})`,
);
addParseSpanInfo(call, output.sourceSpan);
this.scope.addStatement(ts.factory.createExpressionStatement(call));
call.addParseSpanInfo(output.sourceSpan);
this.scope.addStatement(call);
} else {
// If strict checking of DOM inputs is disabled, emit a handler function where the `$event`
// parameter has an explicit `any` type.
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
this.scope.addStatement(ts.factory.createExpressionStatement(handler));
this.scope.addStatement(handler);
}
}
@@ -275,7 +256,7 @@ export class TcbUnclaimedOutputsOp extends TcbOp {
}
class TcbEventHandlerTranslator extends TcbExpressionTranslator {
protected override resolve(ast: AST): ts.Expression | null {
protected override resolve(ast: AST): TcbExpr | null {
// Recognize a property read on the implicit receiver corresponding with the event parameter
// that is available in event bindings. Since this variable is a parameter of the handler
// function that the converted expression becomes a child of, just create a reference to the
@@ -285,9 +266,7 @@ class TcbEventHandlerTranslator extends TcbExpressionTranslator {
ast.receiver instanceof ImplicitReceiver &&
ast.name === EVENT_PARAMETER
) {
const event = ts.factory.createIdentifier(EVENT_PARAMETER);
addParseSpanInfo(event, ast.nameSpan);
return event;
return new TcbExpr(EVENT_PARAMETER).addParseSpanInfo(ast.nameSpan);
}
return super.resolve(ast);
@@ -315,14 +294,14 @@ function tcbCreateEventHandler(
event: TmplAstBoundEvent,
tcb: Context,
scope: Scope,
eventType: EventParamType | ts.TypeNode,
assertionExpression?: ts.Expression,
): ts.Expression {
eventType: EventParamType | string,
assertionExpression?: TcbExpr,
): TcbExpr {
const handler = tcbEventHandlerExpression(event.handler, tcb, scope);
const statements: ts.Statement[] = [];
const statements: TcbExpr[] = [];
if (assertionExpression !== undefined) {
statements.push(ts.factory.createExpressionStatement(assertionExpression));
statements.push(assertionExpression);
}
// TODO(crisbeto): remove the `checkTwoWayBoundEvents` check in v20.
@@ -332,28 +311,23 @@ function tcbCreateEventHandler(
// this will already be covered by the corresponding input binding, however it allows us to
// handle the case where the input has a wider type than the output (see #58971).
const target = tcb.allocateId();
const assignment = ts.factory.createBinaryExpression(
target,
ts.SyntaxKind.EqualsToken,
ts.factory.createIdentifier(EVENT_PARAMETER),
);
const initializer = tcb.env.config.allowSignalsInTwoWayBindings
? unwrapWritableSignal(handler, tcb)
: handler;
statements.push(
tsCreateVariable(
target,
tcb.env.config.allowSignalsInTwoWayBindings ? unwrapWritableSignal(handler, tcb) : handler,
),
ts.factory.createExpressionStatement(assignment),
new TcbExpr(`var ${target} = ${initializer.print()}`),
new TcbExpr(`${target} = ${EVENT_PARAMETER}`),
);
} else {
statements.push(ts.factory.createExpressionStatement(handler));
statements.push(handler);
}
let eventParamType: ts.TypeNode | undefined;
let eventParamType: string | undefined;
if (eventType === EventParamType.Infer) {
eventParamType = undefined;
} else if (eventType === EventParamType.Any) {
eventParamType = ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
eventParamType = 'any';
} else {
eventParamType = eventType;
}
@@ -361,29 +335,18 @@ function tcbCreateEventHandler(
// Obtain all guards that have been applied to the scope and its parents, as they have to be
// repeated within the handler function for their narrowing to be in effect within the handler.
const guards = scope.guards();
let body = `{\n${getStatementsBlock(statements)} }`;
let body = ts.factory.createBlock(statements);
if (guards !== null) {
// Wrap the body in an `if` statement containing all guards that have to be applied.
body = ts.factory.createBlock([ts.factory.createIfStatement(guards, body)]);
body = `{ if (${guards.print()}) ${body} }`;
}
const eventParam = ts.factory.createParameterDeclaration(
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ EVENT_PARAMETER,
/* questionToken */ undefined,
/* type */ eventParamType,
const eventParam = new TcbExpr(
`${EVENT_PARAMETER}${eventParamType === undefined ? '' : ': ' + eventParamType}`,
);
addExpressionIdentifier(eventParam, ExpressionIdentifier.EVENT_PARAMETER);
eventParam.addExpressionIdentifier(ExpressionIdentifier.EVENT_PARAMETER);
// Return an arrow function instead of a function expression to preserve the `this` context.
return ts.factory.createArrowFunction(
/* modifiers */ undefined,
/* typeParameters */ undefined,
/* parameters */ [eventParam],
/* type */ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
/* equalsGreaterThanToken */ undefined,
/* body */ body,
);
return new TcbExpr(`(${eventParam.print()}): any => ${body}`);
}
@@ -22,11 +22,10 @@ import {
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {astToTypescript, getAnyExpression} from '../expression';
import {addParseSpanInfo, wrapForDiagnostics} from '../diagnostics';
import {markIgnoreDiagnostics} from '../comments';
import {astToTcbExpr} from '../expression';
import {Reference} from '../../../imports';
import {ClassDeclaration} from '../../../reflection';
@@ -34,7 +33,7 @@ import {ClassDeclaration} from '../../../reflection';
* Process an `AST` expression and convert it into a `ts.Expression`, generating references to the
* correct identifiers in the current scope.
*/
export function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
export function tcbExpression(ast: AST, tcb: Context, scope: Scope): TcbExpr {
const translator = new TcbExpressionTranslator(tcb, scope);
return translator.translate(ast);
}
@@ -42,12 +41,12 @@ export function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expressi
/**
* Wraps an expression in an `unwrapSignal` call which extracts the signal's value.
*/
export function unwrapWritableSignal(expression: ts.Expression, tcb: Context): ts.CallExpression {
export function unwrapWritableSignal(expression: TcbExpr, tcb: Context): TcbExpr {
const unwrapRef = tcb.env.referenceExternalSymbol(
R3Identifiers.unwrapWritableSignal.moduleName,
R3Identifiers.unwrapWritableSignal.name,
);
return ts.factory.createCallExpression(unwrapRef, undefined, [expression]);
return new TcbExpr(`${unwrapRef.print()}(${expression.print()})`);
}
/**
@@ -70,7 +69,7 @@ export class TcbExpressionOp extends TcbOp {
override execute(): null {
const expr = tcbExpression(this.expression, this.tcb, this.scope);
this.scope.addStatement(ts.factory.createExpressionStatement(expr));
this.scope.addStatement(expr);
return null;
}
}
@@ -98,7 +97,7 @@ export class TcbConditionOp extends TcbOp {
override execute(): null {
const expr = tcbExpression(this.expression, this.tcb, this.scope);
// Wrap in an if-statement to enable TS2774 for uninvoked signals/functions.
this.scope.addStatement(ts.factory.createIfStatement(expr, ts.factory.createBlock([])));
this.scope.addStatement(new TcbExpr(`if (${expr.print()}) {}`));
return null;
}
}
@@ -109,11 +108,11 @@ export class TcbExpressionTranslator {
protected scope: Scope,
) {}
translate(ast: AST): ts.Expression {
// `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed
translate(ast: AST): TcbExpr {
// `astToTcbExpr` actually does the conversion. A special resolver `tcbResolve` is passed
// which interprets specific expression nodes that interact with the `ImplicitReceiver`. These
// nodes actually refer to identifiers within the current scope.
return astToTypescript(ast, (ast) => this.resolve(ast), this.tcb.env.config);
return astToTcbExpr(ast, (ast) => this.resolve(ast), this.tcb.env.config);
}
/**
@@ -122,7 +121,7 @@ export class TcbExpressionTranslator {
* Some `AST` expressions refer to top-level concepts (references, variables, the component
* context). This method assists in resolving those.
*/
protected resolve(ast: AST): ts.Expression | null {
protected resolve(ast: AST): TcbExpr | null {
if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) {
// Try to resolve a bound target for this expression. If no such target is available, then
// the expression is referencing the top-level component context. In that case, `null` is
@@ -139,10 +138,7 @@ export class TcbExpressionTranslator {
// We don't use `markIgnoreForDiagnostics` here, because it won't prevent duplicate
// diagnostics for nested accesses in cases like `@let value = value.foo.bar.baz`.
if (targetExpression !== null) {
return ts.factory.createAsExpression(
targetExpression,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
return new TcbExpr(`${targetExpression.print()} as any`);
}
}
return targetExpression;
@@ -160,16 +156,14 @@ export class TcbExpressionTranslator {
const targetExpression = this.getTargetNodeExpression(target, read);
const expr = this.translate(ast.right);
const result = ts.factory.createParenthesizedExpression(
ts.factory.createBinaryExpression(targetExpression, ts.SyntaxKind.EqualsToken, expr),
);
addParseSpanInfo(result, read.sourceSpan);
const result = new TcbExpr(`(${targetExpression.print()} = ${expr.print()})`);
result.addParseSpanInfo(read.sourceSpan);
// Ignore diagnostics from TS produced for writes to `@let` and re-report them using
// our own infrastructure. We can't rely on the TS reporting, because it includes
// the name of the auto-generated TCB variable name.
if (target instanceof TmplAstLetDeclaration) {
markIgnoreDiagnostics(result);
result.markIgnoreDiagnostics();
this.tcb.oobRecorder.illegalWriteToLetDeclaration(this.tcb.id, read, target);
}
@@ -187,17 +181,17 @@ export class TcbExpressionTranslator {
// Therefore if `resolve` is called on an `ImplicitReceiver`, it's because no outer
// PropertyRead/Call resolved to a variable or reference, and therefore this is a
// property read or method call on the component context itself.
return ts.factory.createThis();
return new TcbExpr('this');
} else if (ast instanceof BindingPipe) {
const expr = this.translate(ast.exp);
const pipeMeta = this.tcb.getPipeByName(ast.name);
let pipe: ts.Expression | null;
let pipe: TcbExpr | null;
if (pipeMeta === null) {
// No pipe by that name exists in scope. Record this as an error.
this.tcb.oobRecorder.missingPipe(this.tcb.id, ast, this.tcb.hostIsStandalone);
// Use an 'any' value to at least allow the rest of the expression to be checked.
pipe = getAnyExpression();
pipe = new TcbExpr('(0 as any)');
} else if (
pipeMeta.isExplicitlyDeferred &&
this.tcb.boundTarget.getEagerlyUsedPipes().includes(ast.name)
@@ -207,33 +201,22 @@ export class TcbExpressionTranslator {
this.tcb.oobRecorder.deferredPipeUsedEagerly(this.tcb.id, ast);
// Use an 'any' value to at least allow the rest of the expression to be checked.
pipe = getAnyExpression();
pipe = new TcbExpr('(0 as any)');
} else {
// Use a variable declared as the pipe's type.
pipe = this.tcb.env.pipeInst(
pipeMeta.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>,
);
}
const args = ast.args.map((arg) => this.translate(arg));
let methodAccess: ts.Expression = ts.factory.createPropertyAccessExpression(
pipe,
'transform',
);
addParseSpanInfo(methodAccess, ast.nameSpan);
const args = ast.args.map((arg) => this.translate(arg).print());
let methodAccess = new TcbExpr(`${pipe.print()}.transform`).addParseSpanInfo(ast.nameSpan);
if (!this.tcb.env.config.checkTypeOfPipes) {
methodAccess = ts.factory.createAsExpression(
methodAccess,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
methodAccess = new TcbExpr(`(${methodAccess.print()} as any)`);
}
const result = ts.factory.createCallExpression(
/* expression */ methodAccess,
/* typeArguments */ undefined,
/* argumentsArray */ [expr, ...args],
);
addParseSpanInfo(result, ast.sourceSpan);
return result;
const result = new TcbExpr(`${methodAccess.print()}(${[expr.print(), ...args].join(', ')})`);
return result.addParseSpanInfo(ast.sourceSpan);
} else if (
(ast instanceof Call || ast instanceof SafeCall) &&
(ast.receiver instanceof PropertyRead || ast.receiver instanceof SafePropertyRead)
@@ -246,12 +229,8 @@ export class TcbExpressionTranslator {
ast.args.length === 1
) {
const expr = this.translate(ast.args[0]);
const exprAsAny = ts.factory.createAsExpression(
expr,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
const result = ts.factory.createParenthesizedExpression(exprAsAny);
addParseSpanInfo(result, ast.sourceSpan);
const result = new TcbExpr(`(${expr.print()} as any)`);
result.addParseSpanInfo(ast.sourceSpan);
return result;
}
@@ -264,12 +243,11 @@ export class TcbExpressionTranslator {
return null;
}
const receiver = this.getTargetNodeExpression(target, ast);
const method = wrapForDiagnostics(receiver);
addParseSpanInfo(method, ast.receiver.nameSpan);
const args = ast.args.map((arg) => this.translate(arg));
const node = ts.factory.createCallExpression(method, undefined, args);
addParseSpanInfo(node, ast.sourceSpan);
const method = this.getTargetNodeExpression(target, ast);
method.addParseSpanInfo(ast.receiver.nameSpan).wrapForTypeChecker();
const args = ast.args.map((arg) => this.translate(arg).print());
const node = new TcbExpr(`${method.print()}(${args.join(', ')})`);
node.addParseSpanInfo(ast.sourceSpan);
return node;
} else {
// This AST isn't special after all.
@@ -277,9 +255,9 @@ export class TcbExpressionTranslator {
}
}
private getTargetNodeExpression(targetNode: TemplateEntity, expressionNode: AST): ts.Expression {
private getTargetNodeExpression(targetNode: TemplateEntity, expressionNode: AST): TcbExpr {
const expr = this.scope.resolve(targetNode);
addParseSpanInfo(expr, expressionNode.sourceSpan);
expr.addParseSpanInfo(expressionNode.sourceSpan);
return expr;
}
@@ -14,12 +14,11 @@ import {
TmplAstForLoopBlock,
TmplAstVariable,
} from '@angular/compiler';
import ts from 'typescript';
import {tcbExpression, TcbExpressionTranslator} from './expression';
import type {Context} from './context';
import type {Scope} from './scope';
import {TcbOp} from './base';
import {addParseSpanInfo} from '../diagnostics';
import {getStatementsBlock, TcbExpr} from './codegen';
/**
* A `TcbOp` which renders a `for` block as a TypeScript `for...of` loop.
@@ -47,37 +46,20 @@ export class TcbForOfOp extends TcbOp {
null,
);
const initializerId = loopScope.resolve(this.block.item);
if (!ts.isIdentifier(initializerId)) {
throw new Error(
`Could not resolve for loop variable ${this.block.item.name} to an identifier`,
);
}
const initializer = ts.factory.createVariableDeclarationList(
[ts.factory.createVariableDeclaration(initializerId)],
ts.NodeFlags.Const,
);
addParseSpanInfo(initializer, this.block.item.keySpan);
const initializer = new TcbExpr(`const ${initializerId.print()}`);
initializer.addParseSpanInfo(this.block.item.keySpan);
// It's common to have a for loop over a nullable value (e.g. produced by the `async` pipe).
// Add a non-null expression to allow such values to be assigned.
const expression = ts.factory.createNonNullExpression(
tcbExpression(this.block.expression, this.tcb, this.scope),
const expression = new TcbExpr(
`${tcbExpression(this.block.expression, this.tcb, this.scope).print()}!`,
);
const trackTranslator = new TcbForLoopTrackTranslator(this.tcb, loopScope, this.block);
const trackExpression = trackTranslator.translate(this.block.trackBy);
const statements = [
...loopScope.render(),
ts.factory.createExpressionStatement(trackExpression),
];
const block = getStatementsBlock([...loopScope.render(), trackExpression]);
this.scope.addStatement(
ts.factory.createForOfStatement(
undefined,
initializer,
expression,
ts.factory.createBlock(statements),
),
new TcbExpr(`for (${initializer.print()} of ${expression.print()}) {\n${block} }`),
);
return null;
}
}
@@ -102,7 +84,7 @@ export class TcbForLoopTrackTranslator extends TcbExpressionTranslator {
}
}
protected override resolve(ast: AST): ts.Expression | null {
protected override resolve(ast: AST): TcbExpr | null {
if (
ast instanceof PropertyRead &&
(ast.receiver instanceof ImplicitReceiver || ast.receiver instanceof ThisReceiver)
@@ -7,12 +7,10 @@
*/
import {TmplAstHostElement} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {tsCreateElement, tsCreateVariable} from '../ts_util';
import {addParseSpanInfo} from '../diagnostics';
/**
* A `TcbOp` which creates an expression for a the host element of a directive.
@@ -30,11 +28,19 @@ export class TcbHostElementOp extends TcbOp {
super();
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const id = this.tcb.allocateId();
const initializer = tsCreateElement(...this.element.tagNames);
addParseSpanInfo(initializer, this.element.sourceSpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
return id;
let tagNames: string;
if (this.element.tagNames.length === 1) {
tagNames = `"${this.element.tagNames[0]}"`;
} else {
tagNames = `null! as ${this.element.tagNames.map((t) => `"${t}"`).join(' | ')}`;
}
const initializer = new TcbExpr(`document.createElement(${tagNames})`);
initializer.addParseSpanInfo(this.element.sourceSpan);
this.scope.addStatement(new TcbExpr(`var ${id} = ${initializer.print()}`));
return new TcbExpr(id);
}
}
@@ -7,12 +7,11 @@
*/
import {TmplAstIfBlock, TmplAstIfBlockBranch} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {getStatementsBlock, TcbExpr} from './codegen';
import type {Scope} from './scope';
import type {Context} from './context';
import {tcbExpression} from './expression';
import {markIgnoreDiagnostics} from '../comments';
/**
* A `TcbOp` which renders an `if` template block as a TypeScript `if` statement.
@@ -40,7 +39,7 @@ export class TcbIfOp extends TcbOp {
return null;
}
private generateBranch(index: number): ts.Statement | undefined {
private generateBranch(index: number): TcbExpr | undefined {
const branch = this.block.branches[index];
if (!branch) {
@@ -50,7 +49,7 @@ export class TcbIfOp extends TcbOp {
// If the expression is null, it means that it's an `else` statement.
if (branch.expression === null) {
const branchScope = this.getBranchScope(this.scope, branch, index);
return ts.factory.createBlock(branchScope.render());
return new TcbExpr(`{\n${getStatementsBlock(branchScope.render())}}`);
}
// We process the expression first in the parent scope, but create a scope around the block
@@ -63,19 +62,15 @@ export class TcbIfOp extends TcbOp {
let expression = tcbExpression(branch.expression, this.tcb, this.scope);
if (branch.expressionAlias !== null) {
expression = ts.factory.createBinaryExpression(
ts.factory.createParenthesizedExpression(expression),
ts.SyntaxKind.AmpersandAmpersandToken,
outerScope.resolve(branch.expressionAlias),
expression = new TcbExpr(
`(${expression.print()}) && ${outerScope.resolve(branch.expressionAlias).print()}`,
);
}
const bodyScope = this.getBranchScope(outerScope, branch, index);
const ifStatement = `if (${expression.print()}) {\n${getStatementsBlock(bodyScope.render())}}`;
const elseBranch = this.generateBranch(index + 1);
return ts.factory.createIfStatement(
expression,
ts.factory.createBlock(bodyScope.render()),
this.generateBranch(index + 1),
);
return new TcbExpr(ifStatement + (elseBranch ? ' else ' + elseBranch.print() : ''));
}
private getBranchScope(parentScope: Scope, branch: TmplAstIfBlockBranch, index: number): Scope {
@@ -88,8 +83,8 @@ export class TcbIfOp extends TcbOp {
);
}
private generateBranchGuard(index: number): ts.Expression | null {
let guard: ts.Expression | null = null;
private generateBranchGuard(index: number): TcbExpr | null {
let guard: TcbExpr | null = null;
// Since event listeners are inside callbacks, type narrowing doesn't apply to them anymore.
// To recreate the behavior, we generate an expression that negates all the values of the
@@ -111,41 +106,30 @@ export class TcbIfOp extends TcbOp {
}
const expressionScope = this.expressionScopes.get(branch)!;
let expression: ts.Expression;
let expression: TcbExpr;
// We need to recreate the expression and mark it to be ignored for diagnostics,
// because it was already checked as a part of the block's condition and we don't
// want it to produce a duplicate diagnostic.
expression = tcbExpression(branch.expression, this.tcb, expressionScope);
if (branch.expressionAlias !== null) {
expression = ts.factory.createBinaryExpression(
ts.factory.createParenthesizedExpression(expression),
ts.SyntaxKind.AmpersandAmpersandToken,
expressionScope.resolve(branch.expressionAlias),
expression = new TcbExpr(
`(${expression.print()}) && ${expressionScope.resolve(branch.expressionAlias).print()}`,
);
}
markIgnoreDiagnostics(expression);
expression.markIgnoreDiagnostics();
// The expressions of the preceding branches have to be negated
// (e.g. `expr` becomes `!(expr)`) when comparing in the guard, except
// for the branch's own expression which is preserved as is.
const comparisonExpression =
i === index
? expression
: ts.factory.createPrefixUnaryExpression(
ts.SyntaxKind.ExclamationToken,
ts.factory.createParenthesizedExpression(expression),
);
i === index ? expression : new TcbExpr(`!(${expression.print()})`);
// Finally add the expression to the guard with an && operator.
guard =
guard === null
? comparisonExpression
: ts.factory.createBinaryExpression(
guard,
ts.SyntaxKind.AmpersandAmpersandToken,
comparisonExpression,
);
: new TcbExpr(`(${guard.print()}) && (${comparisonExpression.print()})`);
}
return guard;
@@ -22,12 +22,10 @@ import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {TcbOp} from './base';
import {declareVariable, quoteAndEscape, TcbExpr, tempPrint} from './codegen';
import {BindingPropertyName, ClassPropertyName} from '../../../metadata';
import {addParseSpanInfo, wrapForDiagnostics} from '../diagnostics';
import {markIgnoreDiagnostics} from '../comments';
import {REGISTRY} from '../dom';
import {tcbExpression, unwrapWritableSignal} from './expression';
import {tsCreateTypeQueryForCoercedInput, tsDeclareVariable} from '../ts_util';
import {
checkUnsupportedFieldBindings,
CustomFormControlType,
@@ -40,10 +38,10 @@ import {LocalSymbol} from './references';
/**
* Translates the given attribute binding to a `ts.Expression`.
*/
export function translateInput(value: AST | string, tcb: Context, scope: Scope): ts.Expression {
export function translateInput(value: AST | string, tcb: Context, scope: Scope): TcbExpr {
if (typeof value === 'string') {
// For regular attributes with a static string value, use the represented string literal.
return ts.factory.createStringLiteral(value);
return new TcbExpr(quoteAndEscape(value));
} else {
// Produce an expression representing the value of the binding.
return tcbExpression(value, tcb, scope);
@@ -73,7 +71,7 @@ export class TcbDirectiveInputsOp extends TcbOp {
}
override execute(): null {
let dirId: ts.Expression | null = null;
let dirId: TcbExpr | null = null;
// TODO(joost): report duplicate properties
const seenRequiredInputs = new Set<ClassPropertyName>();
@@ -97,12 +95,15 @@ export class TcbDirectiveInputsOp extends TcbOp {
for (const attr of boundAttrs) {
// For bound inputs, the property is assigned the binding expression.
const expr = widenBinding(translateInput(attr.value, this.tcb, this.scope), this.tcb);
let assignment: ts.Expression = wrapForDiagnostics(expr);
let assignment = widenBinding(
translateInput(attr.value, this.tcb, this.scope),
this.tcb,
attr.value,
);
assignment.wrapForTypeChecker();
for (const {fieldName, required, transformType, isSignal, isTwoWayBinding} of attr.inputs) {
let target: ts.LeftHandSideExpression;
let target: TcbExpr;
if (required) {
seenRequiredInputs.add(fieldName);
@@ -115,10 +116,13 @@ export class TcbDirectiveInputsOp extends TcbOp {
// setting the `WriteT` of such `InputSignalWithTransform<_, WriteT>`.
if (this.dir.coercedInputFields.has(fieldName)) {
let type: ts.TypeNode;
let type: TcbExpr;
if (transformType !== null) {
type = this.tcb.env.referenceTransplantedType(new TransplantedType(transformType));
const tsType = this.tcb.env.referenceTransplantedType(
new TransplantedType(transformType),
);
type = new TcbExpr(tempPrint(tsType, transformType.node.getSourceFile()));
} else {
// The input has a coercion declaration which should be used instead of assigning the
// expression into the input field directly. To achieve this, a variable is declared
@@ -132,11 +136,15 @@ export class TcbDirectiveInputsOp extends TcbOp {
);
}
type = tsCreateTypeQueryForCoercedInput(dirTypeRef.typeName, fieldName);
const typeName = ts.isIdentifier(dirTypeRef.typeName)
? dirTypeRef.typeName.text
: tempPrint(dirTypeRef.typeName, dirTypeRef.typeName.getSourceFile());
type = new TcbExpr(`typeof ${typeName}.ngAcceptInputType_${fieldName}`);
}
const id = this.tcb.allocateId();
this.scope.addStatement(tsDeclareVariable(id, type));
const id = new TcbExpr(this.tcb.allocateId());
this.scope.addStatement(declareVariable(id, type));
target = id;
} else if (this.dir.undeclaredInputFields.has(fieldName)) {
@@ -156,18 +164,15 @@ export class TcbDirectiveInputsOp extends TcbOp {
dirId = this.scope.resolve(this.node, this.dir);
}
const id = this.tcb.allocateId();
const id = new TcbExpr(this.tcb.allocateId());
const dirTypeRef = this.tcb.env.referenceType(this.dir.ref);
if (!ts.isTypeReferenceNode(dirTypeRef)) {
throw new Error(
`Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`,
);
}
const type = ts.factory.createIndexedAccessTypeNode(
ts.factory.createTypeQueryNode(dirId as ts.Identifier),
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(fieldName)),
);
const temp = tsDeclareVariable(id, type);
const type = new TcbExpr(`(typeof ${dirId.print()})[${quoteAndEscape(fieldName)}]`);
const temp = declareVariable(id, type);
this.scope.addStatement(temp);
target = id;
} else {
@@ -179,14 +184,8 @@ export class TcbDirectiveInputsOp extends TcbOp {
// when possible. String literal fields may not be valid JS identifiers so we use
// literal element access instead for those cases.
target = this.dir.stringLiteralInputFields.has(fieldName)
? ts.factory.createElementAccessExpression(
dirId,
ts.factory.createStringLiteral(fieldName),
)
: ts.factory.createPropertyAccessExpression(
dirId,
ts.factory.createIdentifier(fieldName),
);
? new TcbExpr(`${dirId.print()}[${quoteAndEscape(fieldName)}]`)
: new TcbExpr(`${dirId.print()}.${fieldName}`);
}
// For signal inputs, we unwrap the target `InputSignal`. Note that
@@ -199,20 +198,12 @@ export class TcbDirectiveInputsOp extends TcbOp {
R3Identifiers.InputSignalBrandWriteType.moduleName,
R3Identifiers.InputSignalBrandWriteType.name,
);
if (
!ts.isIdentifier(inputSignalBrandWriteSymbol) &&
!ts.isPropertyAccessExpression(inputSignalBrandWriteSymbol)
) {
throw new Error(
`Expected identifier or property access for reference to ${R3Identifiers.InputSignalBrandWriteType.name}`,
);
}
target = ts.factory.createElementAccessExpression(target, inputSignalBrandWriteSymbol);
target = new TcbExpr(`${target.print()}[${inputSignalBrandWriteSymbol.print()}]`);
}
if (attr.keySpan !== null) {
addParseSpanInfo(target, attr.keySpan);
target.addParseSpanInfo(attr.keySpan);
}
// Two-way bindings accept `T | WritableSignal<T>` so we have to unwrap the value.
@@ -221,20 +212,17 @@ export class TcbDirectiveInputsOp extends TcbOp {
}
// Finally the assignment is extended by assigning it into the target expression.
assignment = ts.factory.createBinaryExpression(
target,
ts.SyntaxKind.EqualsToken,
assignment,
);
assignment = new TcbExpr(`${target.print()} = ${assignment.print()}`);
}
addParseSpanInfo(assignment, attr.sourceSpan);
assignment.addParseSpanInfo(attr.sourceSpan);
// Ignore diagnostics for text attributes if configured to do so.
if (!this.tcb.env.config.checkTypeOfAttributes && typeof attr.value === 'string') {
markIgnoreDiagnostics(assignment);
assignment.markIgnoreDiagnostics();
}
this.scope.addStatement(ts.factory.createExpressionStatement(assignment));
this.scope.addStatement(assignment);
}
this.checkRequiredInputs(seenRequiredInputs);
@@ -291,7 +279,7 @@ export class TcbUnclaimedInputsOp extends TcbOp {
override execute(): null {
// `this.inputs` contains only those bindings not matched by any directive. These bindings go to
// the element itself.
let elId: ts.Expression | null = null;
let elId: TcbExpr | null = null;
// TODO(alxhub): this could be more efficient.
for (const binding of this.inputs) {
@@ -303,7 +291,11 @@ export class TcbUnclaimedInputsOp extends TcbOp {
continue;
}
const expr = widenBinding(tcbExpression(binding.value, this.tcb, this.scope), this.tcb);
const expr = widenBinding(
tcbExpression(binding.value, this.tcb, this.scope),
this.tcb,
binding.value,
);
if (this.tcb.env.config.checkTypeOfDomBindings && isPropertyBinding) {
if (binding.name !== 'style' && binding.name !== 'class') {
@@ -312,25 +304,18 @@ export class TcbUnclaimedInputsOp extends TcbOp {
}
// A direct binding to a property.
const propertyName = REGISTRY.getMappedPropName(binding.name);
const prop = ts.factory.createElementAccessExpression(
elId,
ts.factory.createStringLiteral(propertyName),
);
const stmt = ts.factory.createBinaryExpression(
prop,
ts.SyntaxKind.EqualsToken,
wrapForDiagnostics(expr),
);
addParseSpanInfo(stmt, binding.sourceSpan);
this.scope.addStatement(ts.factory.createExpressionStatement(stmt));
const stmt = new TcbExpr(
`${elId.print()}[${quoteAndEscape(propertyName)}] = ${expr.wrapForTypeChecker().print()}`,
).addParseSpanInfo(binding.sourceSpan);
this.scope.addStatement(stmt);
} else {
this.scope.addStatement(ts.factory.createExpressionStatement(expr));
this.scope.addStatement(expr);
}
} else {
// A binding to an animation, attribute, class or style. For now, only validate the right-
// hand side of the expression.
// TODO: properly check class and style bindings.
this.scope.addStatement(ts.factory.createExpressionStatement(expr));
this.scope.addStatement(expr);
}
}
@@ -7,8 +7,8 @@
*/
import {AST} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {Context} from './context';
import type {Scope} from './scope';
import {tcbExpression} from './expression';
@@ -29,14 +29,7 @@ export class TcbIntersectionObserverOp extends TcbOp {
override execute(): null {
const options = tcbExpression(this.options, this.tcb, this.scope);
const callback = ts.factory.createNonNullExpression(ts.factory.createNull());
const expression = ts.factory.createNewExpression(
ts.factory.createIdentifier('IntersectionObserver'),
undefined,
[callback, options],
);
this.scope.addStatement(ts.factory.createExpressionStatement(expression));
this.scope.addStatement(new TcbExpr(`new IntersectionObserver(null!, ${options.print()})`));
return null;
}
}
@@ -7,12 +7,10 @@
*/
import {TmplAstLetDeclaration} from '@angular/compiler';
import ts from 'typescript';
import {Context} from './context';
import type {Scope} from './scope';
import {TcbOp} from './base';
import {addParseSpanInfo, wrapForTypeChecker} from '../diagnostics';
import {tsCreateVariable} from '../ts_util';
import {TcbExpr} from './codegen';
import {tcbExpression} from './expression';
/**
@@ -35,14 +33,13 @@ export class TcbLetDeclarationOp extends TcbOp {
*/
override readonly optional = false;
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
addParseSpanInfo(id, this.node.nameSpan);
const value = tcbExpression(this.node.value, this.tcb, this.scope);
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId()).addParseSpanInfo(this.node.nameSpan);
const value = tcbExpression(this.node.value, this.tcb, this.scope).wrapForTypeChecker();
// Value needs to be wrapped, because spans for the expressions inside of it can
// be picked up incorrectly as belonging to the full variable declaration.
const varStatement = tsCreateVariable(id, wrapForTypeChecker(value), ts.NodeFlags.Const);
addParseSpanInfo(varStatement.declarationList.declarations[0], this.node.sourceSpan);
const varStatement = new TcbExpr(`const ${id.print()} = ${value.print()}`);
varStatement.addParseSpanInfo(this.node.sourceSpan);
this.scope.addStatement(varStatement);
return id;
}
@@ -7,7 +7,6 @@
*/
import {
DYNAMIC_TYPE,
TmplAstComponent,
TmplAstDirective,
TmplAstElement,
@@ -17,14 +16,11 @@ import {
TmplAstTemplate,
TmplAstVariable,
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {addParseSpanInfo} from '../diagnostics';
import {tsCreateVariable} from '../ts_util';
import {getAnyExpression} from '../expression';
/** Types that can referenced locally in a template. */
export type LocalSymbol =
@@ -72,9 +68,9 @@ export class TcbReferenceOp extends TcbOp {
// so it can map a reference variable in the template directly to a node in the TCB.
override readonly optional = true;
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
let initializer: ts.Expression =
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId());
let initializer: TcbExpr =
this.target instanceof TmplAstTemplate || this.target instanceof TmplAstElement
? this.scope.resolve(this.target)
: this.scope.resolve(this.host, this.target);
@@ -88,28 +84,18 @@ export class TcbReferenceOp extends TcbOp {
// References to DOM nodes are pinned to 'any' when `checkTypeOfDomReferences` is `false`.
// References to `TemplateRef`s and directives are pinned to 'any' when
// `checkTypeOfNonDomReferences` is `false`.
initializer = ts.factory.createAsExpression(
initializer,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
initializer = new TcbExpr(`${initializer.print()} as any`);
} else if (this.target instanceof TmplAstTemplate) {
// Direct references to an <ng-template> node simply require a value of type
// `TemplateRef<any>`. To get this, an expression of the form
// `(_t1 as any as TemplateRef<any>)` is constructed.
initializer = ts.factory.createAsExpression(
initializer,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
initializer = ts.factory.createAsExpression(
initializer,
this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]),
);
initializer = ts.factory.createParenthesizedExpression(initializer);
const templateRef = this.tcb.env.referenceExternalSymbol('@angular/core', 'TemplateRef');
initializer = new TcbExpr(`(${initializer.print()} as any as ${templateRef.print()}<any>)`);
}
addParseSpanInfo(initializer, this.node.sourceSpan);
addParseSpanInfo(id, this.node.keySpan);
initializer.addParseSpanInfo(this.node.sourceSpan);
id.addParseSpanInfo(this.node.keySpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
this.scope.addStatement(new TcbExpr(`var ${id.print()} = ${initializer.print()}`));
return id;
}
}
@@ -130,9 +116,9 @@ export class TcbInvalidReferenceOp extends TcbOp {
// The declaration of a missing reference is only needed when the reference is resolved.
override readonly optional = true;
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
this.scope.addStatement(tsCreateVariable(id, getAnyExpression()));
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId());
this.scope.addStatement(new TcbExpr(`var ${id.print()} = any`));
return id;
}
}
@@ -7,9 +7,9 @@
*/
import {BindingType, TmplAstComponent, TmplAstElement, TmplAstHostElement} from '@angular/compiler';
import ts from 'typescript';
import {REGISTRY} from '../dom';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {Context} from './context';
import {getComponentTagName} from './selectorless';
@@ -37,7 +37,7 @@ export class TcbDomSchemaCheckerOp extends TcbOp {
return false;
}
override execute(): ts.Expression | null {
override execute(): TcbExpr | null {
const element = this.element;
const isTemplateElement =
element instanceof TmplAstElement || element instanceof TmplAstComponent;
@@ -35,11 +35,11 @@ import {
} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {TypeCheckableDirectiveMeta} from '../../api';
import {Context} from './context';
import {TcbTemplateBodyOp, TcbTemplateContextOp} from './template';
import {TcbElementOp} from './element';
import {addParseSpanInfo} from '../diagnostics';
import {tcbExpression, TcbConditionOp, TcbExpressionOp} from './expression';
import {TcbBlockImplicitVariableOp, TcbBlockVariableOp, TcbTemplateVariableOp} from './variables';
import {TcbComponentContextCompletionOp} from './completions';
@@ -89,7 +89,7 @@ export class Scope {
/**
* A queue of operations which need to be performed to generate the TCB code for this scope.
*
* This array can contain either a `TcbOp` which has yet to be executed, or a `ts.Expression|null`
* This array can contain either a `TcbOp` which has yet to be executed, or a `TcbExpr|null`
* representing the memoized result of executing the operation. As operations are executed, their
* results are written into the `opQueue`, overwriting the original operation.
*
@@ -99,7 +99,7 @@ export class Scope {
* that fits instead. This has the same semantics as TypeScript itself when types are referenced
* circularly.
*/
private opQueue: (TcbOp | ts.Expression | null)[] = [];
private opQueue: (TcbOp | TcbExpr | null)[] = [];
/**
* A map of `TmplAstElement`s to the index of their `TcbElementOp` in the `opQueue`
@@ -138,7 +138,7 @@ export class Scope {
* `TmplAstVariable` nodes) to the index of their `TcbVariableOp`s in the `opQueue`, or to
* pre-resolved variable identifiers.
*/
private varMap = new Map<TmplAstVariable, number | ts.Identifier>();
private varMap = new Map<TmplAstVariable, number | TcbExpr>();
/**
* A map of the names of `TmplAstLetDeclaration`s to the index of their op in the `opQueue`.
@@ -152,26 +152,26 @@ export class Scope {
*
* Executing the `TcbOp`s in the `opQueue` populates this array.
*/
private statements: ts.Statement[] = [];
private statements: TcbExpr[] = [];
/**
* Gets names of the for loop context variables and their types.
*/
private static getForLoopContextVariableTypes() {
return new Map<string, ts.KeywordTypeSyntaxKind>([
['$first', ts.SyntaxKind.BooleanKeyword],
['$last', ts.SyntaxKind.BooleanKeyword],
['$even', ts.SyntaxKind.BooleanKeyword],
['$odd', ts.SyntaxKind.BooleanKeyword],
['$index', ts.SyntaxKind.NumberKeyword],
['$count', ts.SyntaxKind.NumberKeyword],
return new Map<string, string>([
['$first', 'boolean'],
['$last', 'boolean'],
['$even', 'boolean'],
['$odd', 'boolean'],
['$index', 'number'],
['$count', 'number'],
]);
}
private constructor(
private tcb: Context,
private parent: Scope | null = null,
private guard: ts.Expression | null = null,
private guard: TcbExpr | null = null,
) {}
/**
@@ -195,7 +195,7 @@ export class Scope {
| TmplAstHostElement
| null,
children: TmplAstNode[] | null,
guard: ts.Expression | null,
guard: TcbExpr | null,
): Scope {
const scope = new Scope(tcb, parentScope, guard);
@@ -237,8 +237,8 @@ export class Scope {
} else if (scopedNode instanceof TmplAstForLoopBlock) {
// Register the variable for the loop so it can be resolved by
// children. It'll be declared once the loop is created.
const loopInitializer = tcb.allocateId();
addParseSpanInfo(loopInitializer, scopedNode.item.sourceSpan);
const loopInitializer = new TcbExpr(tcb.allocateId());
loopInitializer.addParseSpanInfo(scopedNode.item.sourceSpan);
scope.varMap.set(scopedNode.item, loopInitializer);
const forLoopContextVariableTypes = Scope.getForLoopContextVariableTypes();
@@ -248,9 +248,7 @@ export class Scope {
throw new Error(`Unrecognized for loop context variable ${variable.name}`);
}
const type = ts.factory.createKeywordTypeNode(
forLoopContextVariableTypes.get(variable.value)!,
);
const type = new TcbExpr(forLoopContextVariableTypes.get(variable.value)!);
Scope.registerVariable(
scope,
variable,
@@ -301,34 +299,11 @@ export class Scope {
* @param directive if present, a directive type on a `TmplAstElement` or `TmplAstTemplate` to
* look up instead of the default for an element or template node.
*/
resolve(
node: LocalSymbol,
directive?: TypeCheckableDirectiveMeta,
): ts.Identifier | ts.NonNullExpression {
resolve(node: LocalSymbol, directive?: TypeCheckableDirectiveMeta): TcbExpr {
// Attempt to resolve the operation locally.
const res = this.resolveLocal(node, directive);
if (res !== null) {
// We want to get a clone of the resolved expression and clear the trailing comments
// so they don't continue to appear in every place the expression is used.
// As an example, this would otherwise produce:
// var _t1 /**T:DIR*/ /*1,2*/ = _ctor1();
// _t1 /**T:DIR*/ /*1,2*/.input = 'value';
//
// In addition, returning a clone prevents the consumer of `Scope#resolve` from
// attaching comments at the declaration site.
let clone: ts.Identifier | ts.NonNullExpression;
if (ts.isIdentifier(res)) {
clone = ts.factory.createIdentifier(res.text);
} else if (ts.isNonNullExpression(res)) {
clone = ts.factory.createNonNullExpression(res.expression);
} else {
throw new Error(`Could not resolve ${node} to an Identifier or a NonNullExpression`);
}
ts.setOriginalNode(clone, res);
(clone as any).parent = clone.parent;
return ts.setSyntheticTrailingComments(clone, []);
return res;
} else if (this.parent !== null) {
// Check with the parent.
return this.parent.resolve(node, directive);
@@ -340,14 +315,14 @@ export class Scope {
/**
* Add a statement to this scope.
*/
addStatement(stmt: ts.Statement): void {
addStatement(stmt: TcbExpr): void {
this.statements.push(stmt);
}
/**
* Get the statements.
*/
render(): ts.Statement[] {
render(): TcbExpr[] {
for (let i = 0; i < this.opQueue.length; i++) {
// Optional statements cannot be skipped when we are generating the TCB for use
// by the TemplateTypeChecker.
@@ -361,8 +336,8 @@ export class Scope {
* Returns an expression of all template guards that apply to this scope, including those of
* parent scopes. If no guards have been applied, null is returned.
*/
guards(): ts.Expression | null {
let parentGuards: ts.Expression | null = null;
guards(): TcbExpr | null {
let parentGuards: TcbExpr | null = null;
if (this.parent !== null) {
// Start with the guards from the parent scope, if present.
parentGuards = this.parent.guards();
@@ -374,16 +349,13 @@ export class Scope {
} else if (parentGuards === null) {
// There's no guards from the parent scope, so this scope's guard represents all available
// guards.
return this.guard;
return typeof this.guard === 'string' ? new TcbExpr(this.guard) : this.guard;
} else {
// Both the parent scope and this scope provide a guard, so create a combination of the two.
// It is important that the parent guard is used as left operand, given that it may provide
// narrowing that is required for this scope's guard to be valid.
return ts.factory.createBinaryExpression(
parentGuards,
ts.SyntaxKind.AmpersandAmpersandToken,
this.guard,
);
const guard = typeof this.guard === 'string' ? this.guard : this.guard.print();
return new TcbExpr(`(${parentGuards.print()}) && (${guard})`);
}
}
@@ -418,15 +390,12 @@ export class Scope {
| TmplAstHostElement
| null,
children: TmplAstNode[] | null,
guard: ts.Expression | null,
guard: TcbExpr | null,
): Scope {
return Scope.forNodes(this.tcb, parentScope, scopedNode, children, guard);
}
private resolveLocal(
ref: LocalSymbol,
directive?: TypeCheckableDirectiveMeta,
): ts.Expression | null {
private resolveLocal(ref: LocalSymbol, directive?: TypeCheckableDirectiveMeta): TcbExpr | null {
if (ref instanceof TmplAstReference && this.referenceOpMap.has(ref)) {
return this.resolveOp(this.referenceOpMap.get(ref)!);
} else if (ref instanceof TmplAstLetDeclaration && this.letDeclOpMap.has(ref.name)) {
@@ -435,7 +404,9 @@ export class Scope {
// Resolving a context variable for this template.
// Execute the `TcbVariableOp` associated with the `TmplAstVariable`.
const opIndexOrNode = this.varMap.get(ref)!;
return typeof opIndexOrNode === 'number' ? this.resolveOp(opIndexOrNode) : opIndexOrNode;
return typeof opIndexOrNode === 'number'
? this.resolveOp(opIndexOrNode)
: new TcbExpr(opIndexOrNode.print(true /* ignoreComments */));
} else if (
ref instanceof TmplAstTemplate &&
directive === undefined &&
@@ -469,9 +440,9 @@ export class Scope {
}
/**
* Like `executeOp`, but assert that the operation actually returned `ts.Expression`.
* Like `executeOp`, but assert that the operation actually returned `TcbExpr`.
*/
private resolveOp(opIndex: number): ts.Expression {
private resolveOp(opIndex: number): TcbExpr {
const res = this.executeOp(opIndex, /* skipOptional */ false);
if (res === null) {
throw new Error(`Error resolving operation, got null`);
@@ -486,10 +457,10 @@ export class Scope {
* and also protects against a circular dependency from the operation to itself by temporarily
* setting the operation's result to a special expression.
*/
private executeOp(opIndex: number, skipOptional: boolean): ts.Expression | null {
private executeOp(opIndex: number, skipOptional: boolean): TcbExpr | null {
const op = this.opQueue[opIndex];
if (!(op instanceof TcbOp)) {
return op;
return op === null ? null : new TcbExpr(op.print(true /* ignoreComments */));
}
if (skipOptional && op.optional) {
@@ -500,7 +471,10 @@ export class Scope {
// operation results in a circular dependency, this will prevent an infinite loop and allow for
// the resolution of such cycles.
this.opQueue[opIndex] = op.circularFallback();
const res = op.execute();
let res = op.execute();
if (res !== null) {
res = new TcbExpr(res.print(true /* ignoreComments */));
}
// Once the operation has finished executing, it's safe to cache the real result.
this.opQueue[opIndex] = res;
return res;
@@ -7,10 +7,8 @@
*/
import {TmplAstComponent} from '@angular/compiler';
import ts from 'typescript';
import {addParseSpanInfo} from '../diagnostics';
import {tsCreateElement, tsCreateVariable} from '../ts_util';
import {TcbOp} from './base';
import {TcbExpr} from './codegen';
import {Context} from './context';
import type {Scope} from './scope';
@@ -37,11 +35,13 @@ export class TcbComponentNodeOp extends TcbOp {
super();
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
const id = this.tcb.allocateId();
const initializer = tsCreateElement(getComponentTagName(this.component));
addParseSpanInfo(initializer, this.component.startSourceSpan || this.component.sourceSpan);
this.scope.addStatement(tsCreateVariable(id, initializer));
return id;
const initializer = new TcbExpr(
`document.createElement("${getComponentTagName(this.component)}")`,
);
initializer.addParseSpanInfo(this.component.startSourceSpan || this.component.sourceSpan);
this.scope.addStatement(new TcbExpr(`var ${id} = ${initializer.print()}`));
return new TcbExpr(id);
}
}
@@ -23,10 +23,8 @@ import {
} from '@angular/compiler';
import ts from 'typescript';
import {TypeCheckableDirectiveMeta} from '../../api';
import {markIgnoreDiagnostics} from '../comments';
import {addParseSpanInfo} from '../diagnostics';
import {tsDeclareVariable} from '../ts_util';
import {TcbOp} from './base';
import {declareVariable, TcbExpr} from './codegen';
import {TcbBoundAttribute} from './bindings';
import type {Context} from './context';
import {tcbExpression} from './expression';
@@ -114,23 +112,24 @@ export class TcbNativeFieldOp extends TcbOp {
checkUnsupportedFieldBindings(this.node, this.unsupportedBindingFields, this.tcb);
const expectedType = this.getExpectedTypeFromDomNode(this.node);
const expectedType = new TcbExpr(this.getExpectedTypeFromDomNode(this.node));
const value = extractFieldValue(fieldBinding.value, this.tcb, this.scope);
// Create a variable with the expected type and check that the field value is assignable, e.g.
// var t1 = null! as string | number; t1 = f().value()`.
const id = this.tcb.allocateId();
const assignment = ts.factory.createBinaryExpression(id, ts.SyntaxKind.EqualsToken, value);
addParseSpanInfo(assignment, fieldBinding.valueSpan ?? fieldBinding.sourceSpan);
this.scope.addStatement(tsDeclareVariable(id, expectedType));
this.scope.addStatement(ts.factory.createExpressionStatement(assignment));
const id = new TcbExpr(this.tcb.allocateId());
const assignment = new TcbExpr(`${id.print()} = ${value.print()}`);
assignment.addParseSpanInfo(fieldBinding.valueSpan ?? fieldBinding.sourceSpan);
this.scope.addStatement(declareVariable(id, expectedType));
this.scope.addStatement(assignment);
return null;
}
private getExpectedTypeFromDomNode(node: TmplAstElement): ts.TypeNode {
private getExpectedTypeFromDomNode(node: TmplAstElement): string {
if (node.name === 'textarea' || node.name === 'select') {
// `<textarea>` and `<select>` are always strings.
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
return 'string';
}
if (node.name !== 'input') {
@@ -139,27 +138,18 @@ export class TcbNativeFieldOp extends TcbOp {
switch (this.inputType) {
case 'checkbox':
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
return 'boolean';
case 'number':
case 'range':
case 'datetime-local':
return ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.factory.createLiteralTypeNode(ts.factory.createNull()),
]);
return 'string | number | null';
case 'date':
case 'month':
case 'time':
case 'week':
return ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.factory.createTypeReferenceNode('Date'),
ts.factory.createLiteralTypeNode(ts.factory.createNull()),
]);
return 'string | number | Date | null';
}
const hasDynamicType =
@@ -172,21 +162,15 @@ export class TcbNativeFieldOp extends TcbOp {
// If the type is dynamic, check it as if it can be any of the types above.
if (hasDynamicType) {
return ts.factory.createUnionTypeNode([
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
ts.factory.createTypeReferenceNode('Date'),
ts.factory.createLiteralTypeNode(ts.factory.createNull()),
]);
return 'string | number | boolean | Date | null';
}
// Fall back to string if we couldn't map the type.
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
return 'string';
}
private getUnsupportedType(): ts.TypeNode {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword);
private getUnsupportedType(): string {
return 'never';
}
}
@@ -208,14 +192,12 @@ export class TcbNativeRadioButtonFieldOp extends TcbNativeFieldOp {
if (valueBinding !== undefined) {
// Include an additional expression to check that the `value` is a string.
const id = this.tcb.allocateId();
const id = new TcbExpr(this.tcb.allocateId());
const value = tcbExpression(valueBinding.value, this.tcb, this.scope);
const assignment = ts.factory.createBinaryExpression(id, ts.SyntaxKind.EqualsToken, value);
addParseSpanInfo(assignment, valueBinding.sourceSpan);
this.scope.addStatement(
tsDeclareVariable(id, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)),
);
this.scope.addStatement(ts.factory.createExpressionStatement(assignment));
const assignment = new TcbExpr(`${id.print()} = ${value.print()}`);
assignment.addParseSpanInfo(valueBinding.sourceSpan);
this.scope.addStatement(declareVariable(id, new TcbExpr('string')));
this.scope.addStatement(assignment);
}
return null;
@@ -430,25 +412,17 @@ export function checkUnsupportedFieldBindings(
/**
* Gets an expression that extracts the value of a field binding.
*/
function extractFieldValue(expression: AST, tcb: Context, scope: Scope): ts.Expression {
function extractFieldValue(expression: AST, tcb: Context, scope: Scope): TcbExpr {
// Unwraps the field, e.g. `[field]="f"` turns into `f()`.
const innerCall = ts.factory.createCallExpression(
tcbExpression(expression, tcb, scope),
undefined,
undefined,
);
const innerCall = new TcbExpr(tcbExpression(expression, tcb, scope).print() + '()');
// Note: we ignore diagnostics on this call, because it might not be callable
// (e.g. `undefined` is passed in). Whether the value conforms to `FieldTree` is
// checked using the common inputs op.
markIgnoreDiagnostics(innerCall);
innerCall.markIgnoreDiagnostics();
// Extract the value from the field, e.g. `f().value()`.
return ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(innerCall, 'value'),
undefined,
undefined,
);
return new TcbExpr(`${innerCall.print()}.value()`);
}
/** Checks whether a directive has a model-like input with a specific name. */
@@ -7,9 +7,8 @@
*/
import {TmplAstSwitchBlock, TmplAstSwitchBlockCaseGroup} from '@angular/compiler';
import ts from 'typescript';
import {markIgnoreDiagnostics} from '../comments';
import {TcbOp} from './base';
import {getStatementsBlock, TcbExpr} from './codegen';
import type {Context} from './context';
import {tcbExpression} from './expression';
import type {Scope} from './scope';
@@ -34,7 +33,7 @@ export class TcbSwitchOp extends TcbOp {
override execute(): null {
const switchExpression = tcbExpression(this.block.expression, this.tcb, this.scope);
const clauses = this.block.groups.flatMap<ts.CaseOrDefaultClause>((current) => {
const clauses = this.block.groups.flatMap<TcbExpr>((current) => {
const checkBody = this.tcb.env.config.checkControlFlowBodies;
const clauseScope = this.scope.createChildScope(
this.scope,
@@ -43,74 +42,60 @@ export class TcbSwitchOp extends TcbOp {
checkBody ? this.generateGuard(current, switchExpression) : null,
);
const statements = [...clauseScope.render(), ts.factory.createBreakStatement()];
const statements = [...clauseScope.render(), new TcbExpr('break')];
return current.cases.map((switchCase, index) => {
const statementsForCase = index === current.cases.length - 1 ? statements : [];
return switchCase.expression === null
? ts.factory.createDefaultClause(statementsForCase)
: ts.factory.createCaseClause(
tcbExpression(switchCase.expression, this.tcb, this.scope),
statementsForCase,
);
const statementsStr = getStatementsBlock(
index === current.cases.length - 1 ? statements : [],
true /* singleLine */,
);
const source =
switchCase.expression === null
? `default: ${statementsStr}`
: `case ${tcbExpression(switchCase.expression, this.tcb, this.scope).print()}: ${statementsStr}`;
return new TcbExpr(source);
});
});
if (this.block.exhaustiveCheck) {
const switchValue = tcbExpression(this.block.expression, this.tcb, this.scope);
const exhaustiveId = this.tcb.allocateId();
clauses.push(
ts.factory.createDefaultClause([
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createUniqueName('tcbExhaustive'),
undefined,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
switchValue,
),
],
ts.NodeFlags.Const,
),
),
]),
new TcbExpr(`default: const tcbExhaustive${exhaustiveId}: never = ${switchValue.print()};`),
);
}
this.scope.addStatement(
ts.factory.createSwitchStatement(switchExpression, ts.factory.createCaseBlock(clauses)),
new TcbExpr(
`switch (${switchExpression.print()}) { ${clauses.map((c) => c.print()).join('\n')} }`,
),
);
return null;
}
private generateGuard(
group: TmplAstSwitchBlockCaseGroup,
switchValue: ts.Expression,
): ts.Expression | null {
private generateGuard(group: TmplAstSwitchBlockCaseGroup, switchValue: TcbExpr): TcbExpr | null {
// For non-default cases, the guard needs to compare against the case value, e.g.
// `switchExpression === caseExpression`.
const hasDefault = group.cases.some((c) => c.expression === null);
if (!hasDefault) {
let guard: ts.Expression | null = null;
let guard: TcbExpr | null = null;
for (const switchCase of group.cases) {
if (switchCase.expression !== null) {
// The expression needs to be ignored for diagnostics since it has been checked already.
const expression = tcbExpression(switchCase.expression, this.tcb, this.scope);
markIgnoreDiagnostics(expression);
const comparison = ts.factory.createBinaryExpression(
switchValue,
ts.SyntaxKind.EqualsEqualsEqualsToken,
expression,
);
expression.markIgnoreDiagnostics();
const comparison = new TcbExpr(`${switchValue.print()} === ${expression.print()}`);
if (guard === null) {
guard = comparison;
} else {
guard = ts.factory.createBinaryExpression(guard, ts.SyntaxKind.BarBarToken, comparison);
guard = new TcbExpr(`(${guard.print()}) || (${comparison.print()})`);
}
}
}
@@ -126,7 +111,7 @@ export class TcbSwitchOp extends TcbOp {
// @default {}
// }
// Will produce the guard `expr !== 1 && expr !== 2`.
let guard: ts.Expression | null = null;
let guard: TcbExpr | null = null;
for (const currentGroup of this.block.groups) {
if (currentGroup === group) {
@@ -141,21 +126,13 @@ export class TcbSwitchOp extends TcbOp {
// The expression needs to be ignored for diagnostics since it has been checked already.
const expression = tcbExpression(switchCase.expression, this.tcb, this.scope);
markIgnoreDiagnostics(expression);
const comparison = ts.factory.createBinaryExpression(
switchValue,
ts.SyntaxKind.ExclamationEqualsEqualsToken,
expression,
);
expression.markIgnoreDiagnostics();
const comparison = new TcbExpr(`${switchValue.print()} !== ${expression.print()}`);
if (guard === null) {
guard = comparison;
} else {
guard = ts.factory.createBinaryExpression(
guard,
ts.SyntaxKind.AmpersandAmpersandToken,
comparison,
);
guard = new TcbExpr(`(${guard.print()}) && (${comparison.print()})`);
}
}
}
@@ -7,15 +7,13 @@
*/
import {TmplAstBoundAttribute, TmplAstDirective, TmplAstTemplate} from '@angular/compiler';
import ts from 'typescript';
import {tsCallMethod, tsDeclareVariable} from '../ts_util';
import {addParseSpanInfo} from '../diagnostics';
import {TcbOp} from './base';
import {declareVariable, getStatementsBlock, TcbExpr} from './codegen';
import type {Context} from './context';
import type {Scope} from './scope';
import {TypeCheckableDirectiveMeta} from '../../api';
import {Reference} from '../../../imports';
import {ClassDeclaration} from '../../../reflection';
import {markIgnoreDiagnostics} from '../comments';
import {tcbExpression} from './expression';
/**
@@ -34,12 +32,11 @@ export class TcbTemplateContextOp extends TcbOp {
// The declaration of the context variable is only needed when the context is actually referenced.
override readonly optional = true;
override execute(): ts.Identifier {
override execute(): TcbExpr {
// Allocate a template ctx variable and declare it with an 'any' type. The type of this variable
// may be narrowed as a result of template guard conditions.
const ctx = this.tcb.allocateId();
const type = ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
this.scope.addStatement(tsDeclareVariable(ctx, type));
const ctx = new TcbExpr(this.tcb.allocateId());
this.scope.addStatement(declareVariable(ctx, new TcbExpr('any')));
return ctx;
}
}
@@ -76,8 +73,8 @@ export class TcbTemplateBodyOp extends TcbOp {
// Collect these into `guards` by processing the directives.
// By default the guard is simply `true`.
let guard: ts.Expression | null = null;
const directiveGuards: ts.Expression[] = [];
let guard: TcbExpr | null = null;
const directiveGuards: TcbExpr[] = [];
this.addDirectiveGuards(
directiveGuards,
@@ -98,8 +95,7 @@ export class TcbTemplateBodyOp extends TcbOp {
// Pop the first value and use it as the initializer to reduce(). This way, a single guard
// will be used on its own, but two or more will be combined into binary AND expressions.
guard = directiveGuards.reduce(
(expr, dirGuard) =>
ts.factory.createBinaryExpression(expr, ts.SyntaxKind.AmpersandAmpersandToken, dirGuard),
(expr, dirGuard) => new TcbExpr(`${expr.print()} && ${dirGuard.print()}`),
directiveGuards.pop()!,
);
}
@@ -125,22 +121,19 @@ export class TcbTemplateBodyOp extends TcbOp {
return null;
}
let tmplBlock: ts.Statement = ts.factory.createBlock(statements);
let tmplBlock = `{\n${getStatementsBlock(statements)}}`;
if (guard !== null) {
// The scope has a guard that needs to be applied, so wrap the template block into an `if`
// statement containing the guard expression.
tmplBlock = ts.factory.createIfStatement(
/* expression */ guard,
/* thenStatement */ tmplBlock,
);
tmplBlock = `if (${guard.print()}) ${tmplBlock}`;
}
this.scope.addStatement(tmplBlock);
this.scope.addStatement(new TcbExpr(tmplBlock));
return null;
}
private addDirectiveGuards(
guards: ts.Expression[],
guards: TcbExpr[],
hostNode: TmplAstTemplate | TmplAstDirective,
directives: TypeCheckableDirectiveMeta[] | null,
) {
@@ -174,7 +167,7 @@ export class TcbTemplateBodyOp extends TcbOp {
// The expression has already been checked in the type constructor invocation, so
// it should be ignored when used within a template guard.
markIgnoreDiagnostics(expr);
expr.markIgnoreDiagnostics();
if (guard.type === 'binding') {
// Use the binding expression itself as guard.
@@ -182,11 +175,11 @@ export class TcbTemplateBodyOp extends TcbOp {
} else {
// Call the guard function on the directive with the directive instance and that
// expression.
const guardInvoke = tsCallMethod(dirId, `ngTemplateGuard_${guard.inputName}`, [
dirInstId,
expr,
]);
addParseSpanInfo(guardInvoke, boundInput.value.sourceSpan);
const guardInvoke = new TcbExpr(
`${dirId.print()}.ngTemplateGuard_${guard.inputName}(${dirInstId.print()}, ${expr.print()})`,
);
guardInvoke.addParseSpanInfo(boundInput.value.sourceSpan);
guards.push(guardInvoke);
}
}
@@ -197,9 +190,12 @@ export class TcbTemplateBodyOp extends TcbOp {
if (dir.hasNgTemplateContextGuard) {
if (this.tcb.env.config.applyTemplateContextGuards) {
const ctx = this.scope.resolve(hostNode);
const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]);
markIgnoreDiagnostics(guardInvoke);
addParseSpanInfo(guardInvoke, hostNode.sourceSpan);
const guardInvoke = new TcbExpr(
`${dirId.print()}.ngTemplateContextGuard(${dirInstId.print()}, ${ctx.print()})`,
);
guardInvoke.markIgnoreDiagnostics();
guardInvoke.addParseSpanInfo(hostNode.sourceSpan);
guards.push(guardInvoke);
} else if (
isTemplate &&
@@ -7,12 +7,10 @@
*/
import {TmplAstTemplate, TmplAstVariable} from '@angular/compiler';
import ts from 'typescript';
import {TcbOp} from './base';
import type {Context} from './context';
import type {Scope} from './scope';
import {addParseSpanInfo, wrapForTypeChecker} from '../diagnostics';
import {tsCreateVariable, tsDeclareVariable} from '../ts_util';
import {declareVariable, TcbExpr} from './codegen';
/**
* A `TcbOp` which renders a variable that is implicitly available within a block (e.g. `$count`
@@ -24,7 +22,7 @@ export class TcbBlockImplicitVariableOp extends TcbOp {
constructor(
private tcb: Context,
private scope: Scope,
private type: ts.TypeNode,
private type: TcbExpr,
private variable: TmplAstVariable,
) {
super();
@@ -32,11 +30,11 @@ export class TcbBlockImplicitVariableOp extends TcbOp {
override readonly optional = true;
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
addParseSpanInfo(id, this.variable.keySpan);
const variable = tsDeclareVariable(id, this.type);
addParseSpanInfo(variable.declarationList.declarations[0], this.variable.sourceSpan);
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId());
id.addParseSpanInfo(this.variable.keySpan);
const variable = declareVariable(id, this.type);
variable.addParseSpanInfo(this.variable.sourceSpan);
this.scope.addStatement(variable);
return id;
}
@@ -62,28 +60,23 @@ export class TcbTemplateVariableOp extends TcbOp {
return false;
}
override execute(): ts.Identifier {
override execute(): TcbExpr {
// Look for a context variable for the template.
const ctx = this.scope.resolve(this.template);
// Allocate an identifier for the TmplAstVariable, and initialize it to a read of the variable
// on the template context.
const id = this.tcb.allocateId();
const initializer = ts.factory.createPropertyAccessExpression(
/* expression */ ctx,
/* name */ this.variable.value || '$implicit',
);
addParseSpanInfo(id, this.variable.keySpan);
const id = new TcbExpr(this.tcb.allocateId());
const initializer = new TcbExpr(`${ctx.print()}.${this.variable.value || '$implicit'}`);
id.addParseSpanInfo(this.variable.keySpan);
// Declare the variable, and return its identifier.
let variable: ts.VariableStatement;
if (this.variable.valueSpan !== undefined) {
addParseSpanInfo(initializer, this.variable.valueSpan);
variable = tsCreateVariable(id, wrapForTypeChecker(initializer));
initializer.addParseSpanInfo(this.variable.valueSpan).wrapForTypeChecker();
} else {
variable = tsCreateVariable(id, initializer);
}
addParseSpanInfo(variable.declarationList.declarations[0], this.variable.sourceSpan);
const variable = new TcbExpr(`var ${id.print()} = ${initializer.print()}`);
variable.addParseSpanInfo(this.variable.sourceSpan);
this.scope.addStatement(variable);
return id;
}
@@ -98,7 +91,7 @@ export class TcbBlockVariableOp extends TcbOp {
constructor(
private tcb: Context,
private scope: Scope,
private initializer: ts.Expression,
private initializer: TcbExpr,
private variable: TmplAstVariable,
) {
super();
@@ -108,11 +101,12 @@ export class TcbBlockVariableOp extends TcbOp {
return false;
}
override execute(): ts.Identifier {
const id = this.tcb.allocateId();
addParseSpanInfo(id, this.variable.keySpan);
const variable = tsCreateVariable(id, wrapForTypeChecker(this.initializer));
addParseSpanInfo(variable.declarationList.declarations[0], this.variable.sourceSpan);
override execute(): TcbExpr {
const id = new TcbExpr(this.tcb.allocateId());
id.addParseSpanInfo(this.variable.keySpan);
this.initializer.wrapForTypeChecker();
const variable = new TcbExpr(`var ${id.print()} = ${this.initializer.print()}`);
variable.addParseSpanInfo(this.variable.sourceSpan);
this.scope.addStatement(variable);
return id;
}
@@ -23,7 +23,8 @@ import {
ReferenceEmitter,
} from '../../imports';
import {ReflectionHost} from '../../reflection';
import {ImportManager, translateExpression, translateType} from '../../translator';
import {ImportManager, translateType} from '../../translator';
import {TcbExpr} from './ops/codegen';
/**
* An environment for a given source file that can be used to emit references.
@@ -74,13 +75,20 @@ export class ReferenceEmitEnvironment {
);
}
/**
* Generate a `ts.Expression` that refers to the external symbol. This
* may result in new imports being generated.
*/
referenceExternalSymbol(moduleName: string, name: string): ts.Expression {
const external = new ExternalExpr({moduleName, name});
return translateExpression(this.contextFile, external, this.importManager);
referenceExternalSymbol(moduleName: string, name: string): TcbExpr {
const importResult = this.importManager.addImport({
exportModuleSpecifier: moduleName,
exportSymbolName: name,
requestedFile: this.contextFile,
});
if (ts.isIdentifier(importResult)) {
return new TcbExpr(importResult.text);
} else if (ts.isIdentifier(importResult.expression)) {
return new TcbExpr(`${importResult.expression.text}.${importResult.name.text}`);
}
throw new Error('Unexpected value returned by import manager');
}
/**
@@ -8,179 +8,7 @@
import ts from 'typescript';
import {addExpressionIdentifier, ExpressionIdentifier, hasExpressionIdentifier} from './comments';
/**
* A `Set` of `ts.SyntaxKind`s of `ts.Expression` which are safe to wrap in a `ts.AsExpression`
* without needing to be wrapped in parentheses.
*
* For example, `foo.bar()` is a `ts.CallExpression`, and can be safely cast to `any` with
* `foo.bar() as any`. however, `foo !== bar` is a `ts.BinaryExpression`, and attempting to cast
* without the parentheses yields the expression `foo !== bar as any`. This is semantically
* equivalent to `foo !== (bar as any)`, which is not what was intended. Thus,
* `ts.BinaryExpression`s need to be wrapped in parentheses before casting.
*/
//
let SAFE_TO_CAST_WITHOUT_PARENS: Set<ts.SyntaxKind> | null = null;
export function tsCastToAny(expr: ts.Expression): ts.Expression {
if (SAFE_TO_CAST_WITHOUT_PARENS === null) {
SAFE_TO_CAST_WITHOUT_PARENS = new Set([
// Expressions which are already parenthesized can be cast without further wrapping.
ts.SyntaxKind.ParenthesizedExpression,
// Expressions which form a single lexical unit leave no room for precedence issues with the cast.
ts.SyntaxKind.Identifier,
ts.SyntaxKind.CallExpression,
ts.SyntaxKind.NonNullExpression,
ts.SyntaxKind.ElementAccessExpression,
ts.SyntaxKind.PropertyAccessExpression,
ts.SyntaxKind.ArrayLiteralExpression,
ts.SyntaxKind.ObjectLiteralExpression,
// The same goes for various literals.
ts.SyntaxKind.StringLiteral,
ts.SyntaxKind.NumericLiteral,
ts.SyntaxKind.TrueKeyword,
ts.SyntaxKind.FalseKeyword,
ts.SyntaxKind.NullKeyword,
ts.SyntaxKind.UndefinedKeyword,
]);
}
// Wrap `expr` in parentheses if needed (see `SAFE_TO_CAST_WITHOUT_PARENS` above).
if (!SAFE_TO_CAST_WITHOUT_PARENS.has(expr.kind)) {
expr = ts.factory.createParenthesizedExpression(expr);
}
// The outer expression is always wrapped in parentheses.
return ts.factory.createParenthesizedExpression(
ts.factory.createAsExpression(expr, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)),
);
}
/**
* Create an expression which instantiates an element by its HTML tagName.
*
* Thanks to narrowing of `document.createElement()`, this expression will have its type inferred
* based on the tag name, including for custom elements that have appropriate .d.ts definitions.
*/
export function tsCreateElement(...tagNames: string[]): ts.Expression {
const createElement = ts.factory.createPropertyAccessExpression(
/* expression */ ts.factory.createIdentifier('document'),
'createElement',
);
let arg: ts.Expression;
if (tagNames.length === 1) {
// If there's only one tag name, we can pass it in directly.
arg = ts.factory.createStringLiteral(tagNames[0]);
} else {
// If there's more than one name, we have to generate a union of all the tag names. To do so,
// create an expression in the form of `null! as 'tag-1' | 'tag-2' | 'tag-3'`. This allows
// TypeScript to infer the type as a union of the differnet tags.
const assertedNullExpression = ts.factory.createNonNullExpression(ts.factory.createNull());
const type = ts.factory.createUnionTypeNode(
tagNames.map((tag) => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(tag))),
);
arg = ts.factory.createAsExpression(assertedNullExpression, type);
}
return ts.factory.createCallExpression(
/* expression */ createElement,
/* typeArguments */ undefined,
/* argumentsArray */ [arg],
);
}
/**
* Create a `ts.VariableStatement` which declares a variable without explicit initialization.
*
* The initializer `null!` is used to bypass strict variable initialization checks.
*
* Unlike with `tsCreateVariable`, the type of the variable is explicitly specified.
*/
export function tsDeclareVariable(id: ts.Identifier, type: ts.TypeNode): ts.VariableStatement {
// When we create a variable like `var _t1: boolean = null!`, TypeScript actually infers `_t1`
// to be `never`, instead of a `boolean`. To work around it, we cast the value
// in the initializer, e.g. `var _t1 = null! as boolean;`.
addExpressionIdentifier(type, ExpressionIdentifier.VARIABLE_AS_EXPRESSION);
const initializer: ts.Expression = ts.factory.createAsExpression(
ts.factory.createNonNullExpression(ts.factory.createNull()),
type,
);
const decl = ts.factory.createVariableDeclaration(
/* name */ id,
/* exclamationToken */ undefined,
/* type */ undefined,
/* initializer */ initializer,
);
return ts.factory.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */ [decl],
);
}
/**
* Creates a `ts.TypeQueryNode` for a coerced input.
*
* For example: `typeof MatInput.ngAcceptInputType_value`, where MatInput is `typeName` and `value`
* is the `coercedInputName`.
*
* @param typeName The `EntityName` of the Directive where the static coerced input is defined.
* @param coercedInputName The field name of the coerced input.
*/
export function tsCreateTypeQueryForCoercedInput(
typeName: ts.EntityName,
coercedInputName: string,
): ts.TypeQueryNode {
return ts.factory.createTypeQueryNode(
ts.factory.createQualifiedName(typeName, `ngAcceptInputType_${coercedInputName}`),
);
}
/**
* Create a `ts.VariableStatement` that initializes a variable with a given expression.
*
* Unlike with `tsDeclareVariable`, the type of the variable is inferred from the initializer
* expression.
*/
export function tsCreateVariable(
id: ts.Identifier,
initializer: ts.Expression,
flags: ts.NodeFlags | null = null,
): ts.VariableStatement {
const decl = ts.factory.createVariableDeclaration(
/* name */ id,
/* exclamationToken */ undefined,
/* type */ undefined,
/* initializer */ initializer,
);
return ts.factory.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */ flags === null
? [decl]
: ts.factory.createVariableDeclarationList([decl], flags),
);
}
/**
* Construct a `ts.CallExpression` that calls a method on a receiver.
*/
export function tsCallMethod(
receiver: ts.Expression,
methodName: string,
args: ts.Expression[] = [],
): ts.CallExpression {
const methodAccess = ts.factory.createPropertyAccessExpression(receiver, methodName);
return ts.factory.createCallExpression(
/* expression */ methodAccess,
/* typeArguments */ undefined,
/* argumentsArray */ args,
);
}
import {ExpressionIdentifier, hasExpressionIdentifier} from './comments';
export function isAccessExpression(
node: ts.Node,
@@ -12,7 +12,6 @@ import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection';
import {TypeCheckBlockMetadata} from '../api';
import {addTypeCheckId} from './diagnostics';
import {DomSchemaChecker} from './dom';
import {Environment} from './environment';
import {OutOfBandDiagnosticRecorder} from './oob';
@@ -20,6 +19,7 @@ import {TypeParameterEmitter} from './type_parameter_emitter';
import {createHostBindingsBlockGuard} from './host_bindings';
import {Context, TcbGenericContextBehavior} from './ops/context';
import {Scope} from './ops/scope';
import {getStatementsBlock, tempPrint} from './ops/codegen';
/**
* Given a `ts.ClassDeclaration` for a component, and metadata regarding that component, compose a
@@ -53,7 +53,7 @@ export function generateTypeCheckBlock(
domSchemaChecker: DomSchemaChecker,
oobRecorder: OutOfBandDiagnosticRecorder,
genericContextBehavior: TcbGenericContextBehavior,
): ts.FunctionDeclaration {
): string {
const tcb = new Context(
env,
domSchemaChecker,
@@ -104,8 +104,21 @@ export function generateTypeCheckBlock(
}
}
const paramList = [tcbThisParam(ctxRawType.typeName, typeArguments)];
const statements: ts.Statement[] = [];
const sourceFile = env.contextFile;
const typeParamsStr =
typeParameters === undefined || typeParameters.length === 0
? ''
: `<${typeParameters.map((p) => tempPrint(p, sourceFile)).join(', ')}>`;
const typeArgsStr =
typeArguments === undefined || typeArguments.length === 0
? ''
: `<${typeArguments.map((p) => tempPrint(p, sourceFile)).join(', ')}>`;
const typeRef = ts.isIdentifier(ctxRawType.typeName)
? ctxRawType.typeName.text
: tempPrint(ctxRawType.typeName, sourceFile);
const thisParamStr = `this: ${typeRef}${typeArgsStr}`;
const statements: string[] = [];
// Add the template type checking code.
if (tcb.boundTarget.target.template !== undefined) {
@@ -117,7 +130,7 @@ export function generateTypeCheckBlock(
/* guard */ null,
);
statements.push(renderBlockStatements(env, templateScope, ts.factory.createTrue()));
statements.push(renderBlockStatements(env, templateScope, 'true'));
}
// Add the host bindings type checking code.
@@ -126,48 +139,19 @@ export function generateTypeCheckBlock(
statements.push(renderBlockStatements(env, hostScope, createHostBindingsBlockGuard()));
}
const body = ts.factory.createBlock(statements);
const fnDecl = ts.factory.createFunctionDeclaration(
/* modifiers */ undefined,
/* asteriskToken */ undefined,
/* name */ name,
/* typeParameters */ env.config.useContextGenericType ? typeParameters : undefined,
/* parameters */ paramList,
/* type */ undefined,
/* body */ body,
);
addTypeCheckId(fnDecl, meta.id);
return fnDecl;
const bodyStr = `{\n${statements.join('\n')}\n}`;
const funcDeclStr = `function ${name.text}${typeParamsStr}(${thisParamStr}) ${bodyStr}`;
return `/*${meta.id}*/\n${funcDeclStr}`;
}
function renderBlockStatements(
env: Environment,
scope: Scope,
wrapperExpression: ts.Expression,
): ts.Statement {
function renderBlockStatements(env: Environment, scope: Scope, wrapperExpression: string): string {
// Note: this needs to be called first so that it can populate the prelude statements.
const scopeStatements = scope.render();
const innerBody = ts.factory.createBlock([...env.getPreludeStatements(), ...scopeStatements]);
const statements = getStatementsBlock([...env.getPreludeStatements(), ...scopeStatements]);
// Wrap the body in an if statement. This serves two purposes:
// 1. It allows us to distinguish between the sections of the block (e.g. host or template).
// 2. It allows the `ts.Printer` to produce better-looking output.
return ts.factory.createIfStatement(wrapperExpression, innerBody);
}
/**
* Create the `this` parameter to the top-level TCB function, with the given generic type
* arguments.
*/
function tcbThisParam(
name: ts.EntityName,
typeArguments: ts.TypeNode[] | undefined,
): ts.ParameterDeclaration {
return ts.factory.createParameterDeclaration(
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'this',
/* questionToken */ undefined,
/* type */ ts.factory.createTypeReferenceNode(name, typeArguments),
/* initializer */ undefined,
);
return `if (${wrapperExpression}) {\n${statements}\n}`;
}
@@ -19,6 +19,7 @@ import {OutOfBandDiagnosticRecorder} from './oob';
import {ensureTypeCheckFilePreparationImports} from './tcb_util';
import {generateTypeCheckBlock} from './type_check_block';
import {TcbGenericContextBehavior} from './ops/context';
import {getStatementsBlock, TcbExpr} from './ops/codegen';
/**
* An `Environment` representing the single type-checking file into which most (if not all) Type
@@ -30,7 +31,7 @@ import {TcbGenericContextBehavior} from './ops/context';
*/
export class TypeCheckFile extends Environment {
private nextTcbId = 1;
private tcbStatements: ts.Statement[] = [];
private tcbStatements: string[] = [];
constructor(
readonly fileName: AbsoluteFsPath,
@@ -79,7 +80,7 @@ export class TypeCheckFile extends Environment {
this.tcbStatements.push(fn);
}
render(removeComments: boolean): string {
render(): string {
// NOTE: We are conditionally adding imports whenever we discover signal inputs. This has a
// risk of changing the import graph of the TypeScript program, degrading incremental program
// re-use due to program structure changes. For type check block files, we are ensuring an
@@ -93,7 +94,7 @@ export class TypeCheckFile extends Environment {
);
}
const printer = ts.createPrinter({removeComments});
const printer = ts.createPrinter();
let source = '';
const newImports = importChanges.newImports.get(this.contextFile.fileName);
@@ -104,15 +105,12 @@ export class TypeCheckFile extends Environment {
}
source += '\n';
for (const stmt of this.pipeInstStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
}
for (const stmt of this.typeCtorStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
}
source += getStatementsBlock(this.pipeInstStatements);
source += getStatementsBlock(this.typeCtorStatements);
source += '\n';
for (const stmt of this.tcbStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
source += stmt + '\n';
}
// Ensure the template type-checking file is an ES module. Otherwise, it's interpreted as some
@@ -123,7 +121,7 @@ export class TypeCheckFile extends Environment {
return source;
}
override getPreludeStatements(): ts.Statement[] {
override getPreludeStatements(): TcbExpr[] {
return [];
}
}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ExpressionType, R3Identifiers, WrappedNodeExpr} from '@angular/compiler';
import {R3Identifiers} from '@angular/compiler';
import ts from 'typescript';
import {ClassDeclaration, ReflectionHost} from '../../reflection';
@@ -14,50 +14,37 @@ import {TypeCtorMetadata} from '../api';
import {ReferenceEmitEnvironment} from './reference_emit_environment';
import {checkIfGenericTypeBoundsCanBeEmitted} from './tcb_util';
import {tsCreateTypeQueryForCoercedInput} from './ts_util';
import {quoteAndEscape, TcbExpr, tempPrint} from './ops/codegen';
export function generateTypeCtorDeclarationFn(
env: ReferenceEmitEnvironment,
meta: TypeCtorMetadata,
nodeTypeRef: ts.EntityName,
typeParams: ts.TypeParameterDeclaration[] | undefined,
): ts.Statement {
const rawTypeArgs = typeParams !== undefined ? generateGenericArgs(typeParams) : undefined;
const rawType = ts.factory.createTypeReferenceNode(nodeTypeRef, rawTypeArgs);
const initParam = constructTypeCtorParameter(env, meta, rawType);
): TcbExpr {
const typeArgs = generateGenericArgs(typeParams);
const typeRef = ts.isIdentifier(nodeTypeRef)
? nodeTypeRef.text
: tempPrint(nodeTypeRef, nodeTypeRef.getSourceFile());
const typeRefWithGenerics = `${typeRef}${typeArgs}`;
const initParam = constructTypeCtorParameter(
env,
meta,
nodeTypeRef.getSourceFile(),
typeRef,
typeRefWithGenerics,
);
const typeParameters = typeParametersWithDefaultTypes(typeParams);
let source: string;
if (meta.body) {
const fnType = ts.factory.createFunctionTypeNode(
/* typeParameters */ typeParameters,
/* parameters */ [initParam],
/* type */ rawType,
);
const decl = ts.factory.createVariableDeclaration(
/* name */ meta.fnName,
/* exclamationToken */ undefined,
/* type */ fnType,
/* body */ ts.factory.createNonNullExpression(ts.factory.createNull()),
);
const declList = ts.factory.createVariableDeclarationList([decl], ts.NodeFlags.Const);
return ts.factory.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */ declList,
);
const fnType = `${typeParameters}(${initParam}) => ${typeRefWithGenerics}`;
source = `const ${meta.fnName}: ${fnType} = null!`;
} else {
return ts.factory.createFunctionDeclaration(
/* modifiers */ [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
/* asteriskToken */ undefined,
/* name */ meta.fnName,
/* typeParameters */ typeParameters,
/* parameters */ [initParam],
/* type */ rawType,
/* body */ undefined,
);
source = `declare function ${meta.fnName}${typeParameters}(${initParam}): ${typeRefWithGenerics}`;
}
return new TcbExpr(source);
}
/**
@@ -99,44 +86,37 @@ export function generateInlineTypeCtor(
env: ReferenceEmitEnvironment,
node: ClassDeclaration<ts.ClassDeclaration>,
meta: TypeCtorMetadata,
): ts.MethodDeclaration {
): string {
// Build rawType, a `ts.TypeNode` of the class with its generic parameters passed through from
// the definition without any type bounds. For example, if the class is
// `FooDirective<T extends Bar>`, its rawType would be `FooDirective<T>`.
const rawTypeArgs =
node.typeParameters !== undefined ? generateGenericArgs(node.typeParameters) : undefined;
const rawType = ts.factory.createTypeReferenceNode(node.name, rawTypeArgs);
const initParam = constructTypeCtorParameter(env, meta, rawType);
const typeRef = node.name.text;
const typeRefWithGenerics = `${typeRef}${generateGenericArgs(node.typeParameters)}`;
const initParam = constructTypeCtorParameter(
env,
meta,
node.getSourceFile(),
typeRef,
typeRefWithGenerics,
);
// If this constructor is being generated into a .ts file, then it needs a fake body. The body
// is set to a return of `null!`. If the type constructor is being generated into a .d.ts file,
// it needs no body.
let body: ts.Block | undefined = undefined;
if (meta.body) {
body = ts.factory.createBlock([
ts.factory.createReturnStatement(ts.factory.createNonNullExpression(ts.factory.createNull())),
]);
}
const body = `{ return null!; }`;
const typeParams = typeParametersWithDefaultTypes(node.typeParameters);
// Create the type constructor method declaration.
return ts.factory.createMethodDeclaration(
/* modifiers */ [ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)],
/* asteriskToken */ undefined,
/* name */ meta.fnName,
/* questionToken */ undefined,
/* typeParameters */ typeParametersWithDefaultTypes(node.typeParameters),
/* parameters */ [initParam],
/* type */ rawType,
/* body */ body,
);
return `static ${meta.fnName}${typeParams}(${initParam}): ${typeRefWithGenerics} ${body}`;
}
function constructTypeCtorParameter(
env: ReferenceEmitEnvironment,
meta: TypeCtorMetadata,
rawType: ts.TypeReferenceNode,
): ts.ParameterDeclaration {
sourceFile: ts.SourceFile,
typeRef: string,
typeRefWithGenerics: string,
): string {
// initType is the type of 'init', the single argument to the type constructor method.
// If the Directive has any inputs, its initType will be:
//
@@ -146,90 +126,68 @@ function constructTypeCtorParameter(
// directive will be inferred.
//
// In the special case there are no inputs, initType is set to {}.
let initType: ts.TypeNode | null = null;
let initType: string | null = null;
const plainKeys: ts.LiteralTypeNode[] = [];
const coercedKeys: ts.PropertySignature[] = [];
const signalInputKeys: ts.LiteralTypeNode[] = [];
const plainKeys: string[] = [];
const coercedKeys: string[] = [];
const signalInputKeys: string[] = [];
for (const {classPropertyName, transform, isSignal} of meta.fields.inputs) {
if (isSignal) {
signalInputKeys.push(
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName)),
);
signalInputKeys.push(quoteAndEscape(classPropertyName));
} else if (!meta.coercedInputFields.has(classPropertyName)) {
plainKeys.push(
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName)),
);
plainKeys.push(quoteAndEscape(classPropertyName));
} else {
const coercionType =
transform != null
? transform.type.node
: tsCreateTypeQueryForCoercedInput(rawType.typeName, classPropertyName);
? tempPrint(transform.type.node, sourceFile)
: `typeof ${typeRef}.ngAcceptInputType_${classPropertyName}`;
coercedKeys.push(
ts.factory.createPropertySignature(
/* modifiers */ undefined,
/* name */ classPropertyName,
/* questionToken */ undefined,
/* type */ coercionType,
),
);
coercedKeys.push(`${classPropertyName}: ${coercionType}`);
}
}
if (plainKeys.length > 0) {
// Construct a union of all the field names.
const keyTypeUnion = ts.factory.createUnionTypeNode(plainKeys);
// Construct the Pick<rawType, keyTypeUnion>.
initType = ts.factory.createTypeReferenceNode('Pick', [rawType, keyTypeUnion]);
initType = `Pick<${typeRefWithGenerics}, ${plainKeys.join(' | ')}>`;
}
if (coercedKeys.length > 0) {
const coercedLiteral = ts.factory.createTypeLiteralNode(coercedKeys);
initType =
initType !== null
? ts.factory.createIntersectionTypeNode([initType, coercedLiteral])
: coercedLiteral;
let coercedLiteral = '{\n';
for (const key of coercedKeys) {
coercedLiteral += `${key};\n`;
}
coercedLiteral += '}';
initType = initType !== null ? `${initType} & ${coercedLiteral}` : coercedLiteral;
}
if (signalInputKeys.length > 0) {
const keyTypeUnion = ts.factory.createUnionTypeNode(signalInputKeys);
const keyTypeUnion = signalInputKeys.join(' | ');
// Construct the UnwrapDirectiveSignalInputs<rawType, keyTypeUnion>.
const unwrapDirectiveSignalInputsExpr = env.referenceExternalType(
const unwrapRef = env.referenceExternalSymbol(
R3Identifiers.UnwrapDirectiveSignalInputs.moduleName,
R3Identifiers.UnwrapDirectiveSignalInputs.name,
[
// TODO:
new ExpressionType(new WrappedNodeExpr(rawType)),
new ExpressionType(new WrappedNodeExpr(keyTypeUnion)),
],
);
initType =
initType !== null
? ts.factory.createIntersectionTypeNode([initType, unwrapDirectiveSignalInputsExpr])
: unwrapDirectiveSignalInputsExpr;
const unwrapExpr = `${unwrapRef.print()}<${typeRefWithGenerics}, ${keyTypeUnion}>`;
initType = initType !== null ? `${initType} & ${unwrapExpr}` : unwrapExpr;
}
if (initType === null) {
// Special case - no inputs, outputs, or other fields which could influence the result type.
initType = ts.factory.createTypeLiteralNode([]);
initType = '{}';
}
// Create the 'init' parameter itself.
return ts.factory.createParameterDeclaration(
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'init',
/* questionToken */ undefined,
/* type */ initType,
/* initializer */ undefined,
);
return `init: ${initType}`;
}
function generateGenericArgs(params: ReadonlyArray<ts.TypeParameterDeclaration>): ts.TypeNode[] {
return params.map((param) => ts.factory.createTypeReferenceNode(param.name, undefined));
function generateGenericArgs(
typeParameters: ReadonlyArray<ts.TypeParameterDeclaration> | undefined,
): string {
if (typeParameters === undefined || typeParameters.length === 0) {
return '';
}
return `<${typeParameters.map((param) => param.name.text).join(', ')}>`;
}
export function requiresInlineTypeCtor(
@@ -288,22 +246,21 @@ export function requiresInlineTypeCtor(
*/
function typeParametersWithDefaultTypes(
params: ReadonlyArray<ts.TypeParameterDeclaration> | undefined,
): ts.TypeParameterDeclaration[] | undefined {
if (params === undefined) {
return undefined;
): string {
if (params === undefined || params.length === 0) {
return '';
}
return params.map((param) => {
if (param.default === undefined) {
return ts.factory.updateTypeParameterDeclaration(
param,
param.modifiers,
param.name,
param.constraint,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
);
} else {
return param;
}
const paramStrings = params.map((param) => {
const constraint = param.constraint
? ` extends ${tempPrint(param.constraint, param.getSourceFile())}`
: '';
const defaultValue = ` = ${
param.default ? tempPrint(param.default, param.getSourceFile()) : 'any'
}`;
return `${param.name.text}${constraint}${defaultValue}`;
});
return `<${paramStrings.join(', ')}>`;
}
@@ -40,7 +40,7 @@ describe('type check blocks diagnostics', () => {
// statement, which would wrap it into parenthesis that clutter the expected output.
const TEMPLATE = '{{ m({foo: a, bar: b}) }}';
expect(tcbWithSpans(TEMPLATE)).toContain(
'(this).m /*3,4*/({ "foo" /*6,9*/: ((this).a /*11,12*/) /*11,12*/, "bar" /*14,17*/: ((this).b /*19,20*/) /*19,20*/ } /*5,21*/) /*3,22*/',
'((this).m /*3,4*/(({ "foo" /*6,9*/: ((this).a /*11,12*/) /*11,12*/, "bar" /*14,17*/: ((this).b /*19,20*/) /*19,20*/ } /*5,21*/)) /*3,22*/)',
);
});
@@ -49,7 +49,7 @@ describe('type check blocks diagnostics', () => {
// statement, which would wrap it into parenthesis that clutter the expected output.
const TEMPLATE = '{{ m({a, b}) }}';
expect(tcbWithSpans(TEMPLATE)).toContain(
'((this).m /*3,4*/({ "a" /*6,7*/: ((this).a /*6,7*/) /*6,7*/, "b" /*9,10*/: ((this).b /*9,10*/) /*9,10*/ } /*5,11*/) /*3,12*/)',
'((this).m /*3,4*/(({ "a" /*6,7*/: ((this).a /*6,7*/) /*6,7*/, "b" /*9,10*/: ((this).b /*9,10*/) /*9,10*/ } /*5,11*/)) /*3,12*/)',
);
});
@@ -31,7 +31,7 @@ describe('type check blocks', () => {
it('should generate literal map expressions', () => {
const TEMPLATE = '{{ method({foo: a, bar: b}) }}';
expect(tcb(TEMPLATE)).toContain('(this).method({ "foo": ((this).a), "bar": ((this).b) })');
expect(tcb(TEMPLATE)).toContain('(this).method(({ "foo": ((this).a), "bar": ((this).b) }))');
});
it('should generate literal array expressions', () => {
@@ -39,6 +39,11 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE)).toContain('(this).method([((this).a), ((this).b)])');
});
it('should parenthesize literal maps bound to DOM properties', () => {
const TEMPLATE = '<div [class]="{foo: true, bar: false}"></div>';
expect(tcb(TEMPLATE)).toContain('if (true) { ({ "foo": true, "bar": false }); }');
});
it('should handle non-null assertions', () => {
const TEMPLATE = `{{a!}}`;
expect(tcb(TEMPLATE)).toContain('((((this).a))!)');
@@ -69,9 +74,11 @@ describe('type check blocks', () => {
it('should handle nullish coalescing operator', () => {
expect(tcb('{{ a ?? b }}')).toContain('((((this).a)) ?? (((this).b)))');
expect(tcb('{{ a ?? b ?? c }}')).toContain('(((((this).a)) ?? (((this).b))) ?? (((this).c)))');
expect(tcb('{{ a ?? b ?? c }}')).toContain(
'((((((this).a)) ?? (((this).b)))) ?? (((this).c)))',
);
expect(tcb('{{ (a ?? b) + (c ?? e) }}')).toContain(
'((((((this).a)) ?? (((this).b)))) + (((((this).c)) ?? (((this).e)))))',
'(((((((this).a)) ?? (((this).b))))) + ((((((this).c)) ?? (((this).e))))))',
);
});
@@ -104,14 +111,14 @@ describe('type check blocks', () => {
it('should handle exponentiation expressions', () => {
expect(tcb('{{a * b ** c + d}}')).toContain(
'(((((this).a)) * ((((this).b)) ** (((this).c)))) + (((this).d)))',
'(((((this).a)) * (((((this).b)) ** (((this).c))))) + (((this).d)))',
);
expect(tcb('{{a ** b ** c}}')).toContain('((((this).a)) ** ((((this).b)) ** (((this).c))))');
expect(tcb('{{a ** b ** c}}')).toContain('((((this).a)) ** (((((this).b)) ** (((this).c)))))');
});
it('should handle "in" expressions', () => {
expect(tcb(`{{'bar' in {bar: 'bar'} }}`)).toContain(`(("bar") in ({ "bar": "bar" }))`);
expect(tcb(`{{!('bar' in {bar: 'bar'}) }}`)).toContain(`!((("bar") in ({ "bar": "bar" })))`);
expect(tcb(`{{'bar' in {bar: 'bar'} }}`)).toContain(`(("bar") in (({ "bar": "bar" })))`);
expect(tcb(`{{!('bar' in {bar: 'bar'}) }}`)).toContain(`!((("bar") in (({ "bar": "bar" }))))`);
});
it('should handle "instanceof" expressions', () => {
@@ -201,19 +208,19 @@ describe('type check blocks', () => {
it('should handle template literals', () => {
expect(tcb('{{ `hello world` }}')).toContain('"" + (`hello world`);');
expect(tcb('{{ `hello \\${name}!!!` }}')).toContain('"" + (`hello \\${name}!!!`);');
expect(tcb('{{ `hello ${name}!!!` }}')).toContain('"" + (`hello ${((this).name)}!!!`);');
expect(tcb('{{ `${a} - ${b} - ${c}` }}')).toContain(
'"" + (`${((this).a)} - ${((this).b)} - ${((this).c)}`);',
);
});
it('should handle tagged template literals', () => {
expect(tcb('{{ tag`hello world` }}')).toContain('"" + (((this).tag) `hello world`);');
expect(tcb('{{ tag`hello \\${name}!!!` }}')).toContain(
'"" + (((this).tag) `hello \\${name}!!!`);',
expect(tcb('{{ tag`hello world` }}')).toContain('"" + (((this).tag)`hello world`);');
expect(tcb('{{ tag`hello ${name}!!!` }}')).toContain(
'"" + (((this).tag)`hello ${((this).name)}!!!`);',
);
expect(tcb('{{ tag`${a} - ${b} - ${c}` }}')).toContain(
'"" + (((this).tag) `${((this).a)} - ${((this).b)} - ${((this).c)}`);',
'"" + (((this).tag)`${((this).a)} - ${((this).b)} - ${((this).c)}`);',
);
});
@@ -306,7 +313,7 @@ describe('type check blocks', () => {
);
});
it('should generate circular references between two directives correctly', () => {
it('should generate circular references between two generic directives correctly', () => {
const TEMPLATE = `
<div #a="dirA" dir-a [inputA]="b">A</div>
<div #b="dirB" dir-b [inputB]="a">B</div>
@@ -339,7 +346,7 @@ describe('type check blocks', () => {
'var _t2 = _ctor2({ "inputB": (_t3) }); ' +
'var _t1 = _t2; ' +
'_t4.inputA = (_t1); ' +
'_t2.inputB = (_t3);',
'_t2.inputB = ((_t3));',
);
});
@@ -1195,8 +1202,8 @@ describe('type check blocks', () => {
strictNullInputBindings: false,
};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t1.dirInput = (((this).a)!);');
expect(block).toContain('((this).b)!;');
expect(block).toContain('_t1.dirInput = ((((this).a))!);');
expect(block).toContain('(((this).b))!;');
});
});
@@ -1215,8 +1222,8 @@ describe('type check blocks', () => {
checkTypeOfInputBindings: false,
};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('_t1.dirInput = ((((this).a) as any));');
expect(block).toContain('(((this).b) as any);');
expect(block).toContain('_t1.dirInput = (((((this).a)) as any));');
expect(block).toContain('((((this).b)) as any);');
});
it('should wrap the cast to any in parentheses when required', () => {
@@ -1843,7 +1850,7 @@ describe('type check blocks', () => {
}
`;
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) { }');
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) {}');
});
it('should generate `prefetch when` trigger', () => {
@@ -1853,7 +1860,7 @@ describe('type check blocks', () => {
}
`;
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) { }');
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) {}');
});
it('should generate `hydrate when` trigger', () => {
@@ -1863,7 +1870,7 @@ describe('type check blocks', () => {
}
`;
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) { }');
expect(tcb(TEMPLATE)).toContain('if (((this).shouldShow()) && (((this).isVisible))) {}');
});
it('should generate options for `viewport` trigger', () => {
@@ -1876,7 +1883,7 @@ describe('type check blocks', () => {
`;
expect(tcb(TEMPLATE)).toContain(
'new IntersectionObserver(null!, { "rootMargin": "123px" }); "" + ((this).main()); "" + ((this).placeholder());',
'new IntersectionObserver(null!, ({ "rootMargin": "123px" })); "" + ((this).main()); "" + ((this).placeholder());',
);
});
});
@@ -1920,13 +1927,13 @@ describe('type check blocks', () => {
expect(result).toContain(`if ((((this).expr)) === (0)) { (this).zero(); }`);
expect(result).toContain(
`if (!((((this).expr)) === (0)) && (((this).expr)) === (1)) { (this).one(); }`,
`if ((!((((this).expr)) === (0))) && ((((this).expr)) === (1))) { (this).one(); }`,
);
expect(result).toContain(
`if (!((((this).expr)) === (0)) && !((((this).expr)) === (1)) && (((this).expr)) === (2)) { (this).two(); }`,
`if (((!((((this).expr)) === (0))) && (!((((this).expr)) === (1)))) && ((((this).expr)) === (2))) { (this).two(); }`,
);
expect(result).toContain(
`if (!((((this).expr)) === (0)) && !((((this).expr)) === (1)) && !((((this).expr)) === (2))) { (this).otherwise(); }`,
`if (((!((((this).expr)) === (0))) && (!((((this).expr)) === (1)))) && (!((((this).expr)) === (2)))) { (this).otherwise(); }`,
);
});
@@ -1936,7 +1943,7 @@ describe('type check blocks', () => {
}`;
expect(tcb(TEMPLATE)).toContain(
'var _t1 = ((((this).expr)) === (1)); if (((((this).expr)) === (1)) && _t1) { "" + (_t1); } } }',
'var _t1 = ((((this).expr)) === (1)); if (((((this).expr)) === (1)) && _t1) { "" + (_t1); }; } }',
);
});
@@ -2089,7 +2096,7 @@ describe('type check blocks', () => {
expect(result).toContain(`if (((this).expr) === 1) { (this).one(); }`);
expect(result).toContain(`if (((this).expr) === 2) { (this).two(); }`);
expect(result).toContain(
`if (((this).expr) !== 1 && ((this).expr) !== 2) { (this).default(); }`,
`if ((((this).expr) !== 1) && (((this).expr) !== 2)) { (this).default(); }`,
);
});
@@ -2114,12 +2121,12 @@ describe('type check blocks', () => {
'var _t1 = null! as any; { var _t2 = (_t1.exp); switch (_t2()) { ' +
'case "one": "" + ((this).one()); break; ' +
'case "two": "" + ((this).two()); break; ' +
'default: "" + ((this).default()); break; } }',
'default: "" + ((this).default()); break; }; }',
);
});
it('should handle an empty switch block', () => {
expect(tcb('@switch (expr) {}')).toContain('if (true) { switch (((this).expr)) { } }');
expect(tcb('@switch (expr) {}')).toContain('if (true) { switch (((this).expr)) { }; }');
});
it('should not generate the body of a switch block if checkControlFlowBodies is disabled', () => {
@@ -2159,7 +2166,7 @@ describe('type check blocks', () => {
'switch (((this).expr)) { ' +
'case 1: "" + ((this).one()); break; ' +
'case 2: "" + ((this).two()); break; ' +
'default: const tcbExhaustive_1: never = ((this).expr);',
'default: const tcbExhaustive_t1: never = ((this).expr);',
);
});
@@ -2172,7 +2179,7 @@ describe('type check blocks', () => {
`;
const SOURCE = `
export class TestComponent {
expr!: 1|2;
expr!: 1|2;
}
`;
expect(diagnose(TEMPLATE, SOURCE, undefined, [], undefined, {noUnusedLocals: true})).toEqual([
@@ -2305,7 +2312,7 @@ describe('type check blocks', () => {
expect(result).toContain('for (const _t1 of ((this).items)!) { var _t2 = null! as number;');
expect(result).toContain('"" + (_t1) + (_t2)');
expect(result).toContain('for (const _t3 of ((_t1).items)!) { var _t4 = null! as number;');
expect(result).toContain('"" + (_t1) + (_t2) + (_t3) + (_t4)');
expect(result).toContain('"" + (_t1) + ((_t2)) + (_t3) + (_t4)');
});
it('should generate the tracking expression of a for loop', () => {
@@ -2543,7 +2550,7 @@ describe('type check blocks', () => {
const block = selectorlessTcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('var _t1 = null! as i0.Dir;');
expect(block).toContain('_t1.someInput = (((this).value));');
expect(block).toContain('if (((this).value)) { "" + (((this).value)); } }');
expect(block).toContain('if (((this).value)) { "" + (((this).value)); }; }');
});
it('should generate bindings for unclaimed component inputs', () => {
@@ -64,7 +64,7 @@ runInEachFileSystem(() => {
/* reflector */ null!,
host,
);
const sf = file.render(false /* removeComments */);
const sf = file.render();
expect(sf).toContain('export const IS_A_MODULE = true;');
});
@@ -297,24 +297,23 @@ export const ALL_ENABLED_CONFIG: Readonly<TypeCheckingConfig> = {
};
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
export interface TestDirective
extends Partial<
Pick<
TypeCheckableDirectiveMeta,
Exclude<
keyof TypeCheckableDirectiveMeta,
| 'ref'
| 'coercedInputFields'
| 'restrictedInputFields'
| 'stringLiteralInputFields'
| 'undeclaredInputFields'
| 'publicMethods'
| 'inputs'
| 'outputs'
| 'hostDirectives'
>
export interface TestDirective extends Partial<
Pick<
TypeCheckableDirectiveMeta,
Exclude<
keyof TypeCheckableDirectiveMeta,
| 'ref'
| 'coercedInputFields'
| 'restrictedInputFields'
| 'stringLiteralInputFields'
| 'undeclaredInputFields'
| 'publicMethods'
| 'inputs'
| 'outputs'
| 'hostDirectives'
>
> {
>
> {
selector: string | null;
name: string;
file?: AbsoluteFsPath;
@@ -464,7 +463,12 @@ export function tcb(
TcbGenericContextBehavior.UseEmitter,
);
const rendered = env.render(!options.emitSpans /* removeComments */);
let rendered = env.render();
if (!options.emitSpans) {
rendered = rendered.replace(/\s+\/\*[\s\S]*?\*\//g, '');
}
return rendered.replace(/\s+/g, ' ');
}
@@ -4,8 +4,8 @@
import { Directive, model } from '@angular/core';
import * as i0 from "@angular/core";
export class TestDir {
counter = model(0, ...(ngDevMode ? [{ debugName: "counter" }] : []));
name = model.required(...(ngDevMode ? [{ debugName: "name" }] : []));
counter = model(0, ...(ngDevMode ? [{ debugName: "counter" }] : /* istanbul ignore next */ []));
name = model.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, inputs: { counter: { classPropertyName: "counter", publicName: "counter", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { counter: "counterChange", name: "nameChange" }, ngImport: i0 });
}
@@ -31,8 +31,8 @@ export declare class TestDir {
import { Component, model } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComp {
counter = model(0, ...(ngDevMode ? [{ debugName: "counter" }] : []));
name = model.required(...(ngDevMode ? [{ debugName: "name" }] : []));
counter = model(0, ...(ngDevMode ? [{ debugName: "counter" }] : /* istanbul ignore next */ []));
name = model.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: TestComp, isStandalone: true, selector: "ng-component", inputs: { counter: { classPropertyName: "counter", publicName: "counter", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { counter: "counterChange", name: "nameChange" }, ngImport: i0, template: 'Works', isInline: true });
}
@@ -60,8 +60,8 @@ export declare class TestComp {
import { Directive, EventEmitter, Input, model, Output } from '@angular/core';
import * as i0 from "@angular/core";
export class TestDir {
counter = model(0, ...(ngDevMode ? [{ debugName: "counter" }] : []));
modelWithAlias = model(false, { ...(ngDevMode ? { debugName: "modelWithAlias" } : {}), alias: 'alias' });
counter = model(0, ...(ngDevMode ? [{ debugName: "counter" }] : /* istanbul ignore next */ []));
modelWithAlias = model(false, { ...(ngDevMode ? { debugName: "modelWithAlias" } : /* istanbul ignore next */ {}), alias: 'alias' });
decoratorInput = true;
decoratorInputWithAlias = true;
decoratorOutput = new EventEmitter();
@@ -187,7 +187,7 @@ export declare class MyComponent {
import { Component, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class MyComponent {
enterClass = signal('slide', ...(ngDevMode ? [{ debugName: "enterClass" }] : []));
enterClass = signal('slide', ...(ngDevMode ? [{ debugName: "enterClass" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: `
<div>
@@ -296,7 +296,7 @@ export declare class MyComponent {
import { Component, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class MyComponent {
leaveClass = signal('fade', ...(ngDevMode ? [{ debugName: "leaveClass" }] : []));
leaveClass = signal('fade', ...(ngDevMode ? [{ debugName: "leaveClass" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: `
<div>
@@ -4,8 +4,8 @@
import { Component, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComp {
sigA = signal(1, ...(ngDevMode ? [{ debugName: "sigA" }] : []));
sigB = signal(2, ...(ngDevMode ? [{ debugName: "sigB" }] : []));
sigA = signal(1, ...(ngDevMode ? [{ debugName: "sigA" }] : /* istanbul ignore next */ []));
sigB = signal(2, ...(ngDevMode ? [{ debugName: "sigB" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
<button (click)="sigA.update(value => value + 1)">Increment A</button>
@@ -144,7 +144,7 @@ export declare class TestComp {
import { Component, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComp {
someSignal = signal('', ...(ngDevMode ? [{ debugName: "someSignal" }] : []));
someSignal = signal('', ...(ngDevMode ? [{ debugName: "someSignal" }] : /* istanbul ignore next */ []));
componentProp = 0;
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: TestComp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@@ -272,7 +272,7 @@ export declare class TestDir {
import { Directive, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class TestDir {
someSignal = signal(0, ...(ngDevMode ? [{ debugName: "someSignal" }] : []));
someSignal = signal(0, ...(ngDevMode ? [{ debugName: "someSignal" }] : /* istanbul ignore next */ []));
componentProp = 1;
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, host: { listeners: { "click": "someSignal.update(prev => prev + 1)", "mousedown": "someSignal.update(() => componentProp + 1)" } }, ngImport: i0 });
@@ -611,7 +611,7 @@ import { Component, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComp {
componentProp = 0;
result = signal('', ...(ngDevMode ? [{ debugName: "result" }] : []));
result = signal('', ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: TestComp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@let topLevelLet = 1;
@@ -4,7 +4,7 @@
import { Component, Directive, input } from '@angular/core';
import * as i0 from "@angular/core";
export class FormField {
formField = input(...(ngDevMode ? [undefined, { debugName: "formField" }] : []));
formField = input(...(ngDevMode ? [undefined, { debugName: "formField" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FormField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: FormField, isStandalone: true, selector: "[formField]", inputs: { formField: { classPropertyName: "formField", publicName: "formField", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
}
@@ -54,7 +54,7 @@ export declare class MyComponent {
import { Component, Directive, input } from '@angular/core';
import * as i0 from "@angular/core";
export class FormField {
formField = input(...(ngDevMode ? [undefined, { debugName: "formField" }] : []));
formField = input(...(ngDevMode ? [undefined, { debugName: "formField" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FormField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: FormField, isStandalone: true, selector: "[formField]", inputs: { formField: { classPropertyName: "formField", publicName: "formField", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
}
@@ -953,7 +953,7 @@ export declare class App {
import { Component, Directive, model, signal } from '@angular/core';
import * as i0 from "@angular/core";
export class NgModelDirective {
ngModel = model.required(...(ngDevMode ? [{ debugName: "ngModel" }] : []));
ngModel = model.required(...(ngDevMode ? [{ debugName: "ngModel" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: NgModelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: NgModelDirective, isStandalone: true, selector: "[ngModel]", inputs: { ngModel: { classPropertyName: "ngModel", publicName: "ngModel", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { ngModel: "ngModelChange" }, ngImport: i0 });
}
@@ -1005,7 +1005,7 @@ export declare class TestCmp {
import { Component, Directive, model } from '@angular/core';
import * as i0 from "@angular/core";
export class NgModelDirective {
ngModel = model('', ...(ngDevMode ? [{ debugName: "ngModel" }] : []));
ngModel = model('', ...(ngDevMode ? [{ debugName: "ngModel" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: NgModelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: NgModelDirective, isStandalone: true, selector: "[ngModel]", inputs: { ngModel: { classPropertyName: "ngModel", publicName: "ngModel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { ngModel: "ngModelChange" }, ngImport: i0 });
}
@@ -4,8 +4,8 @@
import { Directive, input } from '@angular/core';
import * as i0 from "@angular/core";
export class TestDir {
counter = input(0, ...(ngDevMode ? [{ debugName: "counter" }] : []));
name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
counter = input(0, ...(ngDevMode ? [{ debugName: "counter" }] : /* istanbul ignore next */ []));
name = input.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, inputs: { counter: { classPropertyName: "counter", publicName: "counter", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
}
@@ -31,8 +31,8 @@ export declare class TestDir {
import { Component, input } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComp {
counter = input(0, ...(ngDevMode ? [{ debugName: "counter" }] : []));
name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
counter = input(0, ...(ngDevMode ? [{ debugName: "counter" }] : /* istanbul ignore next */ []));
name = input.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: TestComp, isStandalone: true, selector: "ng-component", inputs: { counter: { classPropertyName: "counter", publicName: "counter", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: 'Works', isInline: true });
}
@@ -63,9 +63,9 @@ function convertToBoolean(value) {
return value === true || value !== '';
}
export class TestDir {
counter = input(0, ...(ngDevMode ? [{ debugName: "counter" }] : []));
signalWithTransform = input(false, { ...(ngDevMode ? { debugName: "signalWithTransform" } : {}), transform: convertToBoolean });
signalWithTransformAndAlias = input(false, { ...(ngDevMode ? { debugName: "signalWithTransformAndAlias" } : {}), alias: 'publicNameSignal', transform: convertToBoolean });
counter = input(0, ...(ngDevMode ? [{ debugName: "counter" }] : /* istanbul ignore next */ []));
signalWithTransform = input(false, { ...(ngDevMode ? { debugName: "signalWithTransform" } : /* istanbul ignore next */ {}), transform: convertToBoolean });
signalWithTransformAndAlias = input(false, { ...(ngDevMode ? { debugName: "signalWithTransformAndAlias" } : /* istanbul ignore next */ {}), alias: 'publicNameSignal', transform: convertToBoolean });
decoratorInput = true;
decoratorInputWithAlias = true;
decoratorInputWithTransformAndAlias = true;
@@ -110,7 +110,7 @@ function convertToBoolean(value) {
return value === true || value !== '';
}
export class TestDir {
name = input.required({ ...(ngDevMode ? { debugName: "name" } : {}), transform: convertToBoolean });
name = input.required({ ...(ngDevMode ? { debugName: "name" } : /* istanbul ignore next */ {}), transform: convertToBoolean });
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
}
@@ -139,10 +139,10 @@ const toBoolean = (v) => v === true || v !== '';
// Note: `@Input` non-signal inputs did not support transform function "builders" and generics.
const complexTransform = (defaultVal) => (v) => v || defaultVal;
export class TestDir {
name = input.required({ ...(ngDevMode ? { debugName: "name" } : {}), transform: (v) => v === true || v !== '' });
name2 = input.required({ ...(ngDevMode ? { debugName: "name2" } : {}), transform: toBoolean });
genericTransform = input.required({ ...(ngDevMode ? { debugName: "genericTransform" } : {}), transform: complexTransform(1) });
genericTransform2 = input.required({ ...(ngDevMode ? { debugName: "genericTransform2" } : {}), transform: complexTransform(null) });
name = input.required({ ...(ngDevMode ? { debugName: "name" } : /* istanbul ignore next */ {}), transform: (v) => v === true || v !== '' });
name2 = input.required({ ...(ngDevMode ? { debugName: "name2" } : /* istanbul ignore next */ {}), transform: toBoolean });
genericTransform = input.required({ ...(ngDevMode ? { debugName: "genericTransform" } : /* istanbul ignore next */ {}), transform: complexTransform(1) });
genericTransform2 = input.required({ ...(ngDevMode ? { debugName: "genericTransform2" } : /* istanbul ignore next */ {}), transform: complexTransform(null) });
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, name2: { classPropertyName: "name2", publicName: "name2", isSignal: true, isRequired: true, transformFunction: null }, genericTransform: { classPropertyName: "genericTransform", publicName: "genericTransform", isSignal: true, isRequired: true, transformFunction: null }, genericTransform2: { classPropertyName: "genericTransform2", publicName: "genericTransform2", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
}
@@ -7,15 +7,15 @@ export class SomeToken {
}
const nonAnalyzableRefersToString = 'a, b, c';
export class TestDir {
query1 = viewChild('locatorA', ...(ngDevMode ? [{ debugName: "query1" }] : []));
query2 = viewChildren('locatorB', ...(ngDevMode ? [{ debugName: "query2" }] : []));
query3 = contentChild('locatorC', ...(ngDevMode ? [{ debugName: "query3" }] : []));
query4 = contentChildren('locatorD', ...(ngDevMode ? [{ debugName: "query4" }] : []));
query5 = viewChild(forwardRef(() => SomeToken), ...(ngDevMode ? [{ debugName: "query5" }] : []));
query6 = viewChildren(SomeToken, ...(ngDevMode ? [{ debugName: "query6" }] : []));
query7 = viewChild('locatorE', { ...(ngDevMode ? { debugName: "query7" } : {}), read: SomeToken });
query8 = contentChildren('locatorF, locatorG', { ...(ngDevMode ? { debugName: "query8" } : {}), descendants: true });
query9 = contentChildren(nonAnalyzableRefersToString, { ...(ngDevMode ? { debugName: "query9" } : {}), descendants: true });
query1 = viewChild('locatorA', ...(ngDevMode ? [{ debugName: "query1" }] : /* istanbul ignore next */ []));
query2 = viewChildren('locatorB', ...(ngDevMode ? [{ debugName: "query2" }] : /* istanbul ignore next */ []));
query3 = contentChild('locatorC', ...(ngDevMode ? [{ debugName: "query3" }] : /* istanbul ignore next */ []));
query4 = contentChildren('locatorD', ...(ngDevMode ? [{ debugName: "query4" }] : /* istanbul ignore next */ []));
query5 = viewChild(forwardRef(() => SomeToken), ...(ngDevMode ? [{ debugName: "query5" }] : /* istanbul ignore next */ []));
query6 = viewChildren(SomeToken, ...(ngDevMode ? [{ debugName: "query6" }] : /* istanbul ignore next */ []));
query7 = viewChild('locatorE', { ...(ngDevMode ? { debugName: "query7" } : /* istanbul ignore next */ {}), read: SomeToken });
query8 = contentChildren('locatorF, locatorG', { ...(ngDevMode ? { debugName: "query8" } : /* istanbul ignore next */ {}), descendants: true });
query9 = contentChildren(nonAnalyzableRefersToString, { ...(ngDevMode ? { debugName: "query9" } : /* istanbul ignore next */ {}), descendants: true });
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, queries: [{ propertyName: "query3", first: true, predicate: ["locatorC"], descendants: true, isSignal: true }, { propertyName: "query4", predicate: ["locatorD"], isSignal: true }, { propertyName: "query8", predicate: ["locatorF, locatorG"], descendants: true, isSignal: true }, { propertyName: "query9", predicate: nonAnalyzableRefersToString, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "query1", first: true, predicate: ["locatorA"], descendants: true, isSignal: true }, { propertyName: "query2", predicate: ["locatorB"], descendants: true, isSignal: true }, { propertyName: "query5", first: true, predicate: i0.forwardRef(() => SomeToken), descendants: true, isSignal: true }, { propertyName: "query6", predicate: SomeToken, descendants: true, isSignal: true }, { propertyName: "query7", first: true, predicate: ["locatorE"], descendants: true, read: SomeToken, isSignal: true }], ngImport: i0 });
}
@@ -50,10 +50,10 @@ export declare class TestDir {
import { Component, contentChild, contentChildren, viewChild, viewChildren } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComp {
query1 = viewChild('locatorA', ...(ngDevMode ? [{ debugName: "query1" }] : []));
query2 = viewChildren('locatorB', ...(ngDevMode ? [{ debugName: "query2" }] : []));
query3 = contentChild('locatorC', ...(ngDevMode ? [{ debugName: "query3" }] : []));
query4 = contentChildren('locatorD', ...(ngDevMode ? [{ debugName: "query4" }] : []));
query1 = viewChild('locatorA', ...(ngDevMode ? [{ debugName: "query1" }] : /* istanbul ignore next */ []));
query2 = viewChildren('locatorB', ...(ngDevMode ? [{ debugName: "query2" }] : /* istanbul ignore next */ []));
query3 = contentChild('locatorC', ...(ngDevMode ? [{ debugName: "query3" }] : /* istanbul ignore next */ []));
query4 = contentChildren('locatorD', ...(ngDevMode ? [{ debugName: "query4" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "0.0.0-PLACEHOLDER", type: TestComp, isStandalone: true, selector: "ng-component", queries: [{ propertyName: "query3", first: true, predicate: ["locatorC"], descendants: true, isSignal: true }, { propertyName: "query4", predicate: ["locatorD"], isSignal: true }], viewQueries: [{ propertyName: "query1", first: true, predicate: ["locatorA"], descendants: true, isSignal: true }, { propertyName: "query2", predicate: ["locatorB"], descendants: true, isSignal: true }], ngImport: i0, template: 'Works', isInline: true });
}
@@ -84,9 +84,9 @@ import { ContentChild, contentChild, Directive, ViewChild, viewChild } from '@an
import * as i0 from "@angular/core";
export class TestDir {
decoratorViewChild;
signalViewChild = viewChild('locator1', ...(ngDevMode ? [{ debugName: "signalViewChild" }] : []));
signalViewChild = viewChild('locator1', ...(ngDevMode ? [{ debugName: "signalViewChild" }] : /* istanbul ignore next */ []));
decoratorContentChild;
signalContentChild = contentChild('locator2', ...(ngDevMode ? [{ debugName: "signalContentChild" }] : []));
signalContentChild = contentChild('locator2', ...(ngDevMode ? [{ debugName: "signalContentChild" }] : /* istanbul ignore next */ []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "0.0.0-PLACEHOLDER", type: TestDir, isStandalone: true, queries: [{ propertyName: "signalContentChild", first: true, predicate: ["locator2"], descendants: true, isSignal: true }, { propertyName: "decoratorContentChild", first: true, predicate: ["locator2"], descendants: true }], viewQueries: [{ propertyName: "signalViewChild", first: true, predicate: ["locator1"], descendants: true, isSignal: true }, { propertyName: "decoratorViewChild", first: true, predicate: ["locator1"], descendants: true }], ngImport: i0 });
}
@@ -70,7 +70,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`signal('Hello World', ...(ngDevMode ? [{ debugName: "testSignal" }] : []))`,
`signal('Hello World', ...(ngDevMode ? [{ debugName: "testSignal" }] : /* istanbul ignore next */ []))`,
);
});
@@ -118,7 +118,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`signal('Hello World', { ...(ngDevMode ? { debugName: "testSignal" } : {}), equal: () => true })`,
`signal('Hello World', { ...(ngDevMode ? { debugName: "testSignal" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -160,7 +160,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`signal('Hello World', ...(ngDevMode ? [{ debugName: "testSignal" }] : [])`,
`signal('Hello World', ...(ngDevMode ? [{ debugName: "testSignal" }] : /* istanbul ignore next */ [])`,
);
});
@@ -225,7 +225,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`signal('Hello World', { ...(ngDevMode ? { debugName: "testSignal" } : {}), equal: () => true })`,
`signal('Hello World', { ...(ngDevMode ? { debugName: "testSignal" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -277,7 +277,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`signal('Hello World', ...(ngDevMode ? [{ debugName: "testSignal" }] : [])`,
`signal('Hello World', ...(ngDevMode ? [{ debugName: "testSignal" }] : /* istanbul ignore next */ [])`,
);
});
@@ -351,7 +351,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`signal('Hello World', { ...(ngDevMode ? { debugName: "testSignal" } : {}), equal: () => true })`,
`signal('Hello World', { ...(ngDevMode ? { debugName: "testSignal" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -411,7 +411,7 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`computed(() => testSignal(), ...(ngDevMode ? [{ debugName: "testComputed" }] : []))`,
`computed(() => testSignal(), ...(ngDevMode ? [{ debugName: "testComputed" }] : /* istanbul ignore next */ []))`,
);
});
@@ -462,7 +462,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`computed(() => testSignal(), { ...(ngDevMode ? { debugName: "testComputed" } : {}), equal: () => true })`,
`computed(() => testSignal(), { ...(ngDevMode ? { debugName: "testComputed" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -569,7 +569,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`computed(() => this.testSignal(), { ...(ngDevMode ? { debugName: "testComputed" } : {}), equal: () => true })`,
`computed(() => this.testSignal(), { ...(ngDevMode ? { debugName: "testComputed" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -701,7 +701,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`computed(() => this.testSignal(), { ...(ngDevMode ? { debugName: "testComputed" } : {}), equal: () => true })`,
`computed(() => this.testSignal(), { ...(ngDevMode ? { debugName: "testComputed" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -800,10 +800,10 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`model('Hello World', ...(ngDevMode ? [{ debugName: "testModel" }] : [])`,
`model('Hello World', ...(ngDevMode ? [{ debugName: "testModel" }] : /* istanbul ignore next */ [])`,
);
expect(jsContents).toContain(
`model(...(ngDevMode ? [undefined, { debugName: "testModel2" }] : [])`,
`model(...(ngDevMode ? [undefined, { debugName: "testModel2" }] : /* istanbul ignore next */ [])`,
);
});
@@ -845,7 +845,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`model.required(...(ngDevMode ? [{ debugName: "testModel" }] : [])`,
`model.required(...(ngDevMode ? [{ debugName: "testModel" }] : /* istanbul ignore next */ [])`,
);
});
@@ -866,7 +866,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`model.required({ ...(ngDevMode ? { debugName: "testModel" } : {}), alias: 'testModelAlias' })`,
`model.required({ ...(ngDevMode ? { debugName: "testModel" } : /* istanbul ignore next */ {}), alias: 'testModelAlias' })`,
);
});
@@ -990,7 +990,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`input(...(ngDevMode ? [undefined, { debugName: "testInput" }] : [])`,
`input(...(ngDevMode ? [undefined, { debugName: "testInput" }] : /* istanbul ignore next */ [])`,
);
});
@@ -1033,7 +1033,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`input.required(...(ngDevMode ? [{ debugName: "testInput" }] : []))`,
`input.required(...(ngDevMode ? [{ debugName: "testInput" }] : /* istanbul ignore next */ []))`,
);
});
@@ -1054,7 +1054,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`input.required({ ...(ngDevMode ? { debugName: "testInput" } : {}), alias: 'testInputAlias' })`,
`input.required({ ...(ngDevMode ? { debugName: "testInput" } : /* istanbul ignore next */ {}), alias: 'testInputAlias' })`,
);
});
@@ -1191,10 +1191,10 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`viewChild('foo', ...(ngDevMode ? [{ debugName: "testViewChild" }] : [])`,
`viewChild('foo', ...(ngDevMode ? [{ debugName: "testViewChild" }] : /* istanbul ignore next */ [])`,
);
expect(jsContents).toContain(
`viewChild(ChildComponent, ...(ngDevMode ? [{ debugName: "testViewChildComponent" }] : [])`,
`viewChild(ChildComponent, ...(ngDevMode ? [{ debugName: "testViewChildComponent" }] : /* istanbul ignore next */ [])`,
);
});
@@ -1340,7 +1340,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`viewChildren('foo', ...(ngDevMode ? [{ debugName: "testViewChildren" }] : [])`,
`viewChildren('foo', ...(ngDevMode ? [{ debugName: "testViewChildren" }] : /* istanbul ignore next */ [])`,
);
});
@@ -1466,7 +1466,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`contentChild('foo', ...(ngDevMode ? [{ debugName: "testContentChild" }] : [])`,
`contentChild('foo', ...(ngDevMode ? [{ debugName: "testContentChild" }] : /* istanbul ignore next */ [])`,
);
});
@@ -1594,7 +1594,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`contentChildren('foo', ...(ngDevMode ? [{ debugName: "testContentChildren" }] : [])`,
`contentChildren('foo', ...(ngDevMode ? [{ debugName: "testContentChildren" }] : /* istanbul ignore next */ [])`,
);
});
@@ -1724,7 +1724,7 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`effect(() => this.testSignal(), ...(ngDevMode ? [{ debugName: "testEffect" }] : [])`,
`effect(() => this.testSignal(), ...(ngDevMode ? [{ debugName: "testEffect" }] : /* istanbul ignore next */ [])`,
);
});
@@ -1845,7 +1845,7 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`linkedSignal(() => testSignal(), ...(ngDevMode ? [{ debugName: "testLinkedSignal" }] : [])`,
`linkedSignal(() => testSignal(), ...(ngDevMode ? [{ debugName: "testLinkedSignal" }] : /* istanbul ignore next */ [])`,
);
});
@@ -1896,7 +1896,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`linkedSignal(() => testSignal(), { ...(ngDevMode ? { debugName: "testLinkedSignal" } : {}), equal: () => true })`,
`linkedSignal(() => testSignal(), { ...(ngDevMode ? { debugName: "testLinkedSignal" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -1958,7 +1958,7 @@ runInEachFileSystem(() => {
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
'testLinkedSignal = linkedSignal({ ...(ngDevMode ? { debugName: "testLinkedSignal" } : {}), ' +
'testLinkedSignal = linkedSignal({ ...(ngDevMode ? { debugName: "testLinkedSignal" } : /* istanbul ignore next */ {}), ' +
'source: testSignal, ' +
'computation: (src, prev) => src ' +
'})',
@@ -2082,7 +2082,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`linkedSignal(() => this.testSignal(), { ...(ngDevMode ? { debugName: "testLinkedSignal" } : {}), equal: () => true })`,
`linkedSignal(() => this.testSignal(), { ...(ngDevMode ? { debugName: "testLinkedSignal" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -2157,7 +2157,7 @@ runInEachFileSystem(() => {
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
'linkedSignal({ ...(ngDevMode ? { debugName: "testLinkedSignal" } : {}), ' +
'linkedSignal({ ...(ngDevMode ? { debugName: "testLinkedSignal" } : /* istanbul ignore next */ {}), ' +
'source: this.testSignal, ' +
'computation: (src, prev) => src ' +
'})',
@@ -2304,7 +2304,7 @@ runInEachFileSystem(() => {
const jsContents = env.getContents('test.js');
expect(jsContents).toContain(
`linkedSignal(() => this.testSignal(), { ...(ngDevMode ? { debugName: "testLinkedSignal" } : {}), equal: () => true })`,
`linkedSignal(() => this.testSignal(), { ...(ngDevMode ? { debugName: "testLinkedSignal" } : /* istanbul ignore next */ {}), equal: () => true })`,
);
});
@@ -2394,7 +2394,7 @@ runInEachFileSystem(() => {
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
'linkedSignal({ ...(ngDevMode ? { debugName: "testLinkedSignal" } : {}), ' +
'linkedSignal({ ...(ngDevMode ? { debugName: "testLinkedSignal" } : /* istanbul ignore next */ {}), ' +
'source: this.testSignal, ' +
'computation: (src, prev) => src ' +
'})',
@@ -2509,7 +2509,7 @@ runInEachFileSystem(() => {
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
'resource({ ' +
'...(ngDevMode ? { debugName: "testResource" } : {}), ' +
'...(ngDevMode ? { debugName: "testResource" } : /* istanbul ignore next */ {}), ' +
`defaultValue: 'foo', ` +
`loader: async () => 'bar' ` +
'})',
@@ -2587,7 +2587,7 @@ runInEachFileSystem(() => {
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
'resource({ ' +
'...(ngDevMode ? { debugName: "testResource" } : {}), ' +
'...(ngDevMode ? { debugName: "testResource" } : /* istanbul ignore next */ {}), ' +
`defaultValue: 'foo', ` +
`loader: async () => 'bar' ` +
'})',
@@ -2678,7 +2678,7 @@ runInEachFileSystem(() => {
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
'resource({ ' +
'...(ngDevMode ? { debugName: "testResource" } : {}), ' +
'...(ngDevMode ? { debugName: "testResource" } : /* istanbul ignore next */ {}), ' +
`defaultValue: 'foo', ` +
`loader: async () => 'bar' ` +
'})',
@@ -2777,7 +2777,7 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
`httpResource(() => '/api', ...(ngDevMode ? [{ debugName: "testHttpResource" }] : []))`,
`httpResource(() => '/api', ...(ngDevMode ? [{ debugName: "testHttpResource" }] : /* istanbul ignore next */ []))`,
);
});
@@ -2837,7 +2837,7 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
`httpResource(() => '/api', ...(ngDevMode ? [{ debugName: "testHttpResource" }] : []))`,
`httpResource(() => '/api', ...(ngDevMode ? [{ debugName: "testHttpResource" }] : /* istanbul ignore next */ []))`,
);
});
@@ -2909,7 +2909,7 @@ runInEachFileSystem(() => {
env.driveMain();
const jsContents = cleanNewLines(env.getContents('test.js'));
expect(jsContents).toContain(
`httpResource(() => '/api', ...(ngDevMode ? [{ debugName: "testHttpResource" }] : []))`,
`httpResource(() => '/api', ...(ngDevMode ? [{ debugName: "testHttpResource" }] : /* istanbul ignore next */ []))`,
);
});
+2 -3
View File
@@ -803,9 +803,8 @@ export class RecursiveVisitor implements Visitor<void> {
visitAll(this, block.branches);
}
visitIfBlockBranch(block: IfBlockBranch): void {
const blockItems = block.children;
block.expressionAlias && blockItems.push(block.expressionAlias);
visitAll(this, blockItems);
visitAll(this, block.children);
block.expressionAlias?.visit(this);
}
visitContent(content: Content): void {
visitAll(this, content.children);
@@ -0,0 +1,52 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import * as t from '../../src/render3/r3_ast';
import {parseR3 as parse} from './view/util';
describe('RecursiveVisitor', () => {
it('should not mutate IfBlockBranch children when visiting', () => {
// Template with an @if block that has an alias.
// The alias is key because `visitIfBlockBranch` previously pushed the alias
// to the children array, causing mutation if the array wasn't cloned.
const template = '@if (val; as alias) { <div></div> }';
const result = parse(template);
// Structure:
// result.nodes[0] is the IfBlock
// IfBlock.branches[0] is the IfBlockBranch
const ifBlock = result.nodes[0] as t.IfBlock;
expect(ifBlock instanceof t.IfBlock).toBe(true);
const branch = ifBlock.branches[0];
expect(branch instanceof t.IfBlockBranch).toBe(true);
// Verify initial state
// Children should contain only the Element (<div></div>)
expect(branch.children.length).toBe(1);
expect(branch.children[0] instanceof t.Element).toBe(true);
const element = branch.children[0] as t.Element;
expect(element.name).toBe('div');
// Alias should exist
expect(branch.expressionAlias).not.toBeNull();
const alias = branch.expressionAlias!;
const visitor = new t.RecursiveVisitor();
// Visit the branch
visitor.visitIfBlockBranch(branch);
// Verify post-visit state
// The children array should STILL only contain the Element.
// It should NOT contain the alias variable.
expect(branch.children.length).toBe(1);
expect(branch.children[0]).toBe(element);
expect(branch.children).not.toContain(alias);
});
});
+7
View File
@@ -64,4 +64,11 @@ export interface TracingService<T extends TracingSnapshot> {
* @return A new event handler to be bound instead of the original one.
*/
wrapEventListener?<T extends Function>(element: HTMLElement, eventName: string, handler: T): T;
/**
* Trace the creation of a component instance.
* @param className Name of the component. May be null if the class is anonymous.
* @param fn Function that creates the component instance.
*/
componentCreate?<T>(className: string | null, fn: () => T): T;
}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ComponentDef} from '../render3';
import type {ComponentDef} from '../render3';
import {readPatchedLView} from '../render3/context_discovery';
import {isComponentHost, isLContainer, isLView} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, HOST, TVIEW} from '../render3/interfaces/view';
@@ -36,11 +36,11 @@ export function getClosestComponentName(node: Node): string | null {
const tNode = getTNode(tView, i);
if (isComponentHost(tNode)) {
const def = tView.data[tNode.directiveStart + tNode.componentOffset] as ComponentDef<{}>;
const name = def.debugInfo?.className || def.type.name;
const name = getComponentName(def);
// Note: the name may be an empty string if the class name is
// dropped due to minification. In such cases keep going up the tree.
if (name) {
if (name !== null) {
return name;
} else {
break;
@@ -54,3 +54,15 @@ export function getClosestComponentName(node: Node): string | null {
return null;
}
/**
* Gets the class name of a component from its definition.
* Warning! this function will return minified names if the name of the component is minified. The
* consumer of the function is responsible for resolving the minified name to its original name.
* @param tView TView that the node belongs to.
* @param tNode TNode from which to extract the component name.
*/
export function getComponentName(def: ComponentDef<unknown>): string | null {
// Note: the name may be an empty string if the class name is dropped due to minification.
return def.debugInfo?.className || def.type.name || null;
}
+114 -82
View File
@@ -82,6 +82,8 @@ import {ViewRef} from './view_ref';
import {createLView, createTView, getInitialLViewFlagsFromDef} from './view/construction';
import {BINDING, Binding, BindingInternal, DirectiveWithBindings} from './dynamic_bindings';
import {NG_REFLECT_ATTRS_FLAG, NG_REFLECT_ATTRS_FLAG_DEFAULT} from '../ng_reflect';
import {TracingService} from '../application/tracing';
import {getComponentName} from '../internal/get_closest_component_name';
export class ComponentFactoryResolver extends AbstractComponentFactoryResolver {
/**
@@ -169,6 +171,7 @@ function createRootLViewEnvironment(rootLViewInjector: Injector): LViewEnvironme
const sanitizer = rootLViewInjector.get(Sanitizer, null);
const changeDetectionScheduler = rootLViewInjector.get(ChangeDetectionScheduler, null);
const tracingService = rootLViewInjector.get(TracingService, null, {optional: true});
let ngReflect = false;
if (typeof ngDevMode === 'undefined' || ngDevMode) {
@@ -180,6 +183,7 @@ function createRootLViewEnvironment(rootLViewInjector: Injector): LViewEnvironme
sanitizer,
changeDetectionScheduler,
ngReflect,
tracingService,
};
}
@@ -265,101 +269,129 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
try {
const cmpDef = this.componentDef;
ngDevMode && verifyNotAnOrphanComponent(cmpDef);
const rootTView = createRootTView(rootSelectorOrNode, cmpDef, componentBindings, directives);
const rootViewInjector = createRootViewInjector(
cmpDef,
environmentInjector || this.ngModule,
injector,
);
const environment = createRootLViewEnvironment(rootViewInjector);
const hostRenderer = environment.rendererFactory.createRenderer(null, cmpDef);
const hostElement = rootSelectorOrNode
? locateHostElement(
hostRenderer,
rootSelectorOrNode,
cmpDef.encapsulation,
const tracingService = environment.tracingService;
if (tracingService && tracingService.componentCreate) {
return tracingService.componentCreate(getComponentName(cmpDef), () =>
this.createComponentRef(
environment,
rootViewInjector,
)
: createHostElement(cmpDef, hostRenderer);
const hasInputBindings =
componentBindings?.some(isInputBinding) ||
directives?.some((d) => typeof d !== 'function' && d.bindings.some(isInputBinding));
const rootLView = createLView<T>(
null,
rootTView,
null,
LViewFlags.IsRoot | getInitialLViewFlagsFromDef(cmpDef),
null,
null,
environment,
hostRenderer,
rootViewInjector,
null,
retrieveHydrationInfo(hostElement, rootViewInjector, true /* isRootView */),
);
rootLView[HEADER_OFFSET] = hostElement;
// rootView is the parent when bootstrapping
// TODO(misko): it looks like we are entering view here but we don't really need to as
// `renderView` does that. However as the code is written it is needed because
// `createRootComponentView` and `createRootComponent` both read global state. Fixing those
// issues would allow us to drop this.
enterView(rootLView);
let componentView: LView | null = null;
try {
const hostTNode = directiveHostFirstCreatePass(
HEADER_OFFSET,
rootLView,
TNodeType.Element,
'#host',
() => rootTView.directiveRegistry,
true,
0,
projectableNodes,
rootSelectorOrNode,
directives,
componentBindings,
),
);
} else {
return this.createComponentRef(
environment,
rootViewInjector,
projectableNodes,
rootSelectorOrNode,
directives,
componentBindings,
);
// ---- element instruction
setupStaticAttributes(hostRenderer, hostElement, hostTNode);
attachPatchData(hostElement, rootLView);
// TODO(pk): this logic is similar to the instruction code where a node can have directives
createDirectivesInstances(rootTView, rootLView, hostTNode);
executeContentQueries(rootTView, hostTNode, rootLView);
directiveHostEndFirstCreatePass(rootTView, hostTNode);
if (projectableNodes !== undefined) {
projectNodes(hostTNode, this.ngContentSelectors, projectableNodes);
}
componentView = getComponentLViewByIndex(hostTNode.index, rootLView);
// TODO(pk): why do we need this logic?
rootLView[CONTEXT] = componentView[CONTEXT] as T;
renderView(rootTView, rootLView, null);
} catch (e) {
// Stop tracking the views if creation failed since
// the consumer won't have a way to dereference them.
if (componentView !== null) {
unregisterLView(componentView);
}
unregisterLView(rootLView);
throw e;
} finally {
profiler(ProfilerEvent.DynamicComponentEnd);
leaveView();
}
return new ComponentRef(this.componentType, rootLView, !!hasInputBindings);
} finally {
setActiveConsumer(prevConsumer);
}
}
private createComponentRef(
environment: LViewEnvironment,
rootViewInjector: Injector,
projectableNodes?: any[][] | undefined,
rootSelectorOrNode?: any,
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[],
componentBindings?: Binding[],
) {
const cmpDef = this.componentDef;
const rootTView = createRootTView(rootSelectorOrNode, cmpDef, componentBindings, directives);
const hostRenderer = environment.rendererFactory.createRenderer(null, cmpDef);
const hostElement = rootSelectorOrNode
? locateHostElement(hostRenderer, rootSelectorOrNode, cmpDef.encapsulation, rootViewInjector)
: createHostElement(cmpDef, hostRenderer);
const hasInputBindings =
componentBindings?.some(isInputBinding) ||
directives?.some((d) => typeof d !== 'function' && d.bindings.some(isInputBinding));
const rootLView = createLView<T>(
null,
rootTView,
null,
LViewFlags.IsRoot | getInitialLViewFlagsFromDef(cmpDef),
null,
null,
environment,
hostRenderer,
rootViewInjector,
null,
retrieveHydrationInfo(hostElement, rootViewInjector, true /* isRootView */),
);
rootLView[HEADER_OFFSET] = hostElement;
// rootView is the parent when bootstrapping
// TODO(misko): it looks like we are entering view here but we don't really need to as
// `renderView` does that. However as the code is written it is needed because
// `createRootComponentView` and `createRootComponent` both read global state. Fixing those
// issues would allow us to drop this.
enterView(rootLView);
let componentView: LView | null = null;
try {
const hostTNode = directiveHostFirstCreatePass(
HEADER_OFFSET,
rootLView,
TNodeType.Element,
'#host',
() => rootTView.directiveRegistry,
true,
0,
);
// ---- element instruction
setupStaticAttributes(hostRenderer, hostElement, hostTNode);
attachPatchData(hostElement, rootLView);
// TODO(pk): this logic is similar to the instruction code where a node can have directives
createDirectivesInstances(rootTView, rootLView, hostTNode);
executeContentQueries(rootTView, hostTNode, rootLView);
directiveHostEndFirstCreatePass(rootTView, hostTNode);
if (projectableNodes !== undefined) {
projectNodes(hostTNode, this.ngContentSelectors, projectableNodes);
}
componentView = getComponentLViewByIndex(hostTNode.index, rootLView);
// TODO(pk): why do we need this logic?
rootLView[CONTEXT] = componentView[CONTEXT] as T;
renderView(rootTView, rootLView, null);
} catch (e) {
// Stop tracking the views if creation failed since
// the consumer won't have a way to dereference them.
if (componentView !== null) {
unregisterLView(componentView);
}
unregisterLView(rootLView);
throw e;
} finally {
profiler(ProfilerEvent.DynamicComponentEnd);
leaveView();
}
return new ComponentRef(this.componentType, rootLView, !!hasInputBindings);
}
}
function createRootTView(
@@ -23,13 +23,23 @@ import {
markRNodeAsSkippedByHydration,
setSegmentHead,
} from '../../hydration/utils';
import {getComponentName} from '../../internal/get_closest_component_name';
import {assertDefined} from '../../util/assert';
import {assertTNodeCreationIndex} from '../assert';
import {clearElementContents, createElementNode} from '../dom_node_manipulation';
import {ComponentDef} from '../interfaces/definition';
import {hasClassInput, hasStyleInput, TElementNode, TNode, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer_dom';
import {isComponentHost, isDirectiveHost} from '../interfaces/type_checks';
import {HEADER_OFFSET, HYDRATION, LView, RENDERER, TVIEW, TView} from '../interfaces/view';
import {
ENVIRONMENT,
HEADER_OFFSET,
HYDRATION,
LView,
RENDERER,
TVIEW,
TView,
} from '../interfaces/view';
import {assertTNodeType} from '../node_assert';
import {executeContentQueries} from '../queries/query_execution';
import {
@@ -100,6 +110,31 @@ export function ɵɵelementStart(
)
: (tView.data[adjustedIndex] as TElementNode);
// If the node is a component host and we have a tracing service, we need to wrap the init logic.
if (isComponentHost(tNode)) {
const tracingService = lView[ENVIRONMENT].tracingService;
if (tracingService && tracingService.componentCreate) {
const def = tView.data[tNode.directiveStart + tNode.componentOffset] as ComponentDef<{}>;
return tracingService.componentCreate(getComponentName(def), () => {
initializeElement(index, name, lView, tNode, localRefsIndex);
return ɵɵelementStart;
});
}
}
initializeElement(index, name, lView, tNode, localRefsIndex);
return ɵɵelementStart;
}
function initializeElement(
index: number,
name: string,
lView: LView,
tNode: TElementNode,
localRefsIndex: number | undefined,
) {
elementLikeStartShared(tNode, lView, index, name, _locateOrCreateElementNode);
if (isDirectiveHost(tNode)) {
@@ -115,8 +150,6 @@ export function ɵɵelementStart(
if (ngDevMode && lView[TVIEW].firstCreatePass) {
validateElementIsKnown(lView, tNode);
}
return ɵɵelementStart;
}
/**
@@ -7,6 +7,7 @@
*/
import {AnimationLViewData} from '../../animation/interfaces';
import type {TracingService, TracingSnapshot} from '../../application/tracing';
import type {ChangeDetectionScheduler} from '../../change_detection/scheduling/zoneless_scheduling';
import {TDeferBlockDetails} from '../../defer/interfaces';
import type {Injector} from '../../di/injector';
@@ -387,6 +388,9 @@ export interface LViewEnvironment {
/** Scheduler for change detection to notify when application state changes. */
changeDetectionScheduler: ChangeDetectionScheduler | null;
/** Service used for tracing different parts of the application. */
tracingService: TracingService<TracingSnapshot> | null;
/**
* Whether `ng-reflect-*` attributes should be produced in dev mode
* (always disabled in prod mode).
+55 -1
View File
@@ -10,6 +10,7 @@ import {
afterEveryRender,
Component,
provideZoneChangeDetection,
signal,
ɵTracingAction as TracingAction,
ɵTracingService as TracingService,
ɵTracingSnapshot as TracingSnapshot,
@@ -27,13 +28,15 @@ describe('TracingService', () => {
let fakeSnapshot: TracingSnapshot;
let mockTracingService: TracingService<TracingSnapshot>;
let clickCount: number;
let createdComponents: (string | null)[];
beforeEach(() => {
actions = [];
listeners = [];
clickCount = 0;
createdComponents = [];
fakeSnapshot = {
run: function <T>(action: TracingAction, fn: () => T): T {
run: <T>(action: TracingAction, fn: () => T): T => {
actions.push(action);
return fn();
},
@@ -54,6 +57,10 @@ describe('TracingService', () => {
listeners.push({event, handler});
return handler;
}),
componentCreate: (name, fn) => {
createdComponents.push(name);
return fn();
},
};
});
@@ -126,4 +133,51 @@ describe('TracingService', () => {
expect(clickCount).toBe(1);
}));
it('should trace component creations', () => {
TestBed.configureTestingModule({
providers: [{provide: TracingService, useValue: mockTracingService}],
});
@Component({template: 'hello', selector: 'grandchild'})
class GrandChild {}
@Component({template: 'extra', selector: 'extra'})
class Extra {}
@Component({
template: '<grandchild/>',
selector: 'child',
imports: [GrandChild],
})
class Child {}
@Component({
template: ` <child />
<child />
@if (showExtra()) {
<extra />
}`,
imports: [Child, Extra],
})
class App {
showExtra = signal(false);
}
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(createdComponents).toEqual(['App', 'Child', 'Child', 'GrandChild', 'GrandChild']);
fixture.componentInstance.showExtra.set(true);
fixture.detectChanges();
expect(createdComponents).toEqual([
'App',
'Child',
'Child',
'GrandChild',
'GrandChild',
'Extra',
]);
});
});
@@ -494,6 +494,7 @@
"getComponentDef",
"getComponentId",
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getCurrentDirectiveIndex",
"getCurrentInjector",
@@ -575,6 +576,7 @@
"initFeatures",
"initTNodeFlags",
"initializeDirectives",
"initializeElement",
"initializeInputAndOutputAliases",
"inject2",
"injectArgs",
@@ -398,6 +398,7 @@
"getComponentDef",
"getComponentId",
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getCurrentDirectiveIndex",
"getCurrentInjector",
@@ -440,6 +440,7 @@
"getComponentDef",
"getComponentId",
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getCurrentDirectiveIndex",
"getCurrentInjector",
@@ -528,6 +529,7 @@
"initFeatures",
"initTNodeFlags",
"initializeDirectives",
"initializeElement",
"initializeInputAndOutputAliases",
"inject2",
"injectArgs",
@@ -555,6 +555,7 @@
"getComponentDef",
"getComponentId",
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getControlAsyncValidators",
"getControlValidators",
@@ -664,6 +665,7 @@
"initFeatures",
"initTNodeFlags",
"initializeDirectives",
"initializeElement",
"initializeInputAndOutputAliases",
"inject2",
"injectArgs",
@@ -556,6 +556,7 @@
"getComponentDef",
"getComponentId",
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getControlAsyncValidators",
"getControlValidators",
@@ -664,6 +665,7 @@
"initFeatures",
"initTNodeFlags",
"initializeDirectives",
"initializeElement",
"initializeInputAndOutputAliases",
"inject2",
"injectArgs",
@@ -463,6 +463,7 @@
"getComponentDef",
"getComponentId",
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getCurrentDirectiveIndex",
"getCurrentInjector",
@@ -656,6 +656,7 @@
"getComponentDef",
"getComponentId",
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getCurrentDirectiveIndex",
"getCurrentInjector",
@@ -768,6 +769,7 @@
"initFeatures",
"initTNodeFlags",
"initializeDirectives",
"initializeElement",
"initializeInputAndOutputAliases",
"inject2",
"injectArgs",
@@ -365,6 +365,7 @@
"getComponentDef",
"getComponentId",
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getCurrentDirectiveIndex",
"getCurrentInjector",
+1
View File
@@ -155,6 +155,7 @@ describe('di', () => {
sanitizer: null,
changeDetectionScheduler: null,
ngReflect: false,
tracingService: null,
},
{} as any,
null,
@@ -70,6 +70,7 @@ export function enterViewWithOneDiv() {
sanitizer: null,
changeDetectionScheduler: null,
ngReflect: false,
tracingService: null,
},
renderer,
null,

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