mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2b903957a | |||
| bd72571583 | |||
| 628f3f99db | |||
| 79ac570c26 | |||
| 276c12a349 | |||
| 104ac117ea | |||
| c2a74548d9 | |||
| e7d1d8def3 | |||
| 4a390b6c73 | |||
| 5a486a65cf | |||
| c822bf8e76 | |||
| 31e2c99007 | |||
| f8b6bd0cbe | |||
| 5349b027b8 | |||
| 8f8d67dc57 | |||
| 23153a06dd | |||
| fd3b4413f3 | |||
| 670d1660c4 | |||
| 8ca0237d99 | |||
| 3576019c0f | |||
| 05d022d5e6 | |||
| 1df1697c6e | |||
| 05476ea21b |
@@ -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:
|
||||
|
||||
@@ -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 &
|
||||
|
||||
@@ -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}}'
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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: |
|
||||
|
||||
@@ -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
@@ -9,5 +9,5 @@ export const github = {
|
||||
name: 'angular',
|
||||
mainBranchName: 'main',
|
||||
mergeMode: 'caretaker-only',
|
||||
requireReleaseModeForRelease: true,
|
||||
requireReleaseModeForRelease: false,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Generated
+233
-233
@@ -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,4 +1,4 @@
|
||||
{
|
||||
"branchName": "refs/heads/21.2.x",
|
||||
"sha": "a68740532edcf94dafd4a6862a8985f506718016"
|
||||
"sha": "a076ff7f05b678c4cdc6b85dbfe474a09f56022a"
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
+2
-2
@@ -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 });
|
||||
}
|
||||
|
||||
+2
-2
@@ -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 */ []))`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user