mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c60c41e29a | |||
| fc2f6fd67d | |||
| f2cf65a898 | |||
| 467b37b4b4 | |||
| 2dcdf9aae6 | |||
| c097c0144c | |||
| 1b7dcc854a | |||
| 2c6c67bee6 |
@@ -21,21 +21,19 @@ 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY || 'bypassed_for_forks' }}
|
||||
triggering-label: 'adev: preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/adev/dist/browser'
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
npx -y firebase-tools@15.15.0 target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@15.15.0 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -50,9 +50,9 @@ jobs:
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
|
||||
+25
-25
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -69,11 +69,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -198,11 +198,11 @@ jobs:
|
||||
runs-on: ubuntu-latest-8core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build adev
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
labels: '{"requires: TGP": ["packages/core/primitives/**/{*,.*}"]}'
|
||||
@@ -24,14 +24,14 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/labeling/issue@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
run-tests-guide-url: http://go/angular-g3sync-start
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,6 +11,6 @@ jobs:
|
||||
status:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
workflows: ${{ steps.workflows.outputs.workflows }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- id: workflows
|
||||
@@ -38,9 +38,9 @@ jobs:
|
||||
workflow: ${{ fromJSON(needs.list.outputs.workflows) }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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
|
||||
|
||||
+20
-20
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
@@ -73,11 +73,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: |
|
||||
|
||||
@@ -18,7 +18,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: angular/dev-infra/.github/workflows/reusable-release.yml@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/.github/workflows/reusable-release.yml@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
secrets:
|
||||
wombot-token: ${{ secrets.WOMBOT_TOKEN }}
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ git_override(
|
||||
bazel_dep(name = "devinfra")
|
||||
git_override(
|
||||
module_name = "devinfra",
|
||||
commit = "183403ae13b785698eaf13c819dda55b9fed430b",
|
||||
commit = "4a09f9588769980eacb41fcc76e2c76d048eac42",
|
||||
remote = "https://github.com/angular/dev-infra.git",
|
||||
)
|
||||
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
aria-label="align left"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'left'"
|
||||
(click)="alignment.set('left')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_left
|
||||
@@ -98,7 +97,6 @@
|
||||
aria-label="align center"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'center'"
|
||||
(click)="alignment.set('center')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_center
|
||||
@@ -112,7 +110,6 @@
|
||||
aria-label="align right"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'right'"
|
||||
(click)="alignment.set('right')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_right
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
aria-label="align left"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'left'"
|
||||
(click)="alignment.set('left')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_left
|
||||
@@ -98,7 +97,6 @@
|
||||
aria-label="align center"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'center'"
|
||||
(click)="alignment.set('center')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_center
|
||||
@@ -112,7 +110,6 @@
|
||||
aria-label="align right"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'right'"
|
||||
(click)="alignment.set('right')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_right
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
aria-label="align left"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'left'"
|
||||
(click)="alignment.set('left')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_left
|
||||
@@ -98,7 +97,6 @@
|
||||
aria-label="align center"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'center'"
|
||||
(click)="alignment.set('center')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_center
|
||||
@@ -112,7 +110,6 @@
|
||||
aria-label="align right"
|
||||
class="material-symbols-outlined"
|
||||
[attr.aria-checked]="alignment() === 'right'"
|
||||
(click)="alignment.set('right')"
|
||||
translate="no"
|
||||
>
|
||||
format_align_right
|
||||
|
||||
@@ -110,12 +110,22 @@ export class RegistrationComponent {
|
||||
}
|
||||
```
|
||||
|
||||
A field is considered "empty" when:
|
||||
A field is considered "empty" when its value is one of the following, and non-empty for every other
|
||||
value — including `0` and the empty array `[]`:
|
||||
|
||||
| Condition | Example |
|
||||
| ------------------------ | ------- |
|
||||
| Value is `null` | `null`, |
|
||||
| Value is an empty string | `''` |
|
||||
| Condition | Example |
|
||||
| ------------------------ | ----------- |
|
||||
| Value is `null` | `null` |
|
||||
| Value is `undefined` | `undefined` |
|
||||
| Value is an empty string | `''` |
|
||||
| Value is `false` | `false` |
|
||||
| Value is `NaN` | `NaN` |
|
||||
|
||||
The last two are worth calling out:
|
||||
|
||||
- `false` is empty to follow the native semantics of `required` on `<input type="checkbox">`, where
|
||||
an unchecked box fails validation.
|
||||
- `NaN` is empty because it is usually the result of a parsing error, and is not a valid number.
|
||||
|
||||
For conditional requirements, use the `when` option:
|
||||
|
||||
@@ -130,7 +140,7 @@ registrationForm = form(this.registrationModel, (schemaPath) => {
|
||||
|
||||
The validation rule only runs when the `when` function returns `true`.
|
||||
|
||||
Note: `required` treats an empty array as present (valid), so use [`minLength()`](#minlength-and-maxlength) to enforce a minimum number of array items; it treats `false` as missing (invalid), matching `<input type="checkbox" required>`.
|
||||
Note: `required` treats an empty array as present (valid), so use [`minLength()`](#minlength-and-maxlength) to enforce a minimum number of array items.
|
||||
|
||||
### email()
|
||||
|
||||
@@ -505,9 +515,7 @@ interface User {
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
/* ... */
|
||||
})
|
||||
@Component({/* ... */})
|
||||
export class UserFormComponent {
|
||||
readonly userModel = model<User>({
|
||||
firstName: '',
|
||||
@@ -750,9 +758,7 @@ import {Component, computed, signal} from '@angular/core';
|
||||
import {form, FormField, validateStandardSchema} from '@angular/forms/signals';
|
||||
import z from 'zod';
|
||||
|
||||
@Component({
|
||||
/* ... */
|
||||
})
|
||||
@Component({/* ... */})
|
||||
export class DynamicSchema {
|
||||
model = signal({document: '', type: 'dni'});
|
||||
|
||||
|
||||
@@ -155,7 +155,8 @@ You can set the nonce for Angular in one of the following ways:
|
||||
1. Provide the nonce using the `CSP_NONCE` injection token. Use this approach if you have access to the nonce at runtime and you want to be able to cache the `index.html`.
|
||||
|
||||
```ts
|
||||
import {bootstrapApplication, CSP_NONCE} from '@angular/core';
|
||||
import {CSP_NONCE} from '@angular/core';
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
import {AppComponent} from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
|
||||
@@ -27,7 +27,6 @@ ts_project(
|
||||
"//packages/core/primitives/signals",
|
||||
"//packages/core/src/di/interface",
|
||||
"//packages/core/src/util",
|
||||
"//packages/core/test/animation_utils",
|
||||
"//packages/core/test/render3:matchers",
|
||||
"//packages/core/testing",
|
||||
"//packages/localize",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
||||
load("//tools:defaults.bzl", "ts_project")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ts_project(
|
||||
name = "animation_utils",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"tick_animation_frames.ts",
|
||||
],
|
||||
deps = [
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* @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 {tick} from '@angular/core/testing';
|
||||
|
||||
/** Ticks the specified amount of `requestAnimationFrame`-s. */
|
||||
export function tickAnimationFrames(amount: number) {
|
||||
tick(16.6 * amount); // Angular turns rAF calls into 16.6ms timeouts in tests.
|
||||
}
|
||||
@@ -17,6 +17,14 @@ import {requiredError} from './validation_errors';
|
||||
* This function can only be called on any type of path.
|
||||
* In addition to binding a validator, this function adds `REQUIRED` property to the field.
|
||||
*
|
||||
* A value is considered empty when it is `null`, `undefined`, the empty string `''`, `false`, or
|
||||
* `NaN`. Every other value is considered non-empty, including `0` and the empty array `[]` — use
|
||||
* [`minLength()`](api/forms/signals/minLength) to require a minimum number of items in an array.
|
||||
*
|
||||
* `false` is empty to follow the native semantics of `required` on `<input type="checkbox">`, where
|
||||
* an unchecked box fails validation. `NaN` is empty because it is usually the result of a parsing
|
||||
* error, and is not a valid number.
|
||||
*
|
||||
* @param path Path of the field to validate
|
||||
* @param config Optional, allows providing any of the following options:
|
||||
* - `message`: A user-facing message for the error.
|
||||
|
||||
@@ -12,6 +12,58 @@ import {form, required} from '../../../../public_api';
|
||||
import {requiredError} from '../../../../src/api/rules/validation/validation_errors';
|
||||
|
||||
describe('required validator', () => {
|
||||
// Documented on `required()` and in guide/forms/signals/validation#required. `false` follows the
|
||||
// native semantics of `required` on `<input type="checkbox">`; `NaN` is not a valid number.
|
||||
describe('emptiness', () => {
|
||||
it('treats null, empty string, false and NaN as empty', () => {
|
||||
const model = signal<{
|
||||
nullable: string | null;
|
||||
text: string;
|
||||
checkbox: boolean;
|
||||
num: number;
|
||||
}>({nullable: null, text: '', checkbox: false, num: Number.NaN});
|
||||
const f = form(
|
||||
model,
|
||||
(p) => {
|
||||
required(p.nullable);
|
||||
required(p.text);
|
||||
required(p.checkbox);
|
||||
required(p.num);
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
|
||||
expect(f.nullable().errors()).toEqual([requiredError({fieldTree: f.nullable})]);
|
||||
expect(f.text().errors()).toEqual([requiredError({fieldTree: f.text})]);
|
||||
expect(f.checkbox().errors()).toEqual([requiredError({fieldTree: f.checkbox})]);
|
||||
expect(f.num().errors()).toEqual([requiredError({fieldTree: f.num})]);
|
||||
});
|
||||
|
||||
it('treats 0, an empty array and other filled values as non-empty', () => {
|
||||
const model = signal<{
|
||||
zero: number;
|
||||
list: string[];
|
||||
checkbox: boolean;
|
||||
text: string;
|
||||
}>({zero: 0, list: [], checkbox: true, text: 'a'});
|
||||
const f = form(
|
||||
model,
|
||||
(p) => {
|
||||
required(p.zero);
|
||||
required(p.list);
|
||||
required(p.checkbox);
|
||||
required(p.text);
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
|
||||
expect(f.zero().errors()).toEqual([]);
|
||||
expect(f.list().errors()).toEqual([]);
|
||||
expect(f.checkbox().errors()).toEqual([]);
|
||||
expect(f.text().errors()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns required Error when the value is not present', () => {
|
||||
const cat = signal({name: ''});
|
||||
const f = form(
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
import {ɵgetDOM as getDOM} from '@angular/common';
|
||||
import {NgZone} from '@angular/core';
|
||||
|
||||
/** Waits for callbacks queued for the next animation frame to run. */
|
||||
export function nextAnimationFrame(): Promise<void> {
|
||||
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
||||
}
|
||||
|
||||
export function dispatchEvent(element: any, eventType: any): Event {
|
||||
const evt: Event = getDOM().getDefaultDocument().createEvent('Event');
|
||||
evt.initEvent(eventType, true, true);
|
||||
|
||||
@@ -33,21 +33,13 @@ import {
|
||||
NavigationCancellationCode,
|
||||
} from './events';
|
||||
|
||||
export const BLOCKING_SYMBOL: unique symbol = Symbol(
|
||||
export const BLOCKING_SYMBOL: unique symbol = /* @__PURE__ */ Symbol(
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? '__isBlocking' : '',
|
||||
);
|
||||
export const SOURCE_RESOURCE_SYMBOL: unique symbol = Symbol(
|
||||
export const SOURCE_RESOURCE_SYMBOL: unique symbol = /* @__PURE__ */ Symbol(
|
||||
typeof ngDevMode === 'undefined' || ngDevMode ? '__sourceResource' : '',
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if a resource has a value or has transitioned to a resolved/non-loading status.
|
||||
*/
|
||||
export function hasValueOrResolved(res: Resource<unknown>): boolean {
|
||||
const status = res.status();
|
||||
return res.hasValue() || (status !== 'loading' && status !== 'reloading');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -169,7 +161,7 @@ function createTransactionalSnapshot<T>(
|
||||
|
||||
effect(
|
||||
() => {
|
||||
if (isRollbackRecoveryPending() && hasValueOrResolved(source)) {
|
||||
if (isRollbackRecoveryPending() && !source.isLoading()) {
|
||||
isRollbackRecoveryPending.set(false);
|
||||
frozenSnapshot.set(null);
|
||||
}
|
||||
|
||||
@@ -466,24 +466,30 @@ describe('routerResource behavior tests', () => {
|
||||
expect(wrapped.value()).toBe('updated-2');
|
||||
});
|
||||
|
||||
it('should complete rollback recovery when a resource has a value even while remaining in loading state', async () => {
|
||||
it('should maintain frozen state during rollback recovery until loading completes', async () => {
|
||||
const valueSignal = signal<string | undefined>('initial');
|
||||
const statusSignal = signal<ResourceStatus>('resolved');
|
||||
const isLoadingSignal = signal<boolean>(false);
|
||||
const hasValueSignal = signal<boolean>(true);
|
||||
|
||||
const customResource: Resource<string> = {
|
||||
value: valueSignal as Signal<string>,
|
||||
status: signal<ResourceStatus>('loading').asReadonly(),
|
||||
isLoading: signal(true).asReadonly(),
|
||||
status: statusSignal.asReadonly(),
|
||||
isLoading: isLoadingSignal.asReadonly(),
|
||||
hasValue: (() => hasValueSignal()) as any,
|
||||
error: signal<Error | undefined>(undefined).asReadonly(),
|
||||
snapshot: computed(() => ({
|
||||
status: 'loading' as const,
|
||||
value: valueSignal()!,
|
||||
})),
|
||||
snapshot: computed(
|
||||
() =>
|
||||
({
|
||||
status: statusSignal(),
|
||||
value: valueSignal()!,
|
||||
}) as any,
|
||||
),
|
||||
};
|
||||
|
||||
const wrapped = TestBed.runInInjectionContext(() => routerResource(customResource));
|
||||
expect(wrapped.value()).toBe('initial');
|
||||
expect(wrapped.isLoading()).toBe(false);
|
||||
|
||||
// Start navigation to route2 with a failing guard to trigger rollback
|
||||
guardPromise2 = Promise.reject(new Error('Navigation failed'));
|
||||
@@ -492,21 +498,33 @@ describe('routerResource behavior tests', () => {
|
||||
} catch {}
|
||||
|
||||
// Reset value and set hasValue to false to simulate recovery fetch starting
|
||||
statusSignal.set('loading');
|
||||
isLoadingSignal.set(true);
|
||||
valueSignal.set(undefined);
|
||||
hasValueSignal.set(false);
|
||||
await timeout();
|
||||
|
||||
// Wrapped snapshot should be frozen at 'initial' during recovery loading
|
||||
expect(wrapped.value()).toBe('initial');
|
||||
expect(wrapped.isLoading()).toBe(false);
|
||||
|
||||
// Resource receives value while isLoading() remains true and status is 'loading'
|
||||
valueSignal.set('recovered-stream-1');
|
||||
hasValueSignal.set(true);
|
||||
await timeout();
|
||||
|
||||
// Rollback recovery remains frozen because isLoading is still true
|
||||
expect(wrapped.value()).toBe('initial');
|
||||
expect(wrapped.isLoading()).toBe(false);
|
||||
|
||||
// Loading completes
|
||||
statusSignal.set('resolved');
|
||||
isLoadingSignal.set(false);
|
||||
await harness.fixture.whenStable();
|
||||
|
||||
// Rollback recovery unfreezes because hasValue is true despite isLoading being true
|
||||
// Rollback recovery unfreezes because loading is complete
|
||||
expect(wrapped.value()).toBe('recovered-stream-1');
|
||||
expect(wrapped.isLoading()).toBe(true);
|
||||
expect(wrapped.isLoading()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -619,6 +619,62 @@ describe('Router resources integration', () => {
|
||||
expect(resourceRef.isLoading()).toBe(false);
|
||||
});
|
||||
|
||||
it('should maintain frozen state on rollback when navigation fails until rollback reload completes', async () => {
|
||||
let loaderDeferred = promiseWithResolvers<string>();
|
||||
|
||||
const {harness, router} = await setupRouter([
|
||||
{
|
||||
path: 'test/:id',
|
||||
component: TargetCmp,
|
||||
resources: (ctx) => ({
|
||||
data: resource({
|
||||
defaultValue: 'default-value',
|
||||
params: () => ctx.params(),
|
||||
loader: async ({params}) => {
|
||||
if (params['id'] === '2') {
|
||||
throw new Error('Failed navigation');
|
||||
}
|
||||
return loaderDeferred.promise;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
// Initial navigation to /test/1
|
||||
const nav1 = harness.navigateByUrl('/test/1');
|
||||
await timeout();
|
||||
loaderDeferred.resolve('loaded-1');
|
||||
await nav1;
|
||||
await harness.fixture.whenStable();
|
||||
|
||||
const resourceRef = (router.routerState.root.firstChild as ActivatedRouteInternal)
|
||||
?.resources?.['data'] as any;
|
||||
expect(resourceRef.value()).toBe('loaded-1');
|
||||
expect(resourceRef.isLoading()).toBe(false);
|
||||
|
||||
// Prepare a new deferred for the reload of id '1' when /test/2 fails and rolls back to /test/1
|
||||
loaderDeferred = promiseWithResolvers<string>();
|
||||
|
||||
const nav2 = harness.navigateByUrl('/test/2').catch(() => {});
|
||||
await nav2;
|
||||
await timeout();
|
||||
|
||||
// The rollback reload for /test/1 is pending (loaderDeferred is not resolved yet).
|
||||
// The resource should remain frozen: value is 'loaded-1', isLoading is false, status is 'resolved'
|
||||
expect(resourceRef.isLoading()).toBe(false);
|
||||
expect(resourceRef.value()).toBe('loaded-1');
|
||||
expect(resourceRef.status()).toBe('resolved');
|
||||
|
||||
// Now resolve the rollback reload
|
||||
loaderDeferred.resolve('reloaded-1');
|
||||
await harness.fixture.whenStable();
|
||||
|
||||
// After settling, resource is unfrozen and has the new value
|
||||
expect(resourceRef.isLoading()).toBe(false);
|
||||
expect(resourceRef.value()).toBe('reloaded-1');
|
||||
});
|
||||
|
||||
it('should abort previous request via AbortSignal when a new navigation comes in', async () => {
|
||||
const deferred = promiseWithResolvers<{name: string}>();
|
||||
let aborted = false;
|
||||
|
||||
@@ -11,11 +11,14 @@ import * as vscode from 'vscode';
|
||||
const ANGULAR_PROPERTY_ASSIGNMENTS = new Set([
|
||||
'template',
|
||||
'templateUrl',
|
||||
'styles',
|
||||
'styleUrls',
|
||||
'styleUrl',
|
||||
'host',
|
||||
]);
|
||||
|
||||
const ANGULAR_HOST_BINDING_DECORATORS = new Set(['HostBinding', 'HostListener']);
|
||||
|
||||
/**
|
||||
* Determines if the position is inside a decorator
|
||||
* property that supports language service features.
|
||||
@@ -27,10 +30,11 @@ export function isNotTypescriptOrSupportedDecoratorField(
|
||||
if (document.languageId !== 'typescript') {
|
||||
return true;
|
||||
}
|
||||
return isPropertyAssignmentToStringOrStringInArray(
|
||||
return isInsideAngularStringLiteral(
|
||||
document.getText(),
|
||||
document.offsetAt(position),
|
||||
ANGULAR_PROPERTY_ASSIGNMENTS,
|
||||
ANGULAR_HOST_BINDING_DECORATORS,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,27 +69,21 @@ export function isInsideStringLiteral(
|
||||
return true;
|
||||
}
|
||||
const offset = document.offsetAt(position);
|
||||
const scanner = ts.createScanner(ts.ScriptTarget.ESNext, true /* skipTrivia */);
|
||||
scanner.setText(document.getText());
|
||||
const {scanner, scan} = createTemplateAwareScanner(document.getText());
|
||||
|
||||
let token: ts.SyntaxKind = scanner.scan();
|
||||
let token: ts.SyntaxKind = scan();
|
||||
while (token !== ts.SyntaxKind.EndOfFileToken && scanner.getStartPos() < offset) {
|
||||
const isStringToken =
|
||||
token === ts.SyntaxKind.StringLiteral ||
|
||||
token === ts.SyntaxKind.NoSubstitutionTemplateLiteral;
|
||||
const isCursorInToken =
|
||||
scanner.getStartPos() <= offset &&
|
||||
scanner.getStartPos() + scanner.getTokenText().length >= offset;
|
||||
if (isCursorInToken && isStringToken) {
|
||||
if (isStringToken(token) && isCursorInToken(scanner, offset)) {
|
||||
return true;
|
||||
}
|
||||
token = scanner.scan();
|
||||
token = scan();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic scanner to determine if we're inside a string of a property with one of the given names.
|
||||
* Basic scanner to determine if we're inside a string of a property with one of the given names,
|
||||
* or inside a string argument of a decorator with one of the given names.
|
||||
*
|
||||
* This scanner is not currently robust or perfect but provides us with an accurate answer _most_ of
|
||||
* the time.
|
||||
@@ -95,24 +93,32 @@ export function isInsideStringLiteral(
|
||||
* `@Component` or `{styleUrls: [someFunction('stringL¦iteral')]}`, the @angular/language-service
|
||||
* will always give us the correct answer. This helper gives us a quick win for optimizing the
|
||||
* number of requests we send to the server.
|
||||
*
|
||||
* TODO(atscott): tagged templates don't work: #1872 /
|
||||
* https://github.com/Microsoft/TypeScript/issues/20055
|
||||
*/
|
||||
function isPropertyAssignmentToStringOrStringInArray(
|
||||
function isInsideAngularStringLiteral(
|
||||
documentText: string,
|
||||
offset: number,
|
||||
propertyAssignmentNames: Set<string>,
|
||||
decoratorNames: Set<string>,
|
||||
): boolean {
|
||||
const scanner = ts.createScanner(ts.ScriptTarget.ESNext, true /* skipTrivia */);
|
||||
scanner.setText(documentText);
|
||||
const {scanner, scan} = createTemplateAwareScanner(documentText);
|
||||
|
||||
let token: ts.SyntaxKind = scanner.scan();
|
||||
let token: ts.SyntaxKind = scan();
|
||||
let lastToken: ts.SyntaxKind | undefined;
|
||||
let lastTokenText: string | undefined;
|
||||
let secondLastToken: ts.SyntaxKind | undefined;
|
||||
let unclosedBraces = 0;
|
||||
let unclosedBrackets = 0;
|
||||
let unclosedParens = 0;
|
||||
let propertyAssignmentContext = false;
|
||||
// Nesting depth at which the current property assignment context was entered. The context ends
|
||||
// at the first terminator found at that same depth, i.e. the one that ends the assignment's
|
||||
// initializer rather than one nested inside of it.
|
||||
let contextBraces = 0;
|
||||
let contextBrackets = 0;
|
||||
let decoratorContext = false;
|
||||
// Parenthesis depth at which the current decorator context was entered. The context ends at the
|
||||
// closing parenthesis of the decorator call.
|
||||
let contextParens = 0;
|
||||
while (token !== ts.SyntaxKind.EndOfFileToken && scanner.getStartPos() < offset) {
|
||||
if (
|
||||
lastToken === ts.SyntaxKind.Identifier &&
|
||||
@@ -121,36 +127,64 @@ function isPropertyAssignmentToStringOrStringInArray(
|
||||
propertyAssignmentNames.has(lastTokenText)
|
||||
) {
|
||||
propertyAssignmentContext = true;
|
||||
token = scanner.scan();
|
||||
contextBraces = unclosedBraces;
|
||||
contextBrackets = unclosedBrackets;
|
||||
token = scan();
|
||||
continue;
|
||||
}
|
||||
if (unclosedBraces === 0 && unclosedBrackets === 0 && isPropertyAssignmentTerminator(token)) {
|
||||
if (
|
||||
propertyAssignmentContext &&
|
||||
unclosedBraces === contextBraces &&
|
||||
unclosedBrackets === contextBrackets &&
|
||||
isPropertyAssignmentTerminator(token)
|
||||
) {
|
||||
propertyAssignmentContext = false;
|
||||
}
|
||||
|
||||
if (
|
||||
secondLastToken === ts.SyntaxKind.AtToken &&
|
||||
lastToken === ts.SyntaxKind.Identifier &&
|
||||
lastTokenText !== undefined &&
|
||||
token === ts.SyntaxKind.OpenParenToken &&
|
||||
decoratorNames.has(lastTokenText)
|
||||
) {
|
||||
decoratorContext = true;
|
||||
contextParens = unclosedParens;
|
||||
}
|
||||
if (
|
||||
decoratorContext &&
|
||||
token === ts.SyntaxKind.CloseParenToken &&
|
||||
unclosedParens === contextParens + 1
|
||||
) {
|
||||
decoratorContext = false;
|
||||
}
|
||||
|
||||
if (token === ts.SyntaxKind.OpenBracketToken) {
|
||||
unclosedBrackets++;
|
||||
} else if (token === ts.SyntaxKind.OpenBraceToken) {
|
||||
unclosedBraces++;
|
||||
} else if (token === ts.SyntaxKind.OpenParenToken) {
|
||||
unclosedParens++;
|
||||
} else if (token === ts.SyntaxKind.CloseBracketToken) {
|
||||
unclosedBrackets--;
|
||||
} else if (token === ts.SyntaxKind.CloseBraceToken) {
|
||||
unclosedBraces--;
|
||||
} else if (token === ts.SyntaxKind.CloseParenToken) {
|
||||
unclosedParens--;
|
||||
}
|
||||
|
||||
const isStringToken =
|
||||
token === ts.SyntaxKind.StringLiteral ||
|
||||
token === ts.SyntaxKind.NoSubstitutionTemplateLiteral;
|
||||
const isCursorInToken =
|
||||
scanner.getStartPos() <= offset &&
|
||||
scanner.getStartPos() + scanner.getTokenText().length >= offset;
|
||||
if (propertyAssignmentContext && isCursorInToken && isStringToken) {
|
||||
if (
|
||||
(propertyAssignmentContext || decoratorContext) &&
|
||||
isStringToken(token) &&
|
||||
isCursorInToken(scanner, offset)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
lastTokenText = scanner.getTokenText();
|
||||
secondLastToken = lastToken;
|
||||
lastToken = token;
|
||||
token = scanner.scan();
|
||||
token = scan();
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -164,3 +198,62 @@ function isPropertyAssignmentTerminator(token: ts.SyntaxKind) {
|
||||
token === ts.SyntaxKind.CloseBraceToken
|
||||
);
|
||||
}
|
||||
|
||||
function isStringToken(token: ts.SyntaxKind): boolean {
|
||||
return (
|
||||
token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral
|
||||
);
|
||||
}
|
||||
|
||||
function isCursorInToken(scanner: ts.Scanner, offset: number): boolean {
|
||||
return (
|
||||
scanner.getStartPos() <= offset &&
|
||||
scanner.getStartPos() + scanner.getTokenText().length >= offset
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `ts.Scanner` for the given text along with a `scan` function that, unlike
|
||||
* `ts.Scanner#scan`, handles template literals with substitutions correctly.
|
||||
*
|
||||
* The scanner has no notion of template substitutions on its own: it reports the `}` closing a
|
||||
* `${...}` substitution as a plain `CloseBraceToken` and then treats the remainder of the template
|
||||
* literal as the beginning of a new one, mis-scanning everything that follows it (see #65494). Just
|
||||
* like the TypeScript parser, the returned `scan` function re-scans such braces as template
|
||||
* continuations (`TemplateMiddle`/`TemplateTail`) instead.
|
||||
*/
|
||||
function createTemplateAwareScanner(text: string): {
|
||||
scanner: ts.Scanner;
|
||||
scan: () => ts.SyntaxKind;
|
||||
} {
|
||||
const scanner = ts.createScanner(ts.ScriptTarget.ESNext, true /* skipTrivia */);
|
||||
scanner.setText(text);
|
||||
|
||||
// Brace depth at which each currently open template substitution started, innermost last.
|
||||
const substitutionDepths: number[] = [];
|
||||
let braceDepth = 0;
|
||||
|
||||
const scan = (): ts.SyntaxKind => {
|
||||
let token = scanner.scan();
|
||||
if (token === ts.SyntaxKind.OpenBraceToken) {
|
||||
braceDepth++;
|
||||
} else if (token === ts.SyntaxKind.CloseBraceToken) {
|
||||
if (
|
||||
substitutionDepths.length > 0 &&
|
||||
braceDepth === substitutionDepths[substitutionDepths.length - 1]
|
||||
) {
|
||||
// This brace closes a template substitution rather than a block or object literal.
|
||||
substitutionDepths.pop();
|
||||
token = scanner.reScanTemplateToken(false /* isTaggedTemplate */);
|
||||
} else {
|
||||
braceDepth--;
|
||||
}
|
||||
}
|
||||
if (token === ts.SyntaxKind.TemplateHead || token === ts.SyntaxKind.TemplateMiddle) {
|
||||
substitutionDepths.push(braceDepth);
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
return {scanner, scan};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type * as vscode from 'vscode';
|
||||
import {DocumentUri, TextDocument} from 'vscode-languageserver-textdocument';
|
||||
|
||||
import {
|
||||
isInsideStringLiteral,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
isNotTypescriptOrSupportedDecoratorRange,
|
||||
} from '../embedded_support';
|
||||
@@ -90,6 +91,144 @@ describe('embedded language support', () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside styles declared before template', () => {
|
||||
test(
|
||||
`const foo = {styles: ['h1 { col¦or: red; }'], template: '<div></div>'}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a string of another property after template', () => {
|
||||
test(
|
||||
`const foo = {template: '<div></div>', selector: 'app¦-root'}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a string in the class body after a template in the decorator', () => {
|
||||
test(
|
||||
`@Component({template: '<div></div>'})
|
||||
export class AppComponent {
|
||||
private property = 'd¦oe';
|
||||
}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a string of a nested host property', () => {
|
||||
test(
|
||||
`const foo = {host: {'[class.foo]': 'is¦Foo'}, selector: 'app-root'}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a @HostListener decorator string', () => {
|
||||
test(
|
||||
`@Component({template: '<div></div>'})
|
||||
export class AppComponent {
|
||||
@HostListener('cl¦ick', ['$event']) onClick(event: Event) {}
|
||||
}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a @HostBinding decorator string of a directive without template', () => {
|
||||
test(
|
||||
`@Directive({selector: '[foo]'})
|
||||
export class FooDirective {
|
||||
@HostBinding('class.f¦oo') isFoo = true;
|
||||
}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a string after a @HostListener decorator', () => {
|
||||
test(
|
||||
`@Component({template: '<div></div>'})
|
||||
export class AppComponent {
|
||||
@HostListener('click', ['$event']) onClick(event: Event) {
|
||||
console.log('cli¦cked');
|
||||
}
|
||||
}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a string argument of a plain function call named like a decorator', () => {
|
||||
test(`HostListener('cl¦ick');`, isNotTypescriptOrSupportedDecoratorField, false);
|
||||
});
|
||||
|
||||
it('after a template literal with substitutions', () => {
|
||||
test(
|
||||
`@Component({template: \`\`})
|
||||
export class AppComponent {
|
||||
private property = 'doe';
|
||||
private method(): void {
|
||||
console.log(\`\${this.property}\`);
|
||||
this.¦
|
||||
}
|
||||
}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('after a template literal with nested braces in a substitution', () => {
|
||||
test(
|
||||
`@Component({template: \`\`})
|
||||
export class AppComponent {
|
||||
private method(): void {
|
||||
console.log(\`\${{a: 1}.a} and \${[1, 2].map((x) => { return x; })}\`);
|
||||
this.¦
|
||||
}
|
||||
}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a template literal template with substitutions', () => {
|
||||
test(
|
||||
`const foo = {template: \`<div>\${value}</d¦iv>\`}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('inside a template literal template after a template literal with substitutions', () => {
|
||||
test(
|
||||
`const prefix = \`\${getPrefix()}-\`;
|
||||
const foo = {template: \`<div>¦</div>\`}`,
|
||||
isNotTypescriptOrSupportedDecoratorField,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInsideStringLiteral', () => {
|
||||
it('inside a string literal', () => {
|
||||
test(`const foo = 'ba¦r';`, isInsideStringLiteral, true);
|
||||
});
|
||||
|
||||
it('outside of a string literal', () => {
|
||||
test(`const foo = 'bar'; const baz = 1¦;`, isInsideStringLiteral, false);
|
||||
});
|
||||
|
||||
it('after a template literal with substitutions', () => {
|
||||
test(`const foo = \`\${bar}\`; const baz = 1¦;`, isInsideStringLiteral, false);
|
||||
});
|
||||
|
||||
it('inside a string literal after a template literal with substitutions', () => {
|
||||
test(`const foo = \`\${bar}\`; const baz = 'qu¦x';`, isInsideStringLiteral, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNotTypescriptOrSupportedDecoratorRange', () => {
|
||||
|
||||
Reference in New Issue
Block a user