mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d366e4bd02 | |||
| 7825e607c8 | |||
| c73dd603a4 | |||
| 098d28febf | |||
| fcaf15a0a4 | |||
| 79a37f3d0b | |||
| 2591fa6bd9 | |||
| a7d67f9ccc | |||
| 9658ad0ca8 | |||
| f5a968b003 | |||
| b11cdbd758 | |||
| bbbd357bd2 | |||
| b569f6e82b | |||
| 6dcb534e12 | |||
| a3fb4fe052 | |||
| 77c5815faf | |||
| ada038fca8 | |||
| f6fa9eca7d | |||
| ec16a3d6c6 | |||
| 39e362eea5 | |||
| be46ca8696 | |||
| 5496765f97 | |||
| 091456a214 | |||
| 23cf1a828b | |||
| 5d5b2ea72d | |||
| 1caafa6d8a | |||
| a99fb915c0 | |||
| da08a8fa59 | |||
| b2978dbfe1 | |||
| 522041cefe | |||
| 125a794891 | |||
| 50f990a67a | |||
| 2562246429 | |||
| 350ea416b6 | |||
| 1b85b3cd86 |
@@ -13,8 +13,7 @@ When reviewing a pull request for the `angular` repository, follow these essenti
|
||||
|
||||
2. **Key Focus Areas**:
|
||||
- **Comprehensive Reviews**: You **MUST always** perform a deep, comprehensive review of the _entire_ pull request. If the user asks you to look into a specific issue, file, or area of concern, you must investigate that specific area _in addition to_ reviewing the rest of the PR's substantive changes. Do not terminate your review after addressing only the user's focal point.
|
||||
- **Package-Specific & Topic Guidelines**: Check if there are specific guidelines for the package or topic being modified in the `reference/` directory (e.g., `reference/router.md` or `reference/object_create_null.md`). Always prioritize these rules for their respective areas.
|
||||
- **Prototype Collision & `Object.create(null)` PRs**: When reviewing PRs that swap `{}` for `Object.create(null)`, consult `reference/object_create_null.md` for technical evaluation criteria and rules.
|
||||
- **Package-Specific Guidelines**: Check if there are specific guidelines for the package being modified in the `reference/` directory (e.g., `reference/router.md`). Always prioritize these rules for their respective packages.
|
||||
- **Commit Messages**: Evaluate the quality of commit messages. They should explain the _why_ behind the change, not just the _what_. Someone should be able to look at the commit history years from now and clearly understand the context and reasoning for the change.
|
||||
- **Code Cleanliness**: Ensure the code is readable, maintainable, and follows Angular's project standards.
|
||||
- **Performance**: Look out for code that might negatively impact runtime performance or bundle size, particularly in hot paths like change detection or rendering.
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# Rules for `Object.create(null)` and Prototype Collision Prevention
|
||||
|
||||
This guide outlines the technical rules and evaluation criteria for using `Object.create(null)` versus standard object literals (`{}`) or `Map` in the Angular codebase.
|
||||
|
||||
---
|
||||
|
||||
## 1. When `Object.create(null)` is Appropriate
|
||||
|
||||
Using `Object.create(null)` (or `Map`) is appropriate when **all** of the following conditions are met:
|
||||
|
||||
1. The object is used as an **internal key-value lookup map or set**.
|
||||
2. The keys are **arbitrary or untrusted dynamic strings** (e.g., URL query parameters in `$locationShim`, HTML sanitizer tag sets, or `jsaction` DOM event-type resolvers).
|
||||
3. Property existence is checked via direct indexing or key checks (e.g., `map[key] !== undefined` or `key in map`), where a key matching an `Object.prototype` member (such as `'toString'`, `'constructor'`, or `'hasOwnProperty'`) causes false positive matches or incorrect behavior.
|
||||
|
||||
---
|
||||
|
||||
## 2. Handling Public API and Boundary Objects
|
||||
|
||||
If an object receives untrusted dynamic keys **and** is exposed to public consumers or third-party code (e.g., `SimpleChanges` in `ngOnChanges`):
|
||||
|
||||
- **Do NOT blindly change the object to `Object.create(null)`**: Stripping `Object.prototype` from public objects is a breaking API change. Consumer code calling `.hasOwnProperty()`, `.toString()`, `.valueOf()`, or using string interpolation (`` `${obj}` ``) will fail at runtime (`TypeError: obj.hasOwnProperty is not a function`).
|
||||
- **Safe Alternatives for Public Objects:**
|
||||
- **`Object.hasOwn(obj, key)`**: Use `Object.hasOwn` for internal framework property lookups instead of direct index or `in` checks. This prevents prototype collision during internal reads without breaking the object's prototype for consumers.
|
||||
- **Input Key Sanitization**: Filter or delete dangerous key names (`__proto__`, `constructor`, `prototype`) when populating the object.
|
||||
- **`Map` or Custom Classes**: For new public APIs requiring key-value stores with dynamic keys, prefer `Map<K, V>` or dedicated classes with explicit `.get()` and `.has()` methods.
|
||||
- **Deprecation / Breaking Change Process**: If changing a public object's prototype to `null` is unavoidable, it must follow Angular's formal deprecation and major version breaking change process.
|
||||
|
||||
---
|
||||
|
||||
## 3. When `Object.create(null)` Should NOT Be Used
|
||||
|
||||
Do not replace `{}` with `Object.create(null)` in the following scenarios:
|
||||
|
||||
1. **Fixed-Shape Structs and DTOs:** Objects with hardcoded static property names (e.g., `let sortedBreakpoints: {breakpoints?: number[]} = {}`). `Object.assign({}, ...)` only copies _own_ enumerable properties, so prototype properties on sources are never copied.
|
||||
2. **Numeric-Key Maps:** Objects indexed by numbers (e.g., `tasksByHandleId: {[id: number]: Task}`). Numeric keys do not collide with `Object.prototype` string members.
|
||||
3. **Reference Sentinels:** Objects used purely for reference identity checks (e.g., `const EMPTY_OBJECT = {}` or `const IN_PROGRESS_RESOLUTION = {}`).
|
||||
4. **Internal Compiler AST and Visitor State:** Temporary objects with internally generated keys where untrusted user input cannot poison key names.
|
||||
5. **Hot Performance Paths and Size-Critical Bundles:** Standard `{}` literals use V8 fast hidden classes and monomorphic inline caching. `Object.create(null)` forces V8 dictionary mode and increases minified bundle size (e.g., in inline polyfills like `event-dispatch-contract` or SSR hydration bundles).
|
||||
@@ -5,7 +5,7 @@ description: Explains the mental model and architecture of the code under `packa
|
||||
|
||||
# Signal Forms Architecture
|
||||
|
||||
The `packages/forms/signals` directory contains the signal-based forms API for Angular.
|
||||
The `packages/forms/signals` directory contains an experimental, signal-based forms API for Angular.
|
||||
This system differs significantly from the existing Reactive and Template-driven forms.
|
||||
|
||||
## Mental Model
|
||||
@@ -36,27 +36,27 @@ The central internal class representing a single field in the form graph. It agg
|
||||
|
||||
- `structure`: Manages parent/child relationships and signal slicing.
|
||||
- `validationState`: Computes `valid`, `invalid`, `errors` signals.
|
||||
- `nodeState`: Tracks `touched`, `dirty`, and derived logical state.
|
||||
- `nodeState`: Tracks `touched`, `dirty`, `pristine`.
|
||||
- `metadataState`: Stores metadata like `min`, `max`, `required`.
|
||||
- `submitState`: Tracks submission status and server errors.
|
||||
|
||||
### 2. `FieldValidationState` (`src/field/validation.ts`)
|
||||
### 2. `ValidationState` (`src/field/validation.ts`)
|
||||
|
||||
Implements `ValidationState` and manages the complexity of validation:
|
||||
Manages the complexity of validation:
|
||||
|
||||
- **Synchronous Errors**: Derived from schema rules.
|
||||
- **Asynchronous Errors**: Handled via signals, including 'pending' states.
|
||||
- **Tree Errors**: Errors that bubble up or are targeted at specific fields.
|
||||
- **Submission Errors**: Server-side errors injected imperatively via `submit()`.
|
||||
|
||||
### 3. `FormField` Directive (`src/directive/form_field.ts`)
|
||||
### 3. `FormField` Directive (`src/directive/form_field_directive.ts`)
|
||||
|
||||
The bridge between the `FieldNode` and the DOM.
|
||||
|
||||
- Selector: `[formField]`
|
||||
- It supports:
|
||||
- **Native Elements**: `<input>`, `<select>`, `<textarea>`.
|
||||
- **Custom Controls**: Components implementing `FormValueControl` or `FormCheckboxControl`.
|
||||
- **Custom Controls**: Components implementing `FormUiControl` or `FormValueControl`.
|
||||
- **Legacy Interop**: Components implementing `ControlValueAccessor` (via `InteropNgControl`).
|
||||
|
||||
### 4. `Schema` (`src/api/structure.ts` & `src/api/rules`)
|
||||
@@ -69,7 +69,7 @@ Defines the behavior.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Read**: `form.field().value()` reads directly from the underlying signal (projected to the specific path).
|
||||
1. **Read**: `form.field.value()` reads directly from the underlying signal (projected to the specific path).
|
||||
2. **Write**: Writing to the form (e.g., via UI) updates the underlying signal.
|
||||
3. **Validation**: A computed effect observes the value signal and runs validators defined in the schema.
|
||||
|
||||
@@ -97,7 +97,7 @@ const userForm = form(user, userRules); // OR apply(userForm, userRules)
|
||||
- `packages/forms/signals/src/api/structure.ts`: Public API entry points (`form`, `apply`).
|
||||
- `packages/forms/signals/src/api/control.ts`: Interfaces for custom controls (`FormUiControl`).
|
||||
- `packages/forms/signals/src/field/node.ts`: The `FieldNode` implementation.
|
||||
- `packages/forms/signals/src/directive/form_field.ts`: The `[formField]` directive.
|
||||
- `packages/forms/signals/src/directive/form_field_directive.ts`: The `[formField]` directive.
|
||||
|
||||
## Supplemental Information
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
This document explains how the Signal Forms system hooks into the Angular compiler and runtime to provide seamless type-checking and efficient updates.
|
||||
|
||||
## 1. Compiler Integration (Type Checking)
|
||||
## 1. Compiler-CLI Integration (Type Checking)
|
||||
|
||||
The `packages/compiler` package contains specific logic to support `[formField]`. This is primarily handled in `src/typecheck/ops/signal_forms.ts`.
|
||||
The `packages/compiler-cli` package contains specific logic to support `[formField]`. This is primarily handled in `src/ngtsc/typecheck/src/ops/signal_forms.ts`.
|
||||
|
||||
### Key Mechanisms:
|
||||
|
||||
@@ -22,7 +22,7 @@ The `packages/compiler` package contains specific logic to support `[formField]`
|
||||
|
||||
### Relevant Files:
|
||||
|
||||
- `packages/compiler/src/typecheck/ops/signal_forms.ts`: The core Signal Forms type-checking logic, including `TcbNativeFieldOp`.
|
||||
- `packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts`: The core logic for `TcbNativeFieldOp` and `SignalFormFieldOp`.
|
||||
|
||||
## 2. Core Runtime Integration
|
||||
|
||||
@@ -30,8 +30,8 @@ The `packages/core` package provides the low-level instructions that power the `
|
||||
|
||||
### Key Mechanisms:
|
||||
|
||||
- **`ɵngControlCreate` Hook**: The `FormField` directive defines a special method `ɵngControlCreate`, which causes the compiler to install `ɵɵControlFeature`.
|
||||
- **`ɵɵcontrol` Instructions**: A `formField` binding causes the compiler to emit:
|
||||
- **`ɵngControlCreate` Hook**: The `FormField` directive defines a special method `ɵngControlCreate`.
|
||||
- **`ɵɵcontrol` Instructions**: When the compiler sees `ɵngControlCreate`, it emits:
|
||||
- `ɵɵcontrolCreate`: Called during the creation phase.
|
||||
- `ɵɵcontrol`: Called during the update phase.
|
||||
- **`ControlDirectiveHost`**: These instructions provide the directive with a `ControlDirectiveHost`. This is a privileged interface that allows the `FormField` directive to:
|
||||
@@ -43,4 +43,4 @@ The `packages/core` package provides the low-level instructions that power the `
|
||||
### Relevant Files:
|
||||
|
||||
- `packages/core/src/render3/instructions/control.ts`: Implementation of `ɵɵcontrol` instructions.
|
||||
- `packages/forms/signals/src/directive/form_field.ts`: The directive that implements the hook.
|
||||
- `packages/forms/signals/src/directive/form_field_directive.ts`: The directive that implements the hook.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
8.8.0
|
||||
8.7.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
"types": ["node"],
|
||||
"lib": ["es2021"],
|
||||
"experimentalDecorators": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noImplicitOverride": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -12,4 +13,4 @@
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
name: Deploying adev preview to Firebase
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code}
|
||||
workflow_run:
|
||||
workflows: ['Build adev for preview deployment']
|
||||
types: [completed]
|
||||
@@ -46,7 +45,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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
with:
|
||||
github-token: '${{secrets.GITHUB_TOKEN}}'
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
|
||||
@@ -2,7 +2,6 @@ name: DevInfra
|
||||
|
||||
on:
|
||||
push:
|
||||
# zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code}
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled]
|
||||
|
||||
@@ -18,6 +17,6 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -45,14 +45,14 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
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@09090944bd8aaa2a517cefb6e38bf1aae336b42b # v7.4.3
|
||||
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build adev
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name: DevInfra
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code}
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
issues:
|
||||
@@ -16,7 +15,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@649c3afeaa46674507b9625537e49de54a695e2b
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
labels: '{"requires: TGP": ["packages/core/primitives/**/{*,.*}"]}'
|
||||
@@ -24,14 +23,14 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@649c3afeaa46674507b9625537e49de54a695e2b
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/labeling/issue@649c3afeaa46674507b9625537e49de54a695e2b
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name: Google Internal Tests Enforcement
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code}
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
@@ -15,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
with:
|
||||
run-tests-guide-url: http://go/angular-g3sync-start
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: Merge Ready
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - {Trigger is safe as workflow does not checkout untrusted code}
|
||||
pull_request_target:
|
||||
on: pull_request_target
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: {}
|
||||
@@ -11,6 +9,6 @@ jobs:
|
||||
status:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -20,7 +21,7 @@ jobs:
|
||||
workflows: ${{ steps.workflows.outputs.workflows }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- id: workflows
|
||||
@@ -30,17 +31,14 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: list
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
strategy:
|
||||
matrix:
|
||||
workflow: ${{ fromJSON(needs.list.outputs.workflows) }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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
|
||||
@@ -52,7 +50,4 @@ jobs:
|
||||
project_id: 'internal-200822'
|
||||
workload_identity_provider: 'projects/823469418460/locations/global/workloadIdentityPools/measurables-tracking/providers/angular'
|
||||
service_account: 'measures-uploader@internal-200822.iam.gserviceaccount.com'
|
||||
- run: pnpm ng-dev perf workflows --name "$WORKFLOW" --commit-sha "$COMMIT_SHA"
|
||||
env:
|
||||
WORKFLOW: ${{ matrix.workflow }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
- run: pnpm ng-dev perf workflows --name ${{ matrix.workflow }} --commit-sha ${{github.sha}}
|
||||
|
||||
+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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@09090944bd8aaa2a517cefb6e38bf1aae336b42b # v7.4.3
|
||||
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
|
||||
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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@442c2fcbf06a321b5196b4c5fc70e78a49242958
|
||||
- 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@4a09f9588769980eacb41fcc76e2c76d048eac42 # main
|
||||
uses: angular/dev-infra/.github/workflows/reusable-release.yml@a8a341bd2b49b931c92408d497588b6acd012eeb
|
||||
secrets:
|
||||
wombot-token: ${{ secrets.WOMBOT_TOKEN }}
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
name: OpenSSF Scorecard
|
||||
on:
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecards analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Needed to publish results
|
||||
id-token: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: 'Checkout code'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "es2020",
|
||||
"module": "Node16",
|
||||
"noEmit": true,
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ function readPackage(pkg, context) {
|
||||
if (pkg.version === '0.0.0-PLACEHOLDER' && localAngularPackages.has(key)) {
|
||||
pkg.dependencies = {
|
||||
...pkg.dependencies,
|
||||
[key]: 'workspace:*',
|
||||
[key]: 'workspace: *',
|
||||
};
|
||||
|
||||
delete pkg.peerDependencies[key];
|
||||
|
||||
+19
-8
@@ -120,7 +120,6 @@ groups:
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files.exclude('packages/core/primitives/*'), [
|
||||
'.agent/skills/**/{*,.*}',
|
||||
'contributing-docs/public-api-surface.md',
|
||||
'dev-app/**/{*,.*}',
|
||||
'integration/**/{*,.*}',
|
||||
@@ -138,7 +137,6 @@ groups:
|
||||
'packages/platform-browser/**/{*,.*}',
|
||||
'packages/platform-browser-dynamic/**/{*,.*}',
|
||||
'packages/platform-server/**/{*,.*}',
|
||||
'packages/private/**/{*,.*}',
|
||||
'packages/ssr/**/{*,.*}',
|
||||
'packages/router/**/{*,.*}',
|
||||
'packages/service-worker/**/{*,.*}',
|
||||
@@ -208,7 +206,6 @@ groups:
|
||||
- crisbeto
|
||||
- devversion
|
||||
- kirjs
|
||||
- alan-agius4
|
||||
- ~thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- ~amishne
|
||||
@@ -303,10 +300,9 @@ groups:
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files
|
||||
.exclude('.pullapprove.yml')
|
||||
.exclude('.pullapprove.yml')
|
||||
.exclude('tools/manual_api_docs/blocks/*.md')
|
||||
.exclude('tools/manual_api_docs/elements/*.md')
|
||||
.exclude('.agent/**/*.md'),
|
||||
.exclude('tools/manual_api_docs/elements/*.md'),
|
||||
[
|
||||
'{*,.*}',
|
||||
'.agent/**/{*,.*}',
|
||||
@@ -323,6 +319,7 @@ groups:
|
||||
'modules/{*,.*}',
|
||||
'packages/{*,.*}',
|
||||
'packages/examples/test-utils/**/{*,.*}',
|
||||
'packages/private/**/{*,.*}',
|
||||
'packages/examples/{*,.*}',
|
||||
'scripts/**/{*,.*}',
|
||||
'third_party/**/{*,.*}',
|
||||
@@ -484,9 +481,23 @@ groups:
|
||||
- ~thePunderWoman # Jessica Janiuk
|
||||
- ~AndrewKushnir # Andrew Kushnir
|
||||
- atscott # Andrew Scott
|
||||
|
||||
# External team required reviews
|
||||
primitives-shared:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files.exclude('packages/core/primitives/**/*spec.ts'), [
|
||||
'packages/core/primitives/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- csmick # Cameron Smick
|
||||
- mturco # Matt Turco
|
||||
- iteriani # Thomas Nguyen
|
||||
- tbondwilkinson # Tom Wilkinson
|
||||
- rahatarmanahmed # Rahat Ahmed
|
||||
- e-cline # Ethan Cline
|
||||
- rockymeza # Rocky Meza
|
||||
- ENAML # Ethan Cline
|
||||
|
||||
####################################################################################
|
||||
# Override managed result groups
|
||||
|
||||
-557
@@ -1,542 +1,3 @@
|
||||
<a name="22.2.0-next.7"></a>
|
||||
# 22.2.0-next.7 (2026-09-09)
|
||||
No user facing changes in this release
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.2.0-next.6"></a>
|
||||
# 22.2.0-next.6 (2026-09-09)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9b80d4ce9e](https://github.com/angular/angular/commit/9b80d4ce9e00d50d471c1a1a98ab67ec53fbce5f) | fix | namespace `@property` declarations |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f0a271c7bd](https://github.com/angular/angular/commit/f0a271c7bd4be000c52592034573cda9866f26bb) | fix | do not flag callable objects with zero parameters in uninvoked track function check |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f6afb807c1](https://github.com/angular/angular/commit/f6afb807c1e62d26b8b665f2b4a9a52c2433a673) | feat | add ErrorBoundary programmatic APIs ([#70463](https://github.com/angular/angular/pull/70463)) |
|
||||
| [bd9b45b5cc](https://github.com/angular/angular/commit/bd9b45b5cc1dd904cc4a5de45f6de8e1564b70b6) | feat | allow reading `Injector` from a view or content query |
|
||||
| [70756b506c](https://github.com/angular/angular/commit/70756b506c508faa908fe8917e1f6937bcd6df77) | fix | apply SkipSelf to only the starting node in embedded views |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f4a5650ed9](https://github.com/angular/angular/commit/f4a5650ed9c71a8ee1dbd3003e13900464827757) | feat | add support for `@boundary` blocks ([#70463](https://github.com/angular/angular/pull/70463)) |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [bc3a6cda5d](https://github.com/angular/angular/commit/bc3a6cda5d7c4ef9207ee6c6adbcd55b7bd7dcab) | fix | avoid sourcemap corruption during domino path substitution |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3064f3f1dc](https://github.com/angular/angular/commit/3064f3f1dccd78177bf3b86f8ea231102884f0d7) | feat | expose router resources in public API |
|
||||
| [094bce9e3d](https://github.com/angular/angular/commit/094bce9e3defe7e9fd56f951641f3bb74248d06a) | fix | determine blocking state solely by resource loading status |
|
||||
| [8227e5cf6d](https://github.com/angular/angular/commit/8227e5cf6d157d6e472f963da68a804813ad26ed) | fix | keep detached route subtree contexts isolated and intact |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.1.6"></a>
|
||||
# 22.1.6 (2026-09-09)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [cbb8702143](https://github.com/angular/angular/commit/cbb8702143bd04b68253758dbc2c4a8fb49c0237) | fix | namespace `@property` declarations |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [aea121e532](https://github.com/angular/angular/commit/aea121e53217a7f74d2a8e4a7a6bf0f3a96b7c49) | fix | do not flag callable objects with zero parameters in uninvoked track function check |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c19b948ef8](https://github.com/angular/angular/commit/c19b948ef8d34e5a15e41cd5a88cdd4bda848f8f) | fix | apply SkipSelf to only the starting node in embedded views |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d20a379583](https://github.com/angular/angular/commit/d20a37958318c898a72109480cee556e388cd78b) | fix | avoid sourcemap corruption during domino path substitution |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [95c01e9cde](https://github.com/angular/angular/commit/95c01e9cdea251e09d321a01e9b982b2fe1b7873) | fix | keep detached route subtree contexts isolated and intact |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="20.3.31"></a>
|
||||
# 20.3.31 (2026-09-09)
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3db26e2544](https://github.com/angular/angular/commit/3db26e2544964d07a520836ad01f02f36ec88e25) | fix | update domino to latest version |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.23"></a>
|
||||
# 21.2.23 (2026-09-09)
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fc2e8fbc0b](https://github.com/angular/angular/commit/fc2e8fbc0bda2f5c11585e8a0cbd88c074f5e567) | fix | update domino to latest version |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.2.0-next.5"></a>
|
||||
# 22.2.0-next.5 (2026-09-02)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [faafd18a4c](https://github.com/angular/angular/commit/faafd18a4c0c5db161e5d7056e9abfb8675904cb) | fix | check uninvoked signal aliases in extended diagnostic |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [05c4d5a835](https://github.com/angular/angular/commit/05c4d5a8354228100b51176f295ed5dee4f3febc) | feat | add utility for testing directives |
|
||||
| [91a2bf8425](https://github.com/angular/angular/commit/91a2bf84250f9b0a28a8a68e1eded6e9506bc5e9) | feat | support annotations in WebMCP tool declarations |
|
||||
| [0904f90b13](https://github.com/angular/angular/commit/0904f90b13ac2a1bad22a2b43fb232957dba44c3) | fix | cancel stale debounce timers to prevent timer leaks |
|
||||
| [58d536bc83](https://github.com/angular/angular/commit/58d536bc8362f3942c929e58be551d871b00cea7) | fix | don't fail NgModule checks for a pipe that extends a base class |
|
||||
| [7168bed663](https://github.com/angular/angular/commit/7168bed663ba2a53f087a68a251e8ccfd938eb97) | fix | validate SVG animation attributes outside the SVG namespace |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6e299da8cf](https://github.com/angular/angular/commit/6e299da8cfc7df49029d5d8bb7d5739560e1aaa4) | feat | hard-code `readOnlyHint` and `untrustedContentHint` for WebMCP implicit signal forms |
|
||||
| [38861ac41b](https://github.com/angular/angular/commit/38861ac41bb373870e961f41626de6b6b31120f1) | fix | avoid writing to `name` input on ControlValueAccessor |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f72600eadd](https://github.com/angular/angular/commit/f72600eaddfa6a299526e922eb9ad5356be821a6) | fix | skip tsconfig files of non-Angular projects |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b3bb36ad87](https://github.com/angular/angular/commit/b3bb36ad87334f7a86908c1b2b4353c1cc514c2e) | fix | resolve HTTP(S) URLs without authority as relative during SSR |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7137a41223](https://github.com/angular/angular/commit/7137a41223079b4b172aeccb5031347fcc947b79) | feat | stabilize auto cleanup injectors feature |
|
||||
| [af26a8c521](https://github.com/angular/angular/commit/af26a8c52178a0e972c2841051db44f9d04ac371) | fix | avoid view transitions when the user agent provides one |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.1.5"></a>
|
||||
# 22.1.5 (2026-09-02)
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d90698dae7](https://github.com/angular/angular/commit/d90698dae759f21f29052e521ffaec6c3a79f6d1) | fix | check uninvoked signal aliases in extended diagnostic |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2ceeb27078](https://github.com/angular/angular/commit/2ceeb27078d890514a1e87b0d99f366d9a4a796f) | fix | cancel stale debounce timers to prevent timer leaks |
|
||||
| [ed2e401d2d](https://github.com/angular/angular/commit/ed2e401d2da9a6dd5c87006573a8e42934f58f62) | fix | don't fail NgModule checks for a pipe that extends a base class |
|
||||
| [c65d378bb4](https://github.com/angular/angular/commit/c65d378bb4fded436f197aa7bc52c03f507b8bd4) | fix | validate SVG animation attributes outside the SVG namespace |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [43aaeaec29](https://github.com/angular/angular/commit/43aaeaec29e15d7b2504e8930c92c94b898ca200) | fix | avoid writing to `name` input on ControlValueAccessor |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d85288d75f](https://github.com/angular/angular/commit/d85288d75f3f85dc6b10bf813c805b1b7fed7329) | fix | skip tsconfig files of non-Angular projects |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3b8723ce3d](https://github.com/angular/angular/commit/3b8723ce3d88b2bf871185736bc33e3cf9a189f4) | fix | resolve HTTP(S) URLs without authority as relative during SSR |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [4340a63c52](https://github.com/angular/angular/commit/4340a63c52fca4d08ef258a3c053f50932eab27e) | fix | avoid view transitions when the user agent provides one |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.2.0-next.4"></a>
|
||||
# 22.2.0-next.4 (2026-08-26)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [862a0c8ab3](https://github.com/angular/angular/commit/862a0c8ab3403242eeac3b2fb80f7cf63cf0c892) | fix | avoid prototype member collisions |
|
||||
| [7596548e9b](https://github.com/angular/angular/commit/7596548e9b836a5d12e966bf296a4dbba044b78f) | fix | use locale NaN symbol in number formatting |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [58b0cb4735](https://github.com/angular/angular/commit/58b0cb47358861de0a512711addb8e29f984a57e) | fix | scope animations declared in minified nested rules |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7d9f55da11](https://github.com/angular/angular/commit/7d9f55da11319da8f273d9edcd38ff2983bdbb0c) | feat | scope type-checking of keyed defer blocks |
|
||||
| [2e2c426e76](https://github.com/angular/angular/commit/2e2c426e7609de551dafb6360e6373333a278c4e) | fix | deduplicate deferred imports across multiple blocks |
|
||||
| [a46292af26](https://github.com/angular/angular/commit/a46292af260f241da2f9f4e4cbb7d40b14a678f1) | fix | default template diagnostic related message source file to template |
|
||||
| [e9ba39d671](https://github.com/angular/angular/commit/e9ba39d671a3645ef5ca8233c96bacc94f02d155) | fix | Produce correct tcb expression for optional chaining |
|
||||
| [74b294cd51](https://github.com/angular/angular/commit/74b294cd51b0f73e3a71e0cd13a176b1d0f3a89f) | fix | retain metadata for strict standalone errors |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7ba082e08e](https://github.com/angular/angular/commit/7ba082e08e59eadec4594ad80661d5c1ec08cb2a) | fix | avoid prototype member collisions |
|
||||
| [915a03ae85](https://github.com/angular/angular/commit/915a03ae856a595bd5fa0f364928869db8daec19) | fix | explicitly reject foreign components in JIT mode |
|
||||
| [2ab5ff56de](https://github.com/angular/angular/commit/2ab5ff56ded8c6246c0035d9df62a0c7fc9e1308) | fix | preserve namespace for dynamic component hosts |
|
||||
| [168a324cce](https://github.com/angular/angular/commit/168a324cce0f133ed92953374c2510a75868b790) | fix | prevent TransferState prototype pollution |
|
||||
| [83f7695b2e](https://github.com/angular/angular/commit/83f7695b2e4e1cab347f7817da4ae4262dade415) | fix | throw coded RuntimeErrors instead of crashing when hydration/rendering can't find an expected DOM node |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1e35de536d](https://github.com/angular/angular/commit/1e35de536d8332dfcbb3d89d01ca84b50aa56bd2) | fix | use dot-access for readonly rule configuration |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0f0d52e9c1](https://github.com/angular/angular/commit/0f0d52e9c1154f8db9a4a8d9d0ff73f0afdc7031) | fix | honor quote style preference when generating imports |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [bdc09e8183](https://github.com/angular/angular/commit/bdc09e8183a3280d25bfc93b4d5ae4452cc551ec) | fix | preserve registerLocaleData calls in standalone bootstrap migration |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3e924cc8db](https://github.com/angular/angular/commit/3e924cc8dbbb57f23b262cb8f0d7e2bd0673034c) | fix | avoid stripping unicode whitespace during url resolution |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.1.4"></a>
|
||||
# 22.1.4 (2026-08-26)
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [60a874c3fb](https://github.com/angular/angular/commit/60a874c3fbedc2ce216bcf783325f4e1291edcb0) | fix | avoid prototype member collisions |
|
||||
| [e8378dfeab](https://github.com/angular/angular/commit/e8378dfeab39fca5bcdc47126b7859c8fdab7a28) | fix | use locale NaN symbol in number formatting |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [05c7c725a5](https://github.com/angular/angular/commit/05c7c725a534f4ea14c718503966423133474bbf) | fix | scope animations declared in minified nested rules |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [afe8499a14](https://github.com/angular/angular/commit/afe8499a14c1bf8ab42e6e30b35917a873745540) | fix | default template diagnostic related message source file to template |
|
||||
| [079a846263](https://github.com/angular/angular/commit/079a846263599daf38cc1903c46ccea99c5758f8) | fix | Produce correct tcb expression for optional chaining |
|
||||
| [55eeb46418](https://github.com/angular/angular/commit/55eeb46418080e2faf64c84eac389c21abea6df8) | fix | retain metadata for strict standalone errors |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3da35cbab9](https://github.com/angular/angular/commit/3da35cbab94777230871a3e9289455089b80fe7f) | fix | avoid prototype member collisions |
|
||||
| [b199bdfa2a](https://github.com/angular/angular/commit/b199bdfa2ad8b3fe189e7b4cef9dbea600cd4732) | fix | explicitly reject foreign components in JIT mode |
|
||||
| [9a8e4826b9](https://github.com/angular/angular/commit/9a8e4826b9db4359c3f0e7185e9582b7bd08a3ef) | fix | preserve namespace for dynamic component hosts |
|
||||
| [7c752d4815](https://github.com/angular/angular/commit/7c752d4815db41ee04e43d38cc826dd63214432b) | fix | prevent TransferState prototype pollution |
|
||||
| [7546b7a805](https://github.com/angular/angular/commit/7546b7a80549c9620d52692cb0f5cab4c69b546c) | fix | throw coded RuntimeErrors instead of crashing when hydration/rendering can't find an expected DOM node |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c8eb7f0056](https://github.com/angular/angular/commit/c8eb7f00564beceb21445b1f0374c93854b513d1) | fix | use dot-access for readonly rule configuration |
|
||||
### language-service
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [93d7f718d2](https://github.com/angular/angular/commit/93d7f718d2e057e37deda42d197e46672fe7f7f5) | fix | honor quote style preference when generating imports |
|
||||
### migrations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [85c8829ac1](https://github.com/angular/angular/commit/85c8829ac189b0f47b865b036dad98905e576dc9) | fix | preserve registerLocaleData calls in standalone bootstrap migration |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [71e52d1396](https://github.com/angular/angular/commit/71e52d1396b9cef98652929b73e08c4cde645970) | fix | avoid stripping unicode whitespace during url resolution |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.22"></a>
|
||||
# 21.2.22 (2026-08-26)
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5aa6d97deb](https://github.com/angular/angular/commit/5aa6d97deb9ef1de14e23748b7fa74f97d183132) | fix | avoid stripping unicode whitespace during url resolution |
|
||||
| [73d8bbd27c](https://github.com/angular/angular/commit/73d8bbd27cb46495426d4132975a1355b47ad915) | fix | update domino to latest version |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="20.3.30"></a>
|
||||
# 20.3.30 (2026-08-26)
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9339a7a2de](https://github.com/angular/angular/commit/9339a7a2de437ed93f9cc3da7f32d0100412d599) | fix | avoid stripping unicode whitespace during url resolution |
|
||||
| [89b20568df](https://github.com/angular/angular/commit/89b20568dfaee1ec8e0b3bcf1872acdddd2f4fef) | fix | update domino to latest version |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.2.0-next.3"></a>
|
||||
# 22.2.0-next.3 (2026-08-19)
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c73a001fbf](https://github.com/angular/angular/commit/c73a001fbf766b350fab4e64244cfaee23fdc8bc) | fix | detect object trigger values with Object.hasOwn |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [46d2cb7ff0](https://github.com/angular/angular/commit/46d2cb7ff049dc4549080f64dceb1541b3a87cee) | fix | preserve literal key union in KeyValuePipe.transform() |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [48a0fd6e8a](https://github.com/angular/angular/commit/48a0fd6e8a8d14bdc1d901ee5615f4b0ab698fe8) | feat | allow template to access private props |
|
||||
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [dc65e3656f](https://github.com/angular/angular/commit/dc65e3656ff56fc6c5c03cf59991a6e8760b5279) | fix | accept readonly arrays for setClassMetadata decorators |
|
||||
| [eee9ef4d09](https://github.com/angular/angular/commit/eee9ef4d09c091e9066ed6074a19c0eee0e84da2) | fix | allow readonly arrays in RawScopeInfoFromDecorator |
|
||||
| [0ddbc47e7f](https://github.com/angular/angular/commit/0ddbc47e7f6a4eced3a1a986b260747dc37ee9c9) | fix | expose debuggableFn for non-computed signal graph nodes |
|
||||
| [c2b14b7ab4](https://github.com/angular/angular/commit/c2b14b7ab44003d6fd1154300c5cc483644d80e6) | fix | prevent orphaned requestIdleCallback handle from re-entrant scheduling |
|
||||
| [732e505018](https://github.com/angular/angular/commit/732e5050180190df24768ace7aa3e785ca0dcc0a) | fix | replace all hasOwnProperty usages with Object.hasOwn |
|
||||
| [c658d73210](https://github.com/angular/angular/commit/c658d73210d1a2e808cfe659dddd48e8389086c7) | fix | stop running further effects once one destroys the view mid-flush |
|
||||
| [c6e4a36be1](https://github.com/angular/angular/commit/c6e4a36be1246e0fbe06d591a69ac79cd74348d2) | fix | throw a descriptive error instead of crashing when a hydration node is missing |
|
||||
| [4560f4fdcd](https://github.com/angular/angular/commit/4560f4fdcda4d55c9234f71fda2e73938cb55ed6) | fix | throw NG0500 instead of a raw TypeError on element hydration mismatch |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c819880b91](https://github.com/angular/angular/commit/c819880b91bf5ff1c5aa71afd314ae7cae241065) | fix | report forbidden 2way bindings on when `FormField` is applied |
|
||||
| [38d093232c](https://github.com/angular/angular/commit/38d093232c6d2fad99f2e361e9f6e31923507658) | fix | warn in dev mode when ngModel cannot reach parent NgForm across component boundary |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [1a006a8f97](https://github.com/angular/angular/commit/1a006a8f9794240a950c388387bd8b9a671801da) | fix | cancel oversized fetch response bodies |
|
||||
### language-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3f8d9d6ea6](https://github.com/angular/angular/commit/3f8d9d6ea6a0a0e7edbc12baf08b1f6db8c0f6b8) | fix | recover project for external templates in solution-style workspaces |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6f9a6bea50](https://github.com/angular/angular/commit/6f9a6bea50944fff894eb5629075b13f8c3090d7) | fix | disallow event handler attributes in Meta |
|
||||
| [1cb3d606bf](https://github.com/angular/angular/commit/1cb3d606bf5bb01ec9c9e9289703a79f64c76e89) | fix | throw a descriptive error when insertBefore reference node is missing |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2720362818](https://github.com/angular/angular/commit/2720362818cdeb2a940171e4ab6f21cf78c6a302) | feat | add containsTree as public API |
|
||||
| [f1a4c85212](https://github.com/angular/angular/commit/f1a4c8521296d7414a1eab84352ca7db966f35e2) | fix | pass correct component to canDeactivate for named outlets in componentless parent routes |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.1.3"></a>
|
||||
# 22.1.3 (2026-08-19)
|
||||
### animations
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d9620e0f1b](https://github.com/angular/angular/commit/d9620e0f1b85ff5896e4d725c52c5ad6c30e1af9) | fix | detect object trigger values with Object.hasOwn |
|
||||
### common
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b3c78a5081](https://github.com/angular/angular/commit/b3c78a50816af5685b2a6ed51166d08941c09946) | fix | preserve literal key union in KeyValuePipe.transform() |
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [94f0b9a371](https://github.com/angular/angular/commit/94f0b9a3710ad000cf927caf5333db3582db1126) | fix | preserve &ngsp; between sibling control flow blocks |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [afe529cb2d](https://github.com/angular/angular/commit/afe529cb2d97e766b4f9014c3b6864420b7c8deb) | fix | accept readonly arrays for setClassMetadata decorators |
|
||||
| [2c72fe3797](https://github.com/angular/angular/commit/2c72fe3797ff92ead7a2531d32665fac370b4688) | fix | allow readonly arrays in RawScopeInfoFromDecorator |
|
||||
| [ef2ce9a098](https://github.com/angular/angular/commit/ef2ce9a09802faa5ffff0860f473c4018f25f2ac) | fix | expose debuggableFn for non-computed signal graph nodes |
|
||||
| [7bcce260f5](https://github.com/angular/angular/commit/7bcce260f51dc8a099c8e63f1e1e3ff8f580dcf1) | fix | prevent orphaned requestIdleCallback handle from re-entrant scheduling |
|
||||
| [44137117b3](https://github.com/angular/angular/commit/44137117b361f5bbad64e3d488e9aefd2f9ec5cb) | fix | replace all hasOwnProperty usages with Object.hasOwn |
|
||||
| [85f12a5a13](https://github.com/angular/angular/commit/85f12a5a130aa7fd5cd21fce3eb786a2262480e1) | fix | stop running further effects once one destroys the view mid-flush |
|
||||
| [c04931c88b](https://github.com/angular/angular/commit/c04931c88b1c7959cb95abd28cf5b3614d97e168) | fix | throw a descriptive error instead of crashing when a hydration node is missing |
|
||||
| [601d1f6576](https://github.com/angular/angular/commit/601d1f6576b296748f56ffb50646bb1808a7c0fc) | fix | throw NG0500 instead of a raw TypeError on element hydration mismatch |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [83450d2924](https://github.com/angular/angular/commit/83450d292443cae811e87ab61020f3ed72d27d77) | fix | report forbidden 2way bindings on when `FormField` is applied |
|
||||
| [5cb4ea7e35](https://github.com/angular/angular/commit/5cb4ea7e355bb9ea43efa15a03dc38c53d2e4faf) | fix | warn in dev mode when ngModel cannot reach parent NgForm across component boundary |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0cd635e9e2](https://github.com/angular/angular/commit/0cd635e9e2c409a045eaeaf920a226ef54aecc48) | fix | cancel oversized fetch response bodies |
|
||||
### language-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [14fbe04612](https://github.com/angular/angular/commit/14fbe04612d29460c5d01d6c65cde579f0a42e95) | fix | recover project for external templates in solution-style workspaces |
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3ddcb1a101](https://github.com/angular/angular/commit/3ddcb1a10168ead616e523bbe044f4c39ada4cca) | fix | disallow event handler attributes in Meta |
|
||||
| [640460d606](https://github.com/angular/angular/commit/640460d6065cdcd8fe407c7b35883f81f5045473) | fix | throw a descriptive error when insertBefore reference node is missing |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [abe019d505](https://github.com/angular/angular/commit/abe019d5050532c87179cf46e840965e70a78b1b) | fix | pass correct component to canDeactivate for named outlets in componentless parent routes |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="20.3.29"></a>
|
||||
# 20.3.29 (2026-08-19)
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [7538744c10](https://github.com/angular/angular/commit/7538744c1048701d12e86e992704ea5fa13df4f4) | fix | disallow event handler attributes in Meta |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.21"></a>
|
||||
# 21.2.21 (2026-08-19)
|
||||
### platform-browser
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [c19a36c2fb](https://github.com/angular/angular/commit/c19a36c2fbc51f41ce974ea1fd87f9f56b3e2444) | fix | disallow event handler attributes in Meta |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.1.2"></a>
|
||||
# 22.1.2 (2026-08-13)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [76dff307b4](https://github.com/angular/angular/commit/76dff307b4c34bc4ceefcb1c7aa86c39daa5ea45) | fix | Generate correct expression for optional chaning. |
|
||||
| [6f9a64e6f5](https://github.com/angular/angular/commit/6f9a64e6f5b1e3faada6514e8b090f9d61aa8e54) | fix | remove namespaced MathML script elements |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e9660b1801](https://github.com/angular/angular/commit/e9660b1801a0280fa5ba41e7c330cb6d597199ec) | fix | correctly resolve symbol for SafePropertyRead in chained optional navigation |
|
||||
| [ec6deea513](https://github.com/angular/angular/commit/ec6deea51329b0c8410c75943d90ce823081e216) | fix | record class extends clause references in DeferredSymbolTracker |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [0df9d5eb65](https://github.com/angular/angular/commit/0df9d5eb6516106188b879536f7f813a9ca8811c) | fix | ensure i18n_util hasOwnProperty checks are safe for property renaming |
|
||||
| [4d985a179e](https://github.com/angular/angular/commit/4d985a179e66428d46e60b4622435f88c39e1d2b) | fix | incorrect loop in defer blocks |
|
||||
| [7b40456792](https://github.com/angular/angular/commit/7b404567924840aabdb955704a9bc04778b14f73) | fix | reject prefixed SVG script hosts |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [3b5c798072](https://github.com/angular/angular/commit/3b5c798072a3cf43b5e0ec0b3f4abc18bc6196a5) | fix | keep radio inputs in sync when values change |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [4f7e9987fa](https://github.com/angular/angular/commit/4f7e9987fa35888de43895bfd859382dfa436e51) | fix | always decode JSON responses as UTF-8 |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2f82601662](https://github.com/angular/angular/commit/2f82601662a958b3accd4d22dff7ad4ec7c56d77) | fix | limit protocol-relative URL handling to serialization |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.2.0-next.2"></a>
|
||||
# 22.2.0-next.2 (2026-08-13)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a4f1a94948](https://github.com/angular/angular/commit/a4f1a949486d20ce2bf9add427eea6629228b28b) | fix | do not encapsulate nested selectors if parent contains ::ng-deep |
|
||||
| [d7b03f5523](https://github.com/angular/angular/commit/d7b03f5523d220c7935426d23ff5f103c96b00d9) | fix | Generate correct expression for optional chaning. |
|
||||
| [107f6fa49d](https://github.com/angular/angular/commit/107f6fa49d00ec250b94a8d5f484bae2fc3d71c9) | fix | remove namespaced MathML script elements |
|
||||
| [d0d7f57e08](https://github.com/angular/angular/commit/d0d7f57e0810a24ba16dbb1f2ab9f079a096fa3d) | fix | scope nested CSS rules |
|
||||
### compiler-cli
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e8aa222e7d](https://github.com/angular/angular/commit/e8aa222e7d98d2e54c5604107a929a11cb1ef62d) | fix | correctly resolve symbol for SafePropertyRead in chained optional navigation |
|
||||
| [8d6c925392](https://github.com/angular/angular/commit/8d6c925392c7230fbe6b39abdad05381672631b1) | fix | record class extends clause references in DeferredSymbolTracker |
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [8925a4ee49](https://github.com/angular/angular/commit/8925a4ee491aebaf6a1a74880c73dfb20e0a4ba1) | fix | ensure i18n_util hasOwnProperty checks are safe for property renaming |
|
||||
| [565dbb2fe5](https://github.com/angular/angular/commit/565dbb2fe5406f2a86d43a5b3c6993434b7124a7) | fix | incorrect loop in defer blocks |
|
||||
| [9f8e32616b](https://github.com/angular/angular/commit/9f8e32616b79892785051fab1c3120a770f5f682) | fix | reject prefixed SVG script hosts |
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [9e37a58e14](https://github.com/angular/angular/commit/9e37a58e14bd7955d40255e42e20aa84fcde02f4) | fix | keep radio inputs in sync when values change |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [09bc90003e](https://github.com/angular/angular/commit/09bc90003ed6e9f6c166e885503728af49d1af67) | fix | always decode JSON responses as UTF-8 |
|
||||
### router
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [b65dea4f03](https://github.com/angular/angular/commit/b65dea4f03e5fc01093a718c990c72ae9165c43f) | feat | allow throwing RedirectCommand to trigger redirects |
|
||||
| [435f8b2b8b](https://github.com/angular/angular/commit/435f8b2b8b67277fc69888911251d14b32705828) | fix | limit protocol-relative URL handling to serialization |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="20.3.28"></a>
|
||||
# 20.3.28 (2026-08-13)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [2f96c8020f](https://github.com/angular/angular/commit/2f96c8020f85ccb715a76de4b79a0c680c2c7264) | fix | sanitize host bindings on concrete hosts |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [969133d426](https://github.com/angular/angular/commit/969133d426723336eb04ab839ff30d884ce54dd9) | fix | match header values exactly when deleting |
|
||||
| [29dd26bd71](https://github.com/angular/angular/commit/29dd26bd7115f90ea059a1a5d174203255033e36) | fix | preserve immutability of materialized clones |
|
||||
| [e4c416c20a](https://github.com/angular/angular/commit/e4c416c20a1cb222ce73d29c035452b257380c56) | fix | run root interceptors in the terminal request chain |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.20"></a>
|
||||
# 21.2.20 (2026-08-12)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6afe6fa781](https://github.com/angular/angular/commit/6afe6fa781c2f0931f0aedd729b9884a8fe212ee) | fix | sanitize host bindings on concrete hosts |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [fec5977df4](https://github.com/angular/angular/commit/fec5977df4dda3a10d5ce2923e3e06d86ba11ee7) | fix | match header values exactly when deleting |
|
||||
| [e33d69a71c](https://github.com/angular/angular/commit/e33d69a71c5beb8fe5785b53fd6b37658334e8e0) | fix | preserve immutability of materialized clones |
|
||||
| [caf616670f](https://github.com/angular/angular/commit/caf616670fd20d528aa69e0131cc17d60f0cc27d) | fix | run root interceptors in the terminal request chain |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.2.0-next.1"></a>
|
||||
# 22.2.0-next.1 (2026-08-07)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [cfe2cda110](https://github.com/angular/angular/commit/cfe2cda1100bb84e2ecf954687126c4d30212d07) | fix | initialize hydration triggers after late runtime activation |
|
||||
| [e43eb96341](https://github.com/angular/angular/commit/e43eb96341b692525743a1f3aa1380ce21a9dd02) | fix | warn when style property bindings receive invalid values |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ef4dfead83](https://github.com/angular/angular/commit/ef4dfead83ea8b3843b98a17ac572f2a380b1c46) | fix | avoid aborting completed requests in FetchBackend |
|
||||
| [6a0789dc7f](https://github.com/angular/angular/commit/6a0789dc7f0ac9d1249c4d9f315e83424867169b) | fix | respect content-type charset in fetch backend text decoder |
|
||||
| [bb78286e5e](https://github.com/angular/angular/commit/bb78286e5e729d1e1fcfd2733d88351a38083ef5) | fix | run root interceptors in the terminal request chain |
|
||||
| [280d09b160](https://github.com/angular/angular/commit/280d09b16011e4f53f9e696fd95964943c6dcbfa) | fix | strip RFC 6265 DQUOTE characters and handle URIError in parseCookieValue |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.1.1"></a>
|
||||
# 22.1.1 (2026-08-06)
|
||||
### core
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [aa6d3189c1](https://github.com/angular/angular/commit/aa6d3189c141a9247615ed14d856813ea1b95c8d) | fix | initialize hydration triggers after late runtime activation |
|
||||
| [deecb301c8](https://github.com/angular/angular/commit/deecb301c8aab4c22b80eaa505042311ee938b67) | fix | warn when style property bindings receive invalid values |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [ac3728e79f](https://github.com/angular/angular/commit/ac3728e79fd8fae7a4b38e20d152c5a734a6e3fc) | fix | avoid aborting completed requests in FetchBackend |
|
||||
| [688a0a7118](https://github.com/angular/angular/commit/688a0a7118215bcab10004a3bf2667773fefdf74) | fix | respect content-type charset in fetch backend text decoder |
|
||||
| [a13b968451](https://github.com/angular/angular/commit/a13b9684510dfd04e18f3e1d6686d556c051f34e) | fix | run root interceptors in the terminal request chain |
|
||||
| [32af9b525e](https://github.com/angular/angular/commit/32af9b525efe005998a9cd02aebfcc08e6793cf2) | fix | strip RFC 6265 DQUOTE characters and handle URIError in parseCookieValue |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.2.0-next.0"></a>
|
||||
# 22.2.0-next.0 (2026-07-29)
|
||||
### forms
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [d5e8b1ef7a](https://github.com/angular/angular/commit/d5e8b1ef7a02c84d4fd70a6b4d748ead9ff815bf) | feat | allow permanent hidden fields in signal forms |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="21.2.19"></a>
|
||||
# 21.2.19 (2026-07-29)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [e2660c3dee](https://github.com/angular/angular/commit/e2660c3deeccb86471f9c11b7e65ebcc89d84e9e) | fix | disallow i18n event attributes |
|
||||
| [7b884f585a](https://github.com/angular/angular/commit/7b884f585abf50480c271d234c892c141eb61e2c) | fix | restrict possible event handler check to property names longer than 2 characters |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [948a8d6831](https://github.com/angular/angular/commit/948a8d6831e8920b54663ec79421da95210e0e35) | fix | distinguish repeated transfer cache params |
|
||||
| [9949dccce1](https://github.com/angular/angular/commit/9949dccce164638597496a6b3881043117dfd1df) | fix | enable xsrf for root-provided HttpClient |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [f34a93c946](https://github.com/angular/angular/commit/f34a93c946c017eff526cfb0c8d17f51e97f01e4) | fix | update domino to latest version |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.1.0"></a>
|
||||
# 22.1.0 (2026-07-29)
|
||||
## Deprecations
|
||||
@@ -608,24 +69,6 @@ No user facing changes in this release
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="20.3.27"></a>
|
||||
# 20.3.27 (2026-07-29)
|
||||
### compiler
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [5dbcd0ee16](https://github.com/angular/angular/commit/5dbcd0ee16011369bec00e10e22cbcae6ebabd7a) | fix | disallow i18n event attributes |
|
||||
| [db0d4a1a39](https://github.com/angular/angular/commit/db0d4a1a39db2dc0773f3bc9d7a0e1cfc59fa251) | fix | restrict possible event handler check to property names longer than 2 characters |
|
||||
### http
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [a64e2883e9](https://github.com/angular/angular/commit/a64e2883e9dc4abdac70209129be303de79e5b2b) | fix | distinguish repeated transfer cache params |
|
||||
### platform-server
|
||||
| Commit | Type | Description |
|
||||
| -- | -- | -- |
|
||||
| [6f80cca0b8](https://github.com/angular/angular/commit/6f80cca0b8af23b37f24fb8ee0229b6901d01df2) | fix | update domino to latest version |
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
|
||||
<a name="22.0.8"></a>
|
||||
# 22.0.8 (2026-07-22)
|
||||
### common
|
||||
|
||||
+22
-22
@@ -4,42 +4,42 @@ module(
|
||||
name = "angular",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_pkg", version = "1.3.0")
|
||||
bazel_dep(name = "rules_pkg", version = "1.2.0")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.7.5")
|
||||
bazel_dep(name = "aspect_rules_ts", version = "3.10.1")
|
||||
bazel_dep(name = "aspect_rules_js", version = "3.4.1")
|
||||
bazel_dep(name = "aspect_rules_esbuild", version = "0.27.0")
|
||||
bazel_dep(name = "aspect_rules_ts", version = "3.9.2")
|
||||
bazel_dep(name = "aspect_rules_js", version = "3.2.3")
|
||||
bazel_dep(name = "aspect_rules_esbuild", version = "0.26.0")
|
||||
bazel_dep(name = "aspect_rules_jasmine", version = "2.0.4")
|
||||
bazel_dep(name = "aspect_rules_rollup", version = "2.0.1")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.2")
|
||||
bazel_dep(name = "bazel_lib", version = "3.7.2")
|
||||
bazel_dep(name = "tar.bzl", version = "0.10.8")
|
||||
bazel_dep(name = "yq.bzl", version = "0.4.0")
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.0")
|
||||
bazel_dep(name = "bazel_lib", version = "3.5.0")
|
||||
bazel_dep(name = "tar.bzl", version = "0.10.7")
|
||||
bazel_dep(name = "yq.bzl", version = "0.3.6")
|
||||
bazel_dep(name = "rules_angular")
|
||||
git_override(
|
||||
module_name = "rules_angular",
|
||||
commit = "5fa856469c642490a6e381a05d7f58f60fad1913",
|
||||
commit = "db445dccfe48aad20be6ee3df456df91660ff217",
|
||||
remote = "https://github.com/angular/rules_angular.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "devinfra")
|
||||
git_override(
|
||||
module_name = "devinfra",
|
||||
commit = "4a09f9588769980eacb41fcc76e2c76d048eac42",
|
||||
commit = "daa0b5ecd15668d12163f70d4c9836ada70808c9",
|
||||
remote = "https://github.com/angular/dev-infra.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_sass")
|
||||
git_override(
|
||||
module_name = "rules_sass",
|
||||
commit = "bf9d6564f57b4df94b4915086ad0e7ad49a95f37",
|
||||
commit = "f153cb9242233738c3e6eca6554ae4cb864b0def",
|
||||
remote = "https://github.com/angular/rules_sass.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_browsers")
|
||||
git_override(
|
||||
module_name = "rules_browsers",
|
||||
commit = "c37398d63f5e990d618d02f4c6fb2fefe40f1a79",
|
||||
commit = "3a6bd40f9be945dc51c976a21424c656448f9b09",
|
||||
remote = "https://github.com/angular/rules_browsers.git",
|
||||
)
|
||||
|
||||
@@ -49,15 +49,15 @@ use_repo(yq, "yq_toolchains")
|
||||
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
|
||||
node.toolchain(
|
||||
node_repositories = {
|
||||
"24.21.0-darwin_arm64": ("node-v24.21.0-darwin-arm64.tar.gz", "node-v24.21.0-darwin-arm64", "bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057"),
|
||||
"24.21.0-darwin_amd64": ("node-v24.21.0-darwin-x64.tar.gz", "node-v24.21.0-darwin-x64", "1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097"),
|
||||
"24.21.0-linux_arm64": ("node-v24.21.0-linux-arm64.tar.xz", "node-v24.21.0-linux-arm64", "6ad1325edbdb5649c379b75a237147a666c95d4f9ae8d340fef2d1575d289ad2"),
|
||||
"24.21.0-linux_ppc64le": ("node-v24.21.0-linux-ppc64le.tar.xz", "node-v24.21.0-linux-ppc64le", "1936fd64623a2f98d1fb31b456686d10c30e92639899cfefdefa8835d22adccd"),
|
||||
"24.21.0-linux_s390x": ("node-v24.21.0-linux-s390x.tar.xz", "node-v24.21.0-linux-s390x", "2ef7e2ecbf7a6c2f3d08d106f6b2f279419c6dac89fe91b8cad193af7890082c"),
|
||||
"24.21.0-linux_amd64": ("node-v24.21.0-linux-x64.tar.xz", "node-v24.21.0-linux-x64", "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6"),
|
||||
"24.21.0-windows_amd64": ("node-v24.21.0-win-x64.zip", "node-v24.21.0-win-x64", "158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541"),
|
||||
"22.23.1-darwin_arm64": ("node-v22.23.1-darwin-arm64.tar.gz", "node-v22.23.1-darwin-arm64", "ef28d8fab2c0e4314522d4bb1b7173270aa3937e93b92cb7de79c112ac1fa953"),
|
||||
"22.23.1-darwin_amd64": ("node-v22.23.1-darwin-x64.tar.gz", "node-v22.23.1-darwin-x64", "b8da981b8a0b1241b70249204916da76c63573ddf5814dbd2d1e41069105cb81"),
|
||||
"22.23.1-linux_arm64": ("node-v22.23.1-linux-arm64.tar.xz", "node-v22.23.1-linux-arm64", "0294e8b915ab75f92c7513d2fcb830ae06e10684e6c603e99a87dbf8835389c1"),
|
||||
"22.23.1-linux_ppc64le": ("node-v22.23.1-linux-ppc64le.tar.xz", "node-v22.23.1-linux-ppc64le", "a645bcb7bbc498d09ba9249b5c6414aa6d0a461d8854afc61e32096646ba5e66"),
|
||||
"22.23.1-linux_s390x": ("node-v22.23.1-linux-s390x.tar.xz", "node-v22.23.1-linux-s390x", "f16b5636ba925b462e627c5e0ef47de3255bf4a7b38d56db58e2cf165df19c3c"),
|
||||
"22.23.1-linux_amd64": ("node-v22.23.1-linux-x64.tar.xz", "node-v22.23.1-linux-x64", "9749e988f437343b7fa832c69ded82a312e41a03116d766797ac14f6f9eee578"),
|
||||
"22.23.1-windows_amd64": ("node-v22.23.1-win-x64.zip", "node-v22.23.1-win-x64", "7df0bc9375723f4a86b3aa1b7cc73342423d9677a8df4538aca31a049e309c29"),
|
||||
},
|
||||
node_version = "24.21.0",
|
||||
node_version = "22.23.1",
|
||||
)
|
||||
use_repo(node, "nodejs_toolchains")
|
||||
use_repo(node, "nodejs_darwin_amd64")
|
||||
@@ -71,8 +71,8 @@ use_repo(node, "nodejs_windows_amd64")
|
||||
pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
|
||||
pnpm.pnpm(
|
||||
name = "pnpm",
|
||||
pnpm_version = "11.24.0",
|
||||
pnpm_version_integrity = "sha512-vSfjRel23LC+C3oSKCF7BJqBfiGx81XJDb59xGZxiVqLwebQbCRVRQXqk+oLRfSJon7Bv7yN5qlln8oPFvoAAA==",
|
||||
pnpm_version = "11.17.0",
|
||||
pnpm_version_integrity = "sha512-zKPOozKtJUu4QUX5ZtGfSHlhUhA0b8kseaBH8joNezzKPDeS8AdrofGDHSd++88KkRmzGppg7Kf7PWIx8zHvcg==",
|
||||
)
|
||||
use_repo(pnpm, "pnpm")
|
||||
|
||||
|
||||
Generated
+2938
-278
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,6 @@ APPLICATION_DEPS = [
|
||||
":node_modules/@algolia/client-common",
|
||||
":node_modules/@algolia/requester-browser-xhr",
|
||||
":node_modules/@algolia/requester-node-http",
|
||||
":node_modules/beasties",
|
||||
":node_modules/@lezer/css",
|
||||
":node_modules/@lezer/html",
|
||||
":node_modules/@lezer/lr",
|
||||
|
||||
+26
-27
@@ -1,45 +1,45 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.57.0",
|
||||
"@algolia/client-search": "5.57.0",
|
||||
"@algolia/requester-browser-xhr": "5.57.0",
|
||||
"@algolia/requester-node-http": "5.57.0",
|
||||
"@algolia/client-common": "5.56.0",
|
||||
"@algolia/client-search": "5.56.0",
|
||||
"@algolia/requester-browser-xhr": "5.56.0",
|
||||
"@algolia/requester-node-http": "5.56.0",
|
||||
"@angular/animations": "workspace:*",
|
||||
"@angular/aria": "22.2.0-next.5",
|
||||
"@angular/build": "22.2.0-next.7",
|
||||
"@angular/cdk": "22.2.0-next.5",
|
||||
"@angular/cli": "22.2.0-next.7",
|
||||
"@angular/aria": "22.1.0-next.4",
|
||||
"@angular/build": "22.1.0-next.4",
|
||||
"@angular/cdk": "22.1.0-next.4",
|
||||
"@angular/cli": "22.1.0-next.4",
|
||||
"@angular/common": "workspace:*",
|
||||
"@angular/compiler": "workspace:*",
|
||||
"@angular/compiler-cli": "workspace:*",
|
||||
"@angular/core": "workspace:*",
|
||||
"@angular/docs": "workspace:*",
|
||||
"@angular/forms": "workspace:*",
|
||||
"@angular/material": "22.2.0-next.5",
|
||||
"@angular/material": "22.1.0-next.4",
|
||||
"@angular/platform-browser": "workspace:*",
|
||||
"@angular/platform-server": "workspace:*",
|
||||
"@angular/router": "workspace:*",
|
||||
"@angular/ssr": "22.2.0-next.7",
|
||||
"@angular/ssr": "22.1.0-next.4",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.11.0",
|
||||
"@codemirror/commands": "6.10.4",
|
||||
"@codemirror/lang-angular": "0.1.4",
|
||||
"@codemirror/lang-css": "6.3.1",
|
||||
"@codemirror/lang-html": "6.4.12",
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@codemirror/lang-javascript": "6.2.5",
|
||||
"@codemirror/lang-sass": "6.0.2",
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/search": "6.7.1",
|
||||
"@codemirror/state": "6.7.1",
|
||||
"@codemirror/view": "6.43.9",
|
||||
"@codemirror/view": "6.43.6",
|
||||
"@lezer/common": "1.5.2",
|
||||
"@lezer/css": "1.3.6",
|
||||
"@lezer/css": "1.3.4",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lezer/html": "1.3.13",
|
||||
"@lezer/javascript": "1.5.4",
|
||||
"@lezer/lr": "1.4.10",
|
||||
"@lezer/sass": "1.1.0",
|
||||
"@marijn/find-cluster-break": "1.0.4",
|
||||
"@marijn/find-cluster-break": "1.0.3",
|
||||
"@shikijs/core": "^4.0.0",
|
||||
"@shikijs/engine-javascript": "^4.0.0",
|
||||
"@shikijs/engine-oniguruma": "^4.0.0",
|
||||
@@ -51,15 +51,14 @@
|
||||
"@stackblitz/sdk": "1.11.1",
|
||||
"@types/dom-navigation": "1.0.7",
|
||||
"@types/jasmine": "6.0.0",
|
||||
"@types/jsdom": "30.0.0",
|
||||
"@types/jsdom": "28.0.3",
|
||||
"@types/node": "24.13.3",
|
||||
"@typescript/vfs": "1.6.4",
|
||||
"@webcontainer/api": "1.6.4",
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/xterm": "6.0.0",
|
||||
"algoliasearch": "5.57.0",
|
||||
"algoliasearch": "5.56.0",
|
||||
"angular-split": "20.0.0",
|
||||
"beasties": "0.5.4",
|
||||
"ccount": "^2.0.1",
|
||||
"character-entities-html4": "^2.1.0",
|
||||
"character-entities-legacy": "^3.0.0",
|
||||
@@ -71,22 +70,22 @@
|
||||
"hast-util-to-html": "^9.0.5",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"html-void-elements": "^3.0.0",
|
||||
"jsdom": "29.1.1",
|
||||
"jasmine-core": "6.3.0",
|
||||
"jsdom": "30.0.1",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-coverage": "2.2.1",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine-html-reporter": "2.3.0",
|
||||
"marked": "18.0.11",
|
||||
"mermaid": "11.17.2",
|
||||
"karma-jasmine-html-reporter": "2.2.0",
|
||||
"marked": "18.0.6",
|
||||
"mermaid": "11.16.0",
|
||||
"ngx-progressbar": "14.0.0",
|
||||
"playwright-core": "1.62.1",
|
||||
"preact": "10.29.8",
|
||||
"playwright-core": "1.61.1",
|
||||
"preact": "10.29.7",
|
||||
"preact-render-to-string": "6.7.0",
|
||||
"prettier": "3.9.6",
|
||||
"prettier": "3.9.5",
|
||||
"property-information": "^7.1.0",
|
||||
"rxjs": "7.8.2",
|
||||
"shiki": "4.4.3",
|
||||
"shiki": "4.3.1",
|
||||
"space-separated-tokens": "^2.0.2",
|
||||
"stringify-entities": "^4.0.4",
|
||||
"style-mod": "4.1.3",
|
||||
@@ -99,7 +98,7 @@
|
||||
"devDependencies": {
|
||||
"autoprefixer": "10.5.4",
|
||||
"karma": "~6.4.4",
|
||||
"postcss": "8.5.26",
|
||||
"postcss": "8.5.20",
|
||||
"tailwindcss": "3.4.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {ALL_ITEMS} from '../../src/app/routing/navigation-entries/index.js';
|
||||
import {extractHeadingIds, findDuplicateIds} from '../../shared-docs/pipeline/shared/heading.mjs';
|
||||
import {getIdFromHeading} from '../../shared-docs/pipeline/shared/heading.mjs';
|
||||
import {NavigationItem} from '@angular/docs';
|
||||
import {writeFileSync, readFileSync} from 'fs';
|
||||
import {join, resolve} from 'path';
|
||||
@@ -20,7 +20,7 @@ const contentRoot = resolve(process.cwd(), '../../src/content');
|
||||
* in a JSON file. This file then used by other bazel targets to know which routes are valid.
|
||||
*/
|
||||
|
||||
function extractRoutes(items: NavigationItem[], duplicatesByPage: Map<string, string[]>): string[] {
|
||||
function extractRoutes(items: NavigationItem[]): string[] {
|
||||
const routes: string[] = [];
|
||||
for (const item of items) {
|
||||
if (item.path && !item.path.startsWith('http')) {
|
||||
@@ -29,11 +29,7 @@ function extractRoutes(items: NavigationItem[], duplicatesByPage: Map<string, st
|
||||
const content = readFileSync(join(contentRoot, `${item.contentPath}.md`), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const headings = extractHeadingIds(content);
|
||||
const duplicates = findDuplicateIds(headings);
|
||||
if (duplicates.length > 0) {
|
||||
duplicatesByPage.set(`${item.contentPath}.md`, duplicates);
|
||||
}
|
||||
const headings = extractHeadings(content);
|
||||
routes.push(
|
||||
...headings.map(
|
||||
(heading) => `${item.path}#${heading.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
@@ -42,28 +38,33 @@ function extractRoutes(items: NavigationItem[], duplicatesByPage: Map<string, st
|
||||
}
|
||||
}
|
||||
if (item.children) {
|
||||
routes.push(...extractRoutes(item.children, duplicatesByPage));
|
||||
routes.push(...extractRoutes(item.children));
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
function extractHeadings(content: string): string[] {
|
||||
const headings = content
|
||||
.split('\n')
|
||||
// Top level heading (H1) are used for the page title only
|
||||
// and yes, headings can have leading spaces
|
||||
.filter((line) => line.trim().startsWith('##'))
|
||||
.map((line) => line.replace(/^#+\s*/, '').trim());
|
||||
|
||||
const stepRegex = /<docs-step[^>]*title="([^"]*)"/g;
|
||||
let match;
|
||||
while ((match = stepRegex.exec(content)) !== null) {
|
||||
headings.push(match[1]);
|
||||
}
|
||||
|
||||
return headings.map((heading: string) => getIdFromHeading(heading));
|
||||
}
|
||||
|
||||
function main() {
|
||||
const allRoutes: string[] = [];
|
||||
const duplicatesByPage = new Map<string, string[]>();
|
||||
|
||||
allRoutes.push(...extractRoutes(ALL_ITEMS, duplicatesByPage));
|
||||
|
||||
if (duplicatesByPage.size > 0) {
|
||||
const details = Array.from(duplicatesByPage)
|
||||
.map(([page, ids]) => ` ${page}: ${ids.map((id) => `#${id}`).join(', ')}`)
|
||||
.join('\n');
|
||||
throw new Error(
|
||||
`Headings must produce a unique anchor id within a page, otherwise every link to the ` +
|
||||
`anchor resolves to the first heading that claims it. Give the later heading its own ` +
|
||||
`id with the \`{#custom-id}\` syntax.\n${details}`,
|
||||
);
|
||||
}
|
||||
allRoutes.push(...extractRoutes(ALL_ITEMS));
|
||||
|
||||
const uniqueRoutes = Array.from(new Set(allRoutes.filter((r) => !!r)));
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
@@ -171,7 +171,6 @@
|
||||
flex-direction: column;
|
||||
max-height: 12rem;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#codeTabs
|
||||
animationDuration="0ms"
|
||||
mat-stretch-tabs="false"
|
||||
[selectedIndex]="selectedTabIndex()"
|
||||
(selectedTabChange)="onTabIndexChange($event.index)"
|
||||
>
|
||||
@for (tab of tabs(); track tab) {
|
||||
@@ -22,7 +21,7 @@
|
||||
[class.docs-example-code-toggle__show]="!showCode()"
|
||||
[attr.aria-checked]="showCode()"
|
||||
[attr.aria-label]="showCode() ? 'Hide code' : 'Show code'"
|
||||
(click)="toggleCodeVisibility()"
|
||||
(click)="showCode.set(!showCode())"
|
||||
[matTooltip]="showCode() ? 'Hide code' : 'Show code'"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
|
||||
@@ -166,147 +166,6 @@ describe('ExampleViewer', () => {
|
||||
expect(hiddenLine).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the expand control after switching tabs while expanded', async () => {
|
||||
const collapsible = {
|
||||
name: 'example.ts',
|
||||
sanitizedContent:
|
||||
'<pre><code><div class="line">one</div><div class="line">two</div></code></pre>',
|
||||
visibleLinesRange: '1',
|
||||
};
|
||||
|
||||
componentRef.setInput(
|
||||
'metadata',
|
||||
getMetadata({files: [collapsible, {name: 'example.css', sanitizedContent: ''}]}),
|
||||
);
|
||||
|
||||
await component.renderExample();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component.expandable()).toBeTrue();
|
||||
|
||||
component.toggleExampleVisibility();
|
||||
await fixture.whenStable();
|
||||
|
||||
const tabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
await (await tabGroup.getTabs())[1].select();
|
||||
await fixture.whenStable();
|
||||
await (await tabGroup.getTabs())[0].select();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component.expandable()).toBeTrue();
|
||||
});
|
||||
|
||||
it('hides only the lines outside the visible range, and adds no gap within one range', async () => {
|
||||
componentRef.setInput(
|
||||
'metadata',
|
||||
getMetadata({
|
||||
files: [
|
||||
{
|
||||
name: 'example.ts',
|
||||
sanitizedContent:
|
||||
'<pre><code><div class="line">one</div><div class="line">two</div><div class="line">three</div></code></pre>',
|
||||
visibleLinesRange: '2,3',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await component.renderExample();
|
||||
await fixture.whenStable();
|
||||
|
||||
const lines = fixture.debugElement
|
||||
.queryAll(By.css('.line'))
|
||||
.map((line) => (line.nativeElement as HTMLElement).className);
|
||||
|
||||
expect(lines).toEqual(['line hidden', 'line', 'line']);
|
||||
});
|
||||
|
||||
it('hides the line numbers that pair with the hidden lines', async () => {
|
||||
const numbered = (n: number, text: string) =>
|
||||
`<span class="shiki-ln-number">${n}</span><div class="line">${text}</div>`;
|
||||
|
||||
componentRef.setInput(
|
||||
'metadata',
|
||||
getMetadata({
|
||||
files: [
|
||||
{
|
||||
name: 'example.ts',
|
||||
sanitizedContent: `<pre><code>${numbered(1, 'one')}${numbered(2, 'two')}${numbered(3, 'three')}</code></pre>`,
|
||||
visibleLinesRange: '2,3',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await component.renderExample();
|
||||
await fixture.whenStable();
|
||||
|
||||
const visibleNumbers = fixture.debugElement
|
||||
.queryAll(By.css('.shiki-ln-number'))
|
||||
.filter((el) => !(el.nativeElement as HTMLElement).classList.contains('hidden'))
|
||||
.map((el) => (el.nativeElement as HTMLElement).textContent);
|
||||
|
||||
expect(visibleNumbers).toEqual(['2', '3']);
|
||||
});
|
||||
|
||||
it('restores the selected tab and its collapsed lines after hiding and reshowing the code', async () => {
|
||||
componentRef.setInput(
|
||||
'metadata',
|
||||
getMetadata({
|
||||
files: [
|
||||
{name: 'example.ts', sanitizedContent: ''},
|
||||
{
|
||||
name: 'example.css',
|
||||
sanitizedContent:
|
||||
'<pre><code><div class="line">one</div><div class="line">two</div></code></pre>',
|
||||
visibleLinesRange: '2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await component.renderExample();
|
||||
await fixture.whenStable();
|
||||
|
||||
const tabGroup = await loader.getHarness(MatTabGroupHarness);
|
||||
await (await tabGroup.getTabs())[1].select();
|
||||
await fixture.whenStable();
|
||||
|
||||
component.toggleCodeVisibility();
|
||||
await fixture.whenStable();
|
||||
component.toggleCodeVisibility();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(
|
||||
await (await (await loader.getHarness(MatTabGroupHarness)).getSelectedTab()).getLabel(),
|
||||
).toBe('CSS');
|
||||
const lines = fixture.debugElement
|
||||
.queryAll(By.css('.line'))
|
||||
.map((line) => (line.nativeElement as HTMLElement).className);
|
||||
expect(lines).toEqual(['line hidden', 'line']);
|
||||
});
|
||||
|
||||
it('offers no expand control when the range covers the whole file', async () => {
|
||||
componentRef.setInput(
|
||||
'metadata',
|
||||
getMetadata({
|
||||
files: [
|
||||
{
|
||||
name: 'example.ts',
|
||||
sanitizedContent:
|
||||
'<pre><code><div class="line">one</div><div class="line">two</div></code></pre>',
|
||||
visibleLinesRange: '1,2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await component.renderExample();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component.expandable()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should set exampleComponent when metadata contains path and preview is true', async () => {
|
||||
exampleContentSpy.loadPreview.and.resolveTo(ExampleComponent);
|
||||
componentRef.setInput(
|
||||
|
||||
@@ -69,7 +69,6 @@ export class ExampleViewer {
|
||||
readonly expandable = signal<boolean>(false);
|
||||
readonly expanded = signal<boolean>(false);
|
||||
readonly snippetCode = signal<Snippet | undefined>(undefined);
|
||||
readonly selectedTabIndex = signal<number>(0);
|
||||
readonly showCode = signal<boolean>(true);
|
||||
readonly tabs = computed(() =>
|
||||
this.exampleMetadata()?.files.map((file) => ({
|
||||
@@ -102,7 +101,10 @@ export class ExampleViewer {
|
||||
`example-${this.exampleMetadata()?.id.toString()!}`,
|
||||
);
|
||||
|
||||
this.updateExpandable();
|
||||
const lines = this.getHiddenCodeLines();
|
||||
const lineNumbers = this.getHiddenCodeLineNumbers();
|
||||
|
||||
this.expandable.set(lines.length > 0 || lineNumbers.length > 0);
|
||||
},
|
||||
{injector: this.injector},
|
||||
);
|
||||
@@ -114,15 +116,6 @@ export class ExampleViewer {
|
||||
this.setCodeLinesVisibility();
|
||||
}
|
||||
|
||||
toggleCodeVisibility(): void {
|
||||
const showCode = !this.showCode();
|
||||
this.showCode.set(showCode);
|
||||
|
||||
if (showCode) {
|
||||
afterNextRender(() => this.setCodeLinesVisibility(), {injector: this.injector});
|
||||
}
|
||||
}
|
||||
|
||||
copyLink(): void {
|
||||
// Reconstruct the URL using `origin + pathname` so we drop any pre-existing hash.
|
||||
const fullUrl =
|
||||
@@ -135,25 +128,8 @@ export class ExampleViewer {
|
||||
}
|
||||
|
||||
protected onTabIndexChange(index: number): void {
|
||||
this.selectedTabIndex.set(index);
|
||||
this.snippetCode.set(this.exampleMetadata()?.files[index]);
|
||||
|
||||
afterNextRender(
|
||||
() => {
|
||||
this.setCodeLinesVisibility();
|
||||
this.updateExpandable();
|
||||
},
|
||||
{injector: this.injector},
|
||||
);
|
||||
}
|
||||
|
||||
// A file is expandable when its range hides lines. While the block is expanded nothing is
|
||||
// hidden, so trust the presence of a range instead of the rendered state.
|
||||
private updateExpandable(): void {
|
||||
this.expandable.set(
|
||||
!!this.snippetCode()?.visibleLinesRange &&
|
||||
(this.expanded() || this.getHiddenCodeLines().length > 0),
|
||||
);
|
||||
this.setCodeLinesVisibility();
|
||||
}
|
||||
|
||||
private getFileExtension(name: string): string {
|
||||
@@ -212,13 +188,13 @@ export class ExampleViewer {
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (!linesToDisplay.includes(index + 1)) {
|
||||
line.classList.add(HIDDEN_CLASS_NAME);
|
||||
} else if (!linesToDisplay.includes(index)) {
|
||||
} else if (!linesToDisplay.includes(index - 1)) {
|
||||
appendGapBefore.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [index, lineNumber] of lineNumbers.entries()) {
|
||||
if (!linesToDisplay.includes(index + 1)) {
|
||||
if (!linesToDisplay.includes(index)) {
|
||||
lineNumber.classList.add(HIDDEN_CLASS_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.04145 0.04432l6.56351 8.77603L0 15.95564h1.48651l5.78263-6.24705 4.6722 6.24705h5.05865l-6.9328-9.26967L16.21504.04432h-1.48651l-5.32552 5.75341L5.1001.04432H.04145Zm2.18602 1.09497h2.32396l10.26221 13.72122h-2.32396L2.22747 1.13928Z" fill="#A39FA9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
@@ -2,14 +2,14 @@
|
||||
"name": "@angular/docs",
|
||||
"version": "0.0.0-PLACEHOLDER",
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": "^22.2.0-next",
|
||||
"@angular/common": "^22.2.0-next",
|
||||
"@angular/core": "^22.2.0-next",
|
||||
"@angular/forms": "^22.2.0-next",
|
||||
"@angular/material": "^22.2.0-next",
|
||||
"@angular/platform-browser": "^22.2.0-next",
|
||||
"@angular/router": "^22.2.0-next",
|
||||
"@angular/ssr": "^22.2.0-next",
|
||||
"@angular/cdk": "^22.1.0-next",
|
||||
"@angular/common": "^22.1.0-next",
|
||||
"@angular/core": "^22.1.0-next",
|
||||
"@angular/forms": "^22.1.0-next",
|
||||
"@angular/material": "^22.1.0-next",
|
||||
"@angular/platform-browser": "^22.1.0-next",
|
||||
"@angular/router": "^22.1.0-next",
|
||||
"@angular/ssr": "^22.1.0-next",
|
||||
"algoliasearch": "^5.0.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
@@ -18,7 +18,7 @@
|
||||
"diff": "~9.0.0",
|
||||
"emoji-regex": "~10.6.0",
|
||||
"fflate": "^0.8.2",
|
||||
"jsdom": "~30.0.0",
|
||||
"jsdom": "~29.1.0",
|
||||
"marked": "~18.0.0",
|
||||
"mermaid": "^11.0.0",
|
||||
"shiki": "^4.0.0",
|
||||
|
||||
@@ -66,7 +66,7 @@ function getTagSinceVersion(
|
||||
}
|
||||
|
||||
// In case of deprecated tag we need to separate the version from the deprecation message.
|
||||
const version = tag.comment.match(/^\s*(?:(?:since|from|as of)\s+)?v?(\d+(?:\.\d+)?)/i)?.[1];
|
||||
const version = tag.comment.match(/\d+(\.\d+)?/)?.[0];
|
||||
|
||||
return {version};
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export function getTagSinceVersion<T extends HasJsDocTags>(
|
||||
}
|
||||
|
||||
// In case of deprecated tag we need to separate the version from the deprecation message.
|
||||
const version = tag.comment.match(/^\s*(?:(?:since|from|as of)\s+)?v?(\d+(?:\.\d+)?)/i)?.[1];
|
||||
const version = tag.comment.match(/\d+(\.\d+)?/)?.[0];
|
||||
return {version};
|
||||
}
|
||||
|
||||
|
||||
@@ -34,17 +34,16 @@ export function replaceKeywordFromShikiHtml(
|
||||
shikiHtml: string,
|
||||
replaceWith: string,
|
||||
): string {
|
||||
const keywordElement = `<[^>]*>(?:${keyword})<\\/\\w+>`;
|
||||
|
||||
return (
|
||||
shikiHtml
|
||||
// Linking an identifier causes Shiki to emit the space before it as a separate element.
|
||||
.replace(new RegExp(`(${keywordElement})<[^>]*>\\s+<\\/\\w+>`, 'g'), '$1')
|
||||
// Without a link, Shiki includes the space in the identifier's element instead.
|
||||
.replace(new RegExp(`(${keywordElement}<[^>]*>)\\s+(?=[^<\\s]+<\\/\\w+>)`, 'g'), '$1')
|
||||
// remove the leading space of the element after the keyword
|
||||
.replace(
|
||||
new RegExp(`(<[^>]*>(?:${keyword})<\\/\\w+><[^>]*>)(\\s)(\\w+<\\/\\w+>)`, 'g'),
|
||||
'$1$3',
|
||||
)
|
||||
// Shiki requires the keywords (eg. function,interface) for highlighting signatures
|
||||
// here we are replacing it
|
||||
.replace(new RegExp(keywordElement, 'g'), replaceWith)
|
||||
.replace(new RegExp(`<[^>]*>(?:${keyword})<\\/\\w+>`, 'g'), replaceWith)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
{
|
||||
"moduleName": "@angular/core",
|
||||
"entries": [
|
||||
{
|
||||
"name": "Component",
|
||||
"decoratorType": "class",
|
||||
"entryType": "decorator",
|
||||
"members": [
|
||||
{
|
||||
"name": "selector",
|
||||
"type": "string | undefined",
|
||||
"memberType": "property",
|
||||
"memberTags": ["optional"],
|
||||
"description": "The CSS selector that identifies this component in a template.",
|
||||
"jsdocTags": []
|
||||
}
|
||||
],
|
||||
"description": "Decorator that marks a class as an Angular component.",
|
||||
"jsdocTags": [],
|
||||
"rawComment": ""
|
||||
},
|
||||
{
|
||||
"name": "NgTemplateOutlet",
|
||||
"isAbstract": false,
|
||||
|
||||
@@ -33,7 +33,6 @@ describe('renderable', () => {
|
||||
const symbols = Object.fromEntries([
|
||||
['AfterRenderPhase', 'core'],
|
||||
['afterRender', 'core'],
|
||||
['Component', 'core'],
|
||||
['EmbeddedViewRef', 'core'],
|
||||
['ChangeDetectionStrategy', 'core'],
|
||||
['ChangeDetectorRef', 'core'],
|
||||
@@ -80,13 +79,4 @@ describe('renderable', () => {
|
||||
expect(anchor.getAttribute('href')).toBe(`#${id}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should render decorators without whitespace after the @ symbol', () => {
|
||||
const component = entries.get('Component')!;
|
||||
const html = renderEntry(component);
|
||||
const fragment = JSDOM.fragment(html);
|
||||
const api = fragment.querySelector('.docs-reference-api-section code')!;
|
||||
|
||||
expect(api.textContent).toContain('@Component({');
|
||||
});
|
||||
});
|
||||
|
||||
+2304
-582
File diff suppressed because it is too large
Load Diff
@@ -10,21 +10,21 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^22.2.0-next",
|
||||
"@angular/common": "^22.2.0-next",
|
||||
"@angular/compiler": "^22.2.0-next",
|
||||
"@angular/core": "^22.2.0-next",
|
||||
"@angular/forms": "^22.2.0-next",
|
||||
"@angular/platform-browser": "^22.2.0-next",
|
||||
"@angular/router": "^22.2.0-next",
|
||||
"@angular/animations": "^22.1.0-next",
|
||||
"@angular/common": "^22.1.0-next",
|
||||
"@angular/compiler": "^22.1.0-next",
|
||||
"@angular/core": "^22.1.0-next",
|
||||
"@angular/forms": "^22.1.0-next",
|
||||
"@angular/platform-browser": "^22.1.0-next",
|
||||
"@angular/router": "^22.1.0-next",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^22.2.0-next",
|
||||
"@angular/cli": "^22.2.0-next",
|
||||
"@angular/compiler-cli": "^22.2.0-next",
|
||||
"@angular/build": "^22.1.0-next",
|
||||
"@angular/cli": "^22.1.0-next",
|
||||
"@angular/compiler-cli": "^22.1.0-next",
|
||||
"@types/node": "^20.14.8",
|
||||
"typescript": "~6.0.3"
|
||||
}
|
||||
|
||||
@@ -16,16 +16,20 @@
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": false,
|
||||
"useDefineForClassFields": false,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitOverride": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strictBindCallApply": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,51 +27,3 @@ export function getIdFromHeading(heading: string): string {
|
||||
.replace(/\s|\//g, '-') // replace spaces and slashes with dashes
|
||||
.replace(/[^\p{L}\d\-]/gu, ''); // only keep letters, digits & dashes
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the anchor ID of every heading on a page, in document order.
|
||||
* Covers markdown headings below H1, which is reserved for the page title,
|
||||
* and `<docs-step>` titles, which are rendered as headings too. Lines inside
|
||||
* fenced code blocks are ignored so that samples cannot contribute headings.
|
||||
*/
|
||||
export function extractHeadingIds(content: string): string[] {
|
||||
const headings: string[] = [];
|
||||
let insideCodeBlock = false;
|
||||
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (trimmedLine.startsWith('```')) {
|
||||
insideCodeBlock = !insideCodeBlock;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Headings can have leading spaces.
|
||||
if (!insideCodeBlock && trimmedLine.startsWith('##')) {
|
||||
headings.push(trimmedLine.replace(/^#+\s*/, '').trim());
|
||||
}
|
||||
}
|
||||
|
||||
const stepRegex = /<docs-step[^>]*title="([^"]*)"/g;
|
||||
let match;
|
||||
while ((match = stepRegex.exec(content)) !== null) {
|
||||
headings.push(match[1]);
|
||||
}
|
||||
|
||||
return headings.map((heading) => getIdFromHeading(heading));
|
||||
}
|
||||
|
||||
/** Returns the IDs that appear more than once, each reported once. */
|
||||
export function findDuplicateIds(ids: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const duplicates = new Set<string>();
|
||||
|
||||
for (const id of ids) {
|
||||
if (seen.has(id)) {
|
||||
duplicates.add(id);
|
||||
}
|
||||
seen.add(id);
|
||||
}
|
||||
|
||||
return Array.from(duplicates);
|
||||
}
|
||||
|
||||
@@ -34,15 +34,9 @@ const tokenMatcher = new RegExp(
|
||||
's',
|
||||
);
|
||||
|
||||
const tokenStartMatcher = new RegExp(`\n(?:${alertSeverityLevels.join('|')}): `);
|
||||
|
||||
export const docsAlertExtension: TokenizerAndRendererExtension = {
|
||||
name: 'docs-alert',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
const index = src.match(tokenStartMatcher)?.index;
|
||||
return index === undefined ? undefined : index + 1;
|
||||
},
|
||||
tokenizer(this: TokenizerThis, src: string): DocsAlertToken | undefined {
|
||||
const execMatch = tokenMatcher.exec(src);
|
||||
if (execMatch === null) {
|
||||
|
||||
@@ -27,13 +27,11 @@ interface DocsCalloutToken extends Tokens.Generic {
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
// Capture group 2: all content between the open and close tags
|
||||
const calloutRule =
|
||||
/^<docs-callout((?:[^>"'`]|"[^"]*"|'[^']*'|`[^`]*`)*)>((?:.(?!\/docs-callout))*)<\/docs-callout>/s;
|
||||
const calloutRule = /^<docs-callout([^>]*)>((?:.(?!\/docs-callout))*)<\/docs-callout>/s;
|
||||
|
||||
const titleRule = /title=(['"`])(.*?)\1/; // The 2nd capture matters here
|
||||
const attributeValueRule = /=(['"`])(?:.*?)\1/gs;
|
||||
const isImportantRule = /\bimportant\b/;
|
||||
const isCriticalRule = /\bcritical\b/;
|
||||
const titleRule = /title="([^"]*)"/;
|
||||
const isImportantRule = /important/;
|
||||
const isCriticalRule = /critical/;
|
||||
|
||||
export const docsCalloutExtension = {
|
||||
name: 'docs-callout',
|
||||
@@ -48,12 +46,9 @@ export const docsCalloutExtension = {
|
||||
const attr = match[1].trim();
|
||||
const title = titleRule.exec(attr);
|
||||
|
||||
// Severity is a bare word on the tag, so it must not be matched inside an attribute value.
|
||||
const flags = attr.replace(attributeValueRule, '');
|
||||
|
||||
let severityLevel = CalloutSeverityLevel.HELPFUL;
|
||||
if (isImportantRule.exec(flags)) severityLevel = CalloutSeverityLevel.IMPORTANT;
|
||||
if (isCriticalRule.exec(flags)) severityLevel = CalloutSeverityLevel.CRITICAL;
|
||||
if (isImportantRule.exec(attr)) severityLevel = CalloutSeverityLevel.IMPORTANT;
|
||||
if (isCriticalRule.exec(attr)) severityLevel = CalloutSeverityLevel.CRITICAL;
|
||||
|
||||
const body = match[2].trim();
|
||||
|
||||
@@ -61,7 +56,7 @@ export const docsCalloutExtension = {
|
||||
type: 'docs-callout',
|
||||
raw: match[0],
|
||||
severityLevel: severityLevel,
|
||||
title: title ? title[2] : '',
|
||||
title: title ? title[1] : '',
|
||||
titleTokens: [],
|
||||
body: body ?? '',
|
||||
bodyTokens: [],
|
||||
|
||||
@@ -48,7 +48,7 @@ export const docsCodeBlockExtension = {
|
||||
const token: DocsCodeBlock = {
|
||||
raw: match[0],
|
||||
type: 'docs-code-block',
|
||||
code: match[3],
|
||||
code: deindent(match[3]),
|
||||
language: match[1],
|
||||
header: headerRule.exec(metadataStr)?.[2],
|
||||
highlight: highlightRule.exec(metadataStr)?.[1],
|
||||
@@ -68,3 +68,20 @@ export const docsCodeBlockExtension = {
|
||||
return formatCode(token, (this.parser.renderer as AdevDocsRenderer).context);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes leading indentation from code blocks.
|
||||
*/
|
||||
function deindent(str: string): string {
|
||||
const lines = str.split('\n');
|
||||
let minIndent = Infinity;
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) {
|
||||
minIndent = Math.min(line.match(/^(\s*)/)?.[1].length ?? 0, minIndent);
|
||||
}
|
||||
}
|
||||
if (minIndent === Infinity || minIndent === 0) {
|
||||
return str;
|
||||
}
|
||||
return lines.map((line) => line.slice(minIndent)).join('\n');
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export const docsCodeExtension = {
|
||||
const classes = classRule.exec(attr);
|
||||
const style = preferRule.exec(attr);
|
||||
|
||||
let code = match[2] ?? '';
|
||||
let code = match[2]?.trim() ?? '';
|
||||
if (path && path[1]) {
|
||||
code = loadWorkspaceRelativeFile(path[1]);
|
||||
// Remove ESLint Comments
|
||||
|
||||
@@ -52,7 +52,6 @@ export function formatCode(token: CodeToken, context: RendererContext): string {
|
||||
}
|
||||
|
||||
extractRegions(token);
|
||||
token.code = deindent(token.code).trim();
|
||||
highlightCode(context.highlighter, token, context);
|
||||
|
||||
const containerEl = JSDOM.fragment(`
|
||||
@@ -96,11 +95,6 @@ export function processForApiLinks(fragment: Element, apiEntries: ApiEntries): v
|
||||
});
|
||||
}
|
||||
|
||||
/** Escapes text that is interpolated into an HTML string. */
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/** Build the header element if a header is provided in the token. */
|
||||
function buildHeaderElement(token: CodeToken) {
|
||||
let header = '';
|
||||
@@ -109,7 +103,7 @@ function buildHeaderElement(token: CodeToken) {
|
||||
}
|
||||
|
||||
if (token.header) {
|
||||
header += `<h3>${escapeHtml(token.header)}</h3>`;
|
||||
header += `<h3>${token.header}</h3>`;
|
||||
}
|
||||
|
||||
if (!header) return '';
|
||||
@@ -159,20 +153,3 @@ function applyContainerAttributesAndClasses(el: Element, token: CodeToken) {
|
||||
el.classList.add(...token.classes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes leading indentation from code blocks.
|
||||
*/
|
||||
function deindent(str: string): string {
|
||||
const lines = str.split('\n');
|
||||
let minIndent = Infinity;
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
minIndent = Math.min(line.match(/^(\s*)/)?.[1].length ?? 0, minIndent);
|
||||
}
|
||||
}
|
||||
if (minIndent === Infinity || minIndent === 0) {
|
||||
return str;
|
||||
}
|
||||
return lines.map((line) => line.slice(minIndent)).join('\n');
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@ export function extractRegions(token: CodeToken) {
|
||||
if (!region) {
|
||||
throw new Error(`Cannot find ${token.region} in ${token.path}!`);
|
||||
}
|
||||
token.code = region.lines.join('\n');
|
||||
token.code = region.lines.join('\n').trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import {TokenizerThis, Tokens, RendererThis} from 'marked';
|
||||
import {loadWorkspaceRelativeFile} from '../helpers.mjs';
|
||||
import {AdevDocsRenderer} from '../renderer.mjs';
|
||||
import {getPageTitle} from '../transformations/heading.mjs';
|
||||
|
||||
interface DocsDecorativeHeaderToken extends Tokens.Generic {
|
||||
@@ -76,7 +75,7 @@ function getStandardDecorativeHeader(renderer: RendererThis, token: DocsDecorati
|
||||
<div class="docs-header-content">
|
||||
<docs-breadcrumb></docs-breadcrumb>
|
||||
|
||||
${getPageTitle(token.title, (renderer.parser.renderer as AdevDocsRenderer).context.markdownFilePath)}
|
||||
${getPageTitle(token.title)}
|
||||
|
||||
<p>${token.body}</p>
|
||||
</div>
|
||||
@@ -98,7 +97,7 @@ function getGradientDecorativeHeader(renderer: RendererThis, token: DocsDecorati
|
||||
<div class="docs-decorative-header-container">
|
||||
<div class="docs-decorative-gradient-header">
|
||||
<div class="docs-header-content">
|
||||
${getPageTitle(token.title, (renderer.parser.renderer as AdevDocsRenderer).context.markdownFilePath)}
|
||||
${getPageTitle(token.title)}
|
||||
<p>${token.body}</p>
|
||||
</div>
|
||||
<!-- illustration -->
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Mermaid, MermaidConfig} from 'mermaid';
|
||||
import {join, resolve} from 'path';
|
||||
import {chromium} from 'playwright-core';
|
||||
import {DocsCodeToken} from '../extensions/docs-code/docs-code.mjs';
|
||||
import {chromium} from 'playwright-core';
|
||||
import {Mermaid, MermaidConfig} from 'mermaid';
|
||||
import {resolve, join} from 'path';
|
||||
|
||||
const runfilesDir = process.env['JS_BINARY__RUNFILES']!;
|
||||
// Declare mermarid in the context of this file so that typescript doesn't get upset when we
|
||||
@@ -62,7 +62,7 @@ export async function processMermaidCodeBlock(token: DocsCodeToken) {
|
||||
/** The generated SVG element string for the provided token's code. */
|
||||
let {svg} = await page.evaluate(
|
||||
({diagram, config}) => {
|
||||
mermaid.initialize(config as MermaidConfig);
|
||||
mermaid.initialize(config);
|
||||
|
||||
return mermaid.render('mermaid-generated-diagram', diagram);
|
||||
},
|
||||
|
||||
@@ -24,7 +24,6 @@ import {docsCodeExtension, DocsCodeToken} from './extensions/docs-code/docs-code
|
||||
import {docsCodeMultifileExtension} from './extensions/docs-code/docs-code-multifile.mjs';
|
||||
import {docsTabGroupExtension, docsTabExtension} from './extensions/docs-tabs.mjs';
|
||||
import {docsImageExtension} from './extensions/docs-image.mjs';
|
||||
import {validatePairedTags} from './validate-paired-tags.mjs';
|
||||
|
||||
let markedInstance: typeof marked;
|
||||
const extensions = [
|
||||
@@ -52,13 +51,11 @@ export async function parseMarkdownAsync(
|
||||
markdownContent: string,
|
||||
context: Partial<RendererContext>,
|
||||
): Promise<string> {
|
||||
validatePairedTags(markdownContent, context.markdownFilePath);
|
||||
markedInstance ??= marked.use({extensions, walkTokens, async: true});
|
||||
return markedInstance.parse(markdownContent, {renderer: new AdevDocsRenderer(context)});
|
||||
}
|
||||
|
||||
export function parseMarkdown(markdownContent: string, context: Partial<RendererContext>): string {
|
||||
validatePairedTags(markdownContent, context.markdownFilePath);
|
||||
markedInstance ??= marked.use({extensions, walkTokens});
|
||||
return markedInstance.parse(markdownContent, {renderer: new AdevDocsRenderer(context)}) as string;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,10 @@ export class AdevDocsRenderer extends Renderer {
|
||||
route.startsWith('#') || // Anchor link within the same page
|
||||
route.startsWith('mailto:') || // Should we have a regex to exclude any protocol?
|
||||
route.startsWith('playground') ||
|
||||
// TODO: Extract routes from the CDK as well
|
||||
route.startsWith('api/cdk') ||
|
||||
// TODO: Extract routes from Aria as well
|
||||
route.startsWith('api/aria') ||
|
||||
route.startsWith('tutorials') ||
|
||||
route.startsWith('extended-diagnostics')
|
||||
) {
|
||||
|
||||
@@ -17,7 +17,4 @@ IMPORTANT: Use Important for information that's crucial to comprehending the tex
|
||||
|
||||
HELPFUL: Use Best practice to call out practices that are known to be successful or better than alternatives.
|
||||
|
||||
Some prose that runs straight into the alert with no blank line.
|
||||
TIP: THIS TIP FOLLOWS PROSE ON THE NEXT LINE
|
||||
|
||||
NOTE: THIS NOTE WITHOUT A LINE RETURN
|
||||
|
||||
@@ -35,14 +35,6 @@ describe('markdown to html', () => {
|
||||
expect(noteEl?.textContent?.trim()).toContain(`This is a multiline note`);
|
||||
});
|
||||
|
||||
it(`should handle an alert that follows prose in the same paragraph`, () => {
|
||||
const tipEls = markdownDocument.querySelectorAll(`.docs-alert-tip`);
|
||||
|
||||
expect(tipEls[tipEls.length - 1]?.textContent?.trim()).toContain(
|
||||
`THIS TIP FOLLOWS PROSE ON THE NEXT LINE`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`should handle alerts without a line return`, () => {
|
||||
const noteEl = markdownDocument.querySelector(`.docs-alert-note:last-of-type`);
|
||||
|
||||
|
||||
@@ -25,56 +25,4 @@ describe('markdown to html', () => {
|
||||
markdownDocument.querySelector('#default-marker')!.parentElement?.parentElement;
|
||||
calloutDiv?.classList.contains('docs-callout-helpful');
|
||||
});
|
||||
|
||||
const parse = (markdown: string) => JSDOM.fragment(parseMarkdown(markdown, rendererContext));
|
||||
|
||||
it('reads a title quoted with any of the supported quote characters', () => {
|
||||
for (const quote of ['"', "'", '`']) {
|
||||
expect(
|
||||
parse(`<docs-callout title=${quote}Quoted${quote}>Body</docs-callout>`).querySelector('h3')
|
||||
?.textContent,
|
||||
)
|
||||
.withContext(`quoted with ${quote}`)
|
||||
.toBe('Quoted');
|
||||
}
|
||||
});
|
||||
|
||||
it('reads a title that contains the other quote characters', () => {
|
||||
expect(
|
||||
parse(
|
||||
`<docs-callout title='Illustrating the "pristine" state'>Body</docs-callout>`,
|
||||
).querySelector('h3')?.textContent,
|
||||
).toBe('Illustrating the "pristine" state');
|
||||
expect(
|
||||
parse(`<docs-callout title="It's here">Body</docs-callout>`).querySelector('h3')?.textContent,
|
||||
).toBe("It's here");
|
||||
});
|
||||
|
||||
it('reads a title that contains a closing angle bracket', () => {
|
||||
expect(
|
||||
parse('<docs-callout title="Migrate a > b">Body</docs-callout>').querySelector('h3')
|
||||
?.textContent,
|
||||
).toBe('Migrate a > b');
|
||||
});
|
||||
|
||||
it('takes the severity from the tag, not from the title text', () => {
|
||||
expect(
|
||||
parse('<docs-callout title="Why this is important">Body</docs-callout>').querySelector(
|
||||
'.docs-callout',
|
||||
)?.className,
|
||||
).toBe('docs-callout docs-callout-helpful');
|
||||
expect(
|
||||
parse('<docs-callout title="A critical note">Body</docs-callout>').querySelector(
|
||||
'.docs-callout',
|
||||
)?.className,
|
||||
).toBe('docs-callout docs-callout-helpful');
|
||||
expect(
|
||||
parse('<docs-callout important title="X">Body</docs-callout>').querySelector('.docs-callout')
|
||||
?.className,
|
||||
).toBe('docs-callout docs-callout-important');
|
||||
expect(
|
||||
parse('<docs-callout critical title="X">Body</docs-callout>').querySelector('.docs-callout')
|
||||
?.className,
|
||||
).toBe('docs-callout docs-callout-critical');
|
||||
});
|
||||
});
|
||||
|
||||
+1
-2
@@ -35,8 +35,7 @@ describe('markdown to html', () => {
|
||||
|
||||
it('should deindent code blocks correctly', () => {
|
||||
const codeBlock = markdownDocument.querySelectorAll('code')[1];
|
||||
expect(codeBlock.textContent).not.toContain(` if(foo)`);
|
||||
expect(codeBlock.textContent).toContain(` // bar`);
|
||||
expect(codeBlock.innerHTML).toContain(` // bar`);
|
||||
});
|
||||
|
||||
it('should handle code blocks without language', () => {
|
||||
|
||||
@@ -14,9 +14,3 @@ const form = {
|
||||
</docs-code>
|
||||
|
||||
<docs-code hideDollar code="echo 'hello world'" />
|
||||
|
||||
<docs-code language="typescript">
|
||||
if (foo) {
|
||||
// bar
|
||||
}
|
||||
</docs-code>
|
||||
|
||||
@@ -46,7 +46,6 @@ describe('markdown to html', () => {
|
||||
it('should load header and html code', () => {
|
||||
const codeBlock = markdownDocument.querySelectorAll('code')[3];
|
||||
expect(codeBlock).toBeTruthy();
|
||||
expect(codeBlock?.textContent).not.toContain('docregion');
|
||||
});
|
||||
|
||||
it('should not link property names in object literals', () => {
|
||||
@@ -58,9 +57,4 @@ describe('markdown to html', () => {
|
||||
const codeBlock = markdownDocument.querySelectorAll('.docs-code')[5];
|
||||
expect(codeBlock.getAttribute('hideDollar')).toBe('true');
|
||||
});
|
||||
|
||||
it('should deindent inline code blocks correctly', () => {
|
||||
const codeBlock = markdownDocument.querySelectorAll('.docs-code')[6]?.querySelector('code');
|
||||
expect(codeBlock?.textContent).toMatch(/^ \/\/ bar/m);
|
||||
});
|
||||
});
|
||||
|
||||
-15
@@ -33,19 +33,4 @@ describe('markdown to html', () => {
|
||||
it('passes the header text to the content', () => {
|
||||
expect(markdownDocument.querySelector('p')?.textContent?.trim()).toBe('This is header text');
|
||||
});
|
||||
|
||||
for (const gradientBackground of [false, true]) {
|
||||
it(`links to the source file on GitHub (gradientBackground=${gradientBackground})`, () => {
|
||||
const markdownDocument = JSDOM.fragment(
|
||||
parseMarkdown(
|
||||
`<docs-decorative-header title="Custom Title" gradientBackground="${gradientBackground}"></docs-decorative-header>`,
|
||||
{...rendererContext, markdownFilePath: 'adev/src/content/overview.md'},
|
||||
),
|
||||
);
|
||||
|
||||
expect(markdownDocument.querySelector('.docs-github-links')?.getAttribute('href')).toBe(
|
||||
'https://github.com/angular/angular/edit/main/adev/src/content/overview.md',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
-80
@@ -1,80 +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 {parseMarkdown} from '../../parse.mjs';
|
||||
import {rendererContext, setHighlighter} from '../renderer-context.mjs';
|
||||
|
||||
describe('paired tag validation', () => {
|
||||
beforeAll(async () => {
|
||||
await setHighlighter();
|
||||
});
|
||||
|
||||
const parse = (markdown: string) =>
|
||||
parseMarkdown(markdown, {...rendererContext, markdownFilePath: 'guide/example.md'});
|
||||
|
||||
it('throws when a step is left unclosed', () => {
|
||||
expect(() =>
|
||||
parse(`
|
||||
<docs-workflow>
|
||||
<docs-step title="First">
|
||||
First step.
|
||||
<docs-step title="Second">
|
||||
Second step.
|
||||
</docs-step>
|
||||
</docs-workflow>`),
|
||||
).toThrowError(
|
||||
/Unbalanced <docs-step> in guide\/example\.md: 2 opening tag\(s\) and 1 closing/,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when a workflow is left unclosed', () => {
|
||||
expect(() =>
|
||||
parse(`
|
||||
<docs-workflow>
|
||||
<docs-step title="Only">
|
||||
Only step.
|
||||
</docs-step>`),
|
||||
).toThrowError(/Unbalanced <docs-workflow>/);
|
||||
});
|
||||
|
||||
it('accepts balanced tags', () => {
|
||||
expect(() =>
|
||||
parse(`
|
||||
<docs-workflow>
|
||||
<docs-step title="Only">
|
||||
Only step.
|
||||
</docs-step>
|
||||
</docs-workflow>`),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('ignores tags inside code blocks, inline code and comments', () => {
|
||||
expect(() =>
|
||||
parse(
|
||||
[
|
||||
'```html',
|
||||
'<docs-step title="In a fence">',
|
||||
'```',
|
||||
'',
|
||||
'Steps are written as `<docs-step>` elements.',
|
||||
'',
|
||||
'<!-- <docs-workflow> -->',
|
||||
].join('\n'),
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not confuse docs-card with docs-card-container', () => {
|
||||
expect(() =>
|
||||
parse(`
|
||||
<docs-card-container>
|
||||
<docs-card title="One">Body</docs-card>
|
||||
</docs-card-container>`),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,44 +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
|
||||
*/
|
||||
|
||||
/** Custom elements that are always written as a matching pair of open and close tags. */
|
||||
const PAIRED_TAGS = [
|
||||
'docs-callout',
|
||||
'docs-card',
|
||||
'docs-card-container',
|
||||
'docs-decorative-header',
|
||||
'docs-step',
|
||||
'docs-tab',
|
||||
'docs-tab-group',
|
||||
'docs-workflow',
|
||||
];
|
||||
|
||||
/**
|
||||
* Throws when a paired custom element is left unclosed.
|
||||
*
|
||||
* The tokenizers match up to the next closing tag rather than failing, so an unclosed tag pulls
|
||||
* the content that follows it into the block and leaves the next block unparsed in the output.
|
||||
*/
|
||||
export function validatePairedTags(markdown: string, filePath?: string): void {
|
||||
const content = markdown
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/`[^`\n]*`/g, '');
|
||||
|
||||
for (const tag of PAIRED_TAGS) {
|
||||
const opened = content.match(new RegExp(`<${tag}(?=[\\s>])(?![^>]*/>)`, 'g'))?.length ?? 0;
|
||||
const closed = content.match(new RegExp(`</${tag}>`, 'g'))?.length ?? 0;
|
||||
|
||||
if (opened !== closed) {
|
||||
throw new Error(
|
||||
`Unbalanced <${tag}> in ${filePath || 'markdown'}: ` +
|
||||
`${opened} opening tag(s) and ${closed} closing tag(s).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ const REGION_MATCHERS = {
|
||||
es6: inlineC,
|
||||
html: html,
|
||||
svg: html,
|
||||
xlf: html,
|
||||
css: blockC,
|
||||
conf: inlineHash,
|
||||
yaml: inlineHash,
|
||||
|
||||
@@ -13,7 +13,6 @@ export type FileType =
|
||||
| 'es6'
|
||||
| 'html'
|
||||
| 'svg'
|
||||
| 'xlf'
|
||||
| 'css'
|
||||
| 'conf'
|
||||
| 'yaml'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {extractHeadingIds, findDuplicateIds, getIdFromHeading} from '../heading.mjs';
|
||||
import {getIdFromHeading} from '../heading.mjs';
|
||||
|
||||
describe('getIdFromHeading', () => {
|
||||
it('should generate id from simple text', () => {
|
||||
@@ -29,65 +29,3 @@ describe('getIdFromHeading', () => {
|
||||
expect(getIdFromHeading('Duplicate Heading {#unique-id}')).toBe('unique-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractHeadingIds', () => {
|
||||
it('should collect ids for every heading below the page title', () => {
|
||||
const content = [
|
||||
'# Page title',
|
||||
'## First section',
|
||||
'### A subsection',
|
||||
'#### Deeper still',
|
||||
].join('\n');
|
||||
|
||||
expect(extractHeadingIds(content)).toEqual(['first-section', 'a-subsection', 'deeper-still']);
|
||||
});
|
||||
|
||||
it('should collect ids for headings with leading spaces', () => {
|
||||
expect(extractHeadingIds(' ## Indented heading')).toEqual(['indented-heading']);
|
||||
});
|
||||
|
||||
it('should collect ids for docs-step titles', () => {
|
||||
const content = [
|
||||
'## A section',
|
||||
'<docs-step title="Install the package">',
|
||||
'</docs-step>',
|
||||
].join('\n');
|
||||
|
||||
expect(extractHeadingIds(content)).toEqual(['a-section', 'install-the-package']);
|
||||
});
|
||||
|
||||
it('should use the custom id when a heading declares one', () => {
|
||||
expect(extractHeadingIds('## Before {#migration-before}')).toEqual(['migration-before']);
|
||||
});
|
||||
|
||||
it('should ignore headings inside fenced code blocks', () => {
|
||||
const content = [
|
||||
'## A real heading',
|
||||
'```md',
|
||||
'## Not a heading',
|
||||
'### Also not a heading',
|
||||
'```',
|
||||
'## Another real heading',
|
||||
].join('\n');
|
||||
|
||||
expect(extractHeadingIds(content)).toEqual(['a-real-heading', 'another-real-heading']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findDuplicateIds', () => {
|
||||
it('should return an empty array when every id is unique', () => {
|
||||
expect(findDuplicateIds(['one', 'two', 'three'])).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an id that appears twice', () => {
|
||||
expect(findDuplicateIds(['one', 'two', 'one'])).toEqual(['one']);
|
||||
});
|
||||
|
||||
it('should report an id repeated many times only once', () => {
|
||||
expect(findDuplicateIds(['one', 'one', 'one', 'one'])).toEqual(['one']);
|
||||
});
|
||||
|
||||
it('should return every duplicated id', () => {
|
||||
expect(findDuplicateIds(['before', 'after', 'before', 'after'])).toEqual(['before', 'after']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# See https://docs.github.com/en/get-started/git-basics/ignoring-files for more about ignoring files.
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
|
||||
+2284
-563
File diff suppressed because it is too large
Load Diff
@@ -9,18 +9,18 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^22.2.0-next",
|
||||
"@angular/compiler": "^22.2.0-next",
|
||||
"@angular/core": "^22.2.0-next",
|
||||
"@angular/platform-browser": "^22.2.0-next",
|
||||
"@angular/common": "^22.1.0-next",
|
||||
"@angular/compiler": "^22.1.0-next",
|
||||
"@angular/core": "^22.1.0-next",
|
||||
"@angular/platform-browser": "^22.1.0-next",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^22.2.0-next",
|
||||
"@angular/cli": "^22.2.0-next",
|
||||
"@angular/compiler-cli": "^22.2.0-next",
|
||||
"@angular/build": "^22.1.0-next",
|
||||
"@angular/cli": "^22.1.0-next",
|
||||
"@angular/compiler-cli": "^22.1.0-next",
|
||||
"typescript": "~6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
@@ -21,6 +22,8 @@
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true,
|
||||
"_enabledBlockTypes": ["if", "for"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
load("//adev/shared-docs:defaults.bzl", "ng_project", "ts_project", "zoneless_web_test_suite")
|
||||
load("//adev/shared-docs:defaults.bzl", "ng_project", "ts_project")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
@@ -30,20 +30,3 @@ ng_project(
|
||||
"//adev/shared-docs/utils",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":lib",
|
||||
"//adev/shared-docs/interfaces",
|
||||
],
|
||||
)
|
||||
|
||||
zoneless_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
active-label-text-color: transparent,
|
||||
label-text-font: 'InterTight, sans-serif',
|
||||
label-text-tracking: -0.00875rem,
|
||||
label-text-size: 0.875rem,
|
||||
label-text-size: 0.8125rem,
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
:not(.docs-card-grid) > .docs-card {
|
||||
:not(.docs-card-container) .docs-card {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ $code-font-size: 0.875rem;
|
||||
width: 100%;
|
||||
|
||||
a:not(.docs-anchor) > & {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 0.3rem;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"lib": ["ES2022", "dom"],
|
||||
@@ -17,6 +18,7 @@
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
load("//adev/shared-docs:defaults.bzl", "ts_project", "zoneless_web_test_suite")
|
||||
load("//adev/shared-docs:defaults.bzl", "ts_project")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
@@ -21,20 +21,3 @@ ts_project(
|
||||
"//adev/shared-docs/providers",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":utils",
|
||||
"//adev/shared-docs/interfaces",
|
||||
],
|
||||
)
|
||||
|
||||
zoneless_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
|
||||
@@ -1,54 +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 {NavigationItem} from '../interfaces/index';
|
||||
import {isExternalLink, mapNavigationItemsToRoutes} from './navigation.utils';
|
||||
|
||||
describe('navigation.utils', () => {
|
||||
describe('isExternalLink', () => {
|
||||
it('should return true for http and https links', () => {
|
||||
expect(isExternalLink('http://example.com')).toBeTrue();
|
||||
expect(isExternalLink('https://example.com')).toBeTrue();
|
||||
expect(isExternalLink('https://github.com/angular/angularfire#readme')).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return false for internal relative or absolute paths', () => {
|
||||
expect(isExternalLink('guide/signals')).toBeFalse();
|
||||
expect(isExternalLink('/guide/signals')).toBeFalse();
|
||||
expect(isExternalLink('overview')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapNavigationItemsToRoutes', () => {
|
||||
it('should map internal navigation items to routes and ignore external links', () => {
|
||||
const items: NavigationItem[] = [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'overview',
|
||||
},
|
||||
{
|
||||
label: 'AngularFire',
|
||||
path: 'https://github.com/angular/angularfire#readme',
|
||||
},
|
||||
{
|
||||
label: 'Google Maps',
|
||||
path: 'https://github.com/angular/components/tree/main/src/google-maps#readme',
|
||||
},
|
||||
{
|
||||
label: 'Guide',
|
||||
path: 'guide/components',
|
||||
},
|
||||
];
|
||||
|
||||
const routes = mapNavigationItemsToRoutes(items, {});
|
||||
|
||||
expect(routes.length).toBe(2);
|
||||
expect(routes.map((r) => r.path)).toEqual(['overview', 'guide/components']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -96,10 +96,7 @@ export const mapNavigationItemsToRoutes = (
|
||||
additionalRouteProperties: Partial<Route>,
|
||||
): Route[] =>
|
||||
navigationItems
|
||||
.filter(
|
||||
(route): route is NavigationItem & {path: string} =>
|
||||
Boolean(route.path) && !isExternalLink(route.path!),
|
||||
)
|
||||
.filter((route): route is NavigationItem & {path: string} => Boolean(route.path))
|
||||
.map((navigationItem) => {
|
||||
const route = {
|
||||
path: navigationItem.path,
|
||||
|
||||
@@ -22,7 +22,6 @@ import {Footer} from './core/layout/footer/footer.component';
|
||||
import {Navigation} from './core/layout/navigation/navigation.component';
|
||||
import {ProgressBarComponent} from './core/layout/progress-bar/progress-bar.component';
|
||||
import {SecondaryNavigation} from './core/layout/secondary-navigation/secondary-navigation.component';
|
||||
import {ANGULAR_DEV} from './core/constants/links';
|
||||
import {HeaderService} from './core/services/header.service';
|
||||
|
||||
@Component({
|
||||
@@ -102,7 +101,7 @@ export class AppComponent {
|
||||
// In debug this shortcut allows us to open the same page on adev
|
||||
// Helpful to compare differences
|
||||
event.preventDefault();
|
||||
window.open(`${ANGULAR_DEV}/${location.pathname}`, '_blank');
|
||||
window.open(`https://angular.dev/${location.pathname}`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {appConfig} from './app.config';
|
||||
const serverConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideServerRendering(
|
||||
{maxResponseBodySize: 4 * 1024 * 1024},
|
||||
{maxResponseBodySize: 2 * 1024 * 1024},
|
||||
withRoutes([{path: '**', renderMode: RenderMode.Prerender}]),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const ANGULAR_DEV = 'https://angular.dev';
|
||||
|
||||
export const ANGULAR_LINKS = {
|
||||
GITHUB: 'https://github.com/angular/angular',
|
||||
X: 'https://x.com/angular',
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
border-radius: 0.25rem;
|
||||
z-index: var(--z-index-mini-menu);
|
||||
box-shadow: 10px 4px 40px 0 rgba(0, 0, 0, 0.075);
|
||||
max-height: 70dvh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
// Animation for mini-menu appearance
|
||||
animation: menuFadeIn 0.2s ease-out forwards;
|
||||
@@ -110,6 +107,8 @@
|
||||
}
|
||||
|
||||
.adev-version-picker {
|
||||
overflow-y: auto;
|
||||
max-height: 70dvh;
|
||||
margin-inline-start: 10px;
|
||||
|
||||
li {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {inject, Service} from '@angular/core';
|
||||
|
||||
import {ANGULAR_DEV} from '../constants/links';
|
||||
const ANGULAR_DEV = 'https://angular.dev';
|
||||
|
||||
/**
|
||||
* Information about the deployment of this application.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
|
||||
import {PLATFORM_ID, Service, computed, inject, signal} from '@angular/core';
|
||||
import {PLATFORM_ID, Service, inject, signal} from '@angular/core';
|
||||
import {LOCAL_STORAGE} from '@angular/docs';
|
||||
|
||||
// Keep these constants in sync with the code in index.html
|
||||
@@ -26,21 +26,12 @@ export class ThemeManager {
|
||||
private readonly platformId = inject(PLATFORM_ID);
|
||||
|
||||
readonly theme = signal<Theme | null>(this.getThemeFromLocalStorageValue());
|
||||
private readonly osScheme = signal<'dark' | 'light'>('light');
|
||||
readonly resolvedTheme = computed<'dark' | 'light'>(() => {
|
||||
const theme = this.theme();
|
||||
if (theme === null) {
|
||||
return 'light';
|
||||
}
|
||||
return theme === 'auto' ? this.osScheme() : theme;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.osScheme.set(preferredScheme());
|
||||
this.loadThemePreference();
|
||||
this.watchPreferredColorScheme();
|
||||
}
|
||||
@@ -48,7 +39,7 @@ export class ThemeManager {
|
||||
setTheme(theme: Theme): void {
|
||||
this.theme.set(theme);
|
||||
this.setThemeInLocalStorage();
|
||||
this.setThemeBodyClasses(this.resolvedTheme());
|
||||
this.setThemeBodyClasses(theme === 'auto' ? preferredScheme() : theme);
|
||||
}
|
||||
|
||||
// 1. Read theme preferences stored in localStorage
|
||||
@@ -58,7 +49,7 @@ export class ThemeManager {
|
||||
const useTheme = savedUserPreference ?? 'auto';
|
||||
|
||||
this.theme.set(useTheme);
|
||||
this.setThemeBodyClasses(this.resolvedTheme());
|
||||
this.setThemeBodyClasses(useTheme === 'auto' ? preferredScheme() : useTheme);
|
||||
}
|
||||
|
||||
// Set theme classes on the body element
|
||||
@@ -86,11 +77,8 @@ export class ThemeManager {
|
||||
|
||||
private watchPreferredColorScheme() {
|
||||
window.matchMedia(PREFERS_COLOR_SCHEME_DARK).addEventListener('change', (event) => {
|
||||
this.osScheme.set(event.matches ? 'dark' : 'light');
|
||||
if (this.theme() !== 'auto') {
|
||||
return;
|
||||
}
|
||||
this.setThemeBodyClasses(this.resolvedTheme());
|
||||
const preferredScheme = event.matches ? 'dark' : 'light';
|
||||
this.setThemeBodyClasses(preferredScheme);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {httpResource} from '@angular/common/http';
|
||||
import {DOCUMENT, Service, VERSION, computed, inject} from '@angular/core';
|
||||
|
||||
import versionJson from '../../../assets/others/versions.json';
|
||||
import {ANGULAR_DEV} from '../constants/links';
|
||||
|
||||
export interface Version {
|
||||
displayName: string;
|
||||
@@ -61,7 +60,7 @@ export class VersionManager {
|
||||
// which is the most up-to-date anyway.
|
||||
remoteVersions = httpResource(
|
||||
() => ({
|
||||
url: `${ANGULAR_DEV}/assets/others/versions.json`,
|
||||
url: 'https://angular.dev/assets/others/versions.json',
|
||||
transferCache: false,
|
||||
cache: 'no-cache',
|
||||
}),
|
||||
|
||||
@@ -13,11 +13,9 @@ import {
|
||||
ElementRef,
|
||||
EnvironmentInjector,
|
||||
afterRenderEffect,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
linkedSignal,
|
||||
signal,
|
||||
untracked,
|
||||
viewChild,
|
||||
@@ -25,13 +23,12 @@ import {
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {MatTab, MatTabGroup, MatTabLabel} from '@angular/material/tabs';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {from, switchMap} from 'rxjs';
|
||||
import {debounceTime, from, map, switchMap} from 'rxjs';
|
||||
|
||||
import {TerminalType} from '../terminal/terminal-handler.service';
|
||||
|
||||
import {CdkMenu, CdkMenuItem, CdkMenuTrigger} from '@angular/cdk/menu';
|
||||
import {IconComponent} from '@angular/docs';
|
||||
import {ANGULAR_DEV} from '../../core/constants/links';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {DownloadManager} from '../download-manager.service';
|
||||
import {LoadingStep} from '../enums/loading-steps';
|
||||
@@ -47,6 +44,8 @@ export const REQUIRED_FILES = new Set([
|
||||
'src/app/app.component.ts',
|
||||
]);
|
||||
|
||||
const ANGULAR_DEV = 'https://angular.dev';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-tutorial-code-editor',
|
||||
templateUrl: './code-editor.component.html',
|
||||
@@ -83,22 +82,25 @@ export class CodeEditor {
|
||||
private readonly location = inject(Location);
|
||||
private readonly environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
readonly TerminalType = TerminalType;
|
||||
|
||||
protected readonly errors = computed(() =>
|
||||
this.nodeRuntimeState.loadingStep() !== LoadingStep.READY
|
||||
? []
|
||||
: this.diagnosticsState
|
||||
.diagnostics()
|
||||
.filter((item) => item.severity === 'error')
|
||||
.sort((a, b) =>
|
||||
a.lineNumber != b.lineNumber
|
||||
? a.lineNumber - b.lineNumber
|
||||
: a.characterPosition - b.characterPosition,
|
||||
),
|
||||
private readonly errors$ = this.diagnosticsState.diagnostics$.pipe(
|
||||
// Display errors one second after code update
|
||||
debounceTime(1000),
|
||||
map((diagnosticsItem) =>
|
||||
diagnosticsItem
|
||||
.filter((item) => item.severity === 'error')
|
||||
.sort((a, b) =>
|
||||
a.lineNumber != b.lineNumber
|
||||
? a.lineNumber - b.lineNumber
|
||||
: a.characterPosition - b.characterPosition,
|
||||
),
|
||||
),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
protected readonly displayErrorsBox = linkedSignal(() => this.errors().length > 0);
|
||||
readonly TerminalType = TerminalType;
|
||||
|
||||
protected readonly displayErrorsBox = signal<boolean>(false);
|
||||
protected readonly errors = signal<DiagnosticWithLocation[]>([]);
|
||||
protected readonly files = this.codeMirrorEditor.openFiles;
|
||||
protected readonly isCreatingFile = signal<boolean>(false);
|
||||
protected readonly isRenamingFile = signal<boolean>(false);
|
||||
@@ -119,6 +121,7 @@ export class CodeEditor {
|
||||
|
||||
untracked(() => {
|
||||
this.codeMirrorEditor.init(parent);
|
||||
this.listenToDiagnosticsChange();
|
||||
|
||||
this.listenToTabChange();
|
||||
this.setSelectedTabOnTutorialChange();
|
||||
@@ -306,6 +309,16 @@ export class CodeEditor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private listenToDiagnosticsChange(): void {
|
||||
this.errors$.subscribe((diagnostics) => {
|
||||
if (this.nodeRuntimeState.loadingStep() !== LoadingStep.READY) {
|
||||
return;
|
||||
}
|
||||
this.errors.set(diagnostics);
|
||||
this.displayErrorsBox.set(diagnostics.length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
private setSelectedTabOnTutorialChange() {
|
||||
// Using `from` to prevent injecting the embedded tutorial manager once the
|
||||
// injector is destroyed (this may happen in unit tests when the test ends
|
||||
|
||||
@@ -6,23 +6,13 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {WritableSignal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {DiagnosticWithLocation, DiagnosticsState} from './diagnostics-state.service';
|
||||
import {DiagnosticsState} from './diagnostics-state.service';
|
||||
|
||||
describe('DiagnosticsState', () => {
|
||||
let service: DiagnosticsState;
|
||||
|
||||
const diagnostic = (message: string): DiagnosticWithLocation => ({
|
||||
from: 0,
|
||||
to: 1,
|
||||
severity: 'error',
|
||||
message,
|
||||
lineNumber: 1,
|
||||
characterPosition: 0,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(DiagnosticsState);
|
||||
});
|
||||
@@ -30,27 +20,4 @@ describe('DiagnosticsState', () => {
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should start with no diagnostics', () => {
|
||||
expect(service.diagnostics()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should expose the diagnostics it is given', () => {
|
||||
const diagnostics = [diagnostic('first'), diagnostic('second')];
|
||||
|
||||
service.setDiagnostics(diagnostics);
|
||||
|
||||
expect(service.diagnostics()).toEqual(diagnostics);
|
||||
});
|
||||
|
||||
it('should replace the previous diagnostics', () => {
|
||||
service.setDiagnostics([diagnostic('first')]);
|
||||
service.setDiagnostics([]);
|
||||
|
||||
expect(service.diagnostics()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not expose a way to write to the diagnostics signal', () => {
|
||||
expect((service.diagnostics as Partial<WritableSignal<unknown>>).set).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Service, signal} from '@angular/core';
|
||||
import {Service} from '@angular/core';
|
||||
import {Diagnostic} from '@codemirror/lint';
|
||||
import {BehaviorSubject, distinctUntilChanged} from 'rxjs';
|
||||
|
||||
export interface DiagnosticWithLocation extends Diagnostic {
|
||||
lineNumber: number;
|
||||
@@ -16,11 +17,12 @@ export interface DiagnosticWithLocation extends Diagnostic {
|
||||
|
||||
@Service()
|
||||
export class DiagnosticsState {
|
||||
private readonly _diagnostics = signal<DiagnosticWithLocation[]>([]);
|
||||
private readonly _diagnostics$ = new BehaviorSubject<DiagnosticWithLocation[]>([]);
|
||||
|
||||
readonly diagnostics = this._diagnostics.asReadonly();
|
||||
// TODO: use signals when zoneless will be turned off
|
||||
diagnostics$ = this._diagnostics$.asObservable().pipe(distinctUntilChanged());
|
||||
|
||||
setDiagnostics(diagnostics: DiagnosticWithLocation[]): void {
|
||||
this._diagnostics.set(diagnostics);
|
||||
this._diagnostics$.next(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,10 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
import {IconComponent, TutorialType} from '@angular/docs';
|
||||
import {MatTab, MatTabGroup, MatTabLabel} from '@angular/material/tabs';
|
||||
import {map} from 'rxjs';
|
||||
|
||||
import {MAX_RECOMMENDED_WEBCONTAINERS_INSTANCES} from './alert-manager.service';
|
||||
|
||||
@@ -91,9 +93,10 @@ export class EmbeddedEditor {
|
||||
!this.nodeRuntimeState.isResetting(),
|
||||
);
|
||||
|
||||
protected readonly errorsCount = computed(
|
||||
() => this.diagnosticsState.diagnostics().filter((item) => item.severity === 'error').length,
|
||||
private readonly errorsCount$ = this.diagnosticsState.diagnostics$.pipe(
|
||||
map((diagnosticsItem) => diagnosticsItem.filter((item) => item.severity === 'error').length),
|
||||
);
|
||||
protected readonly errorsCount = toSignal(this.errorsCount$, {initialValue: 0});
|
||||
|
||||
constructor() {
|
||||
if (!isPlatformBrowser(this.platformId)) {
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('CSS Value Parser Utils', () => {
|
||||
expect(output).toEqual('translate(42px)');
|
||||
});
|
||||
|
||||
it('should stringify a transform value with a function with multiple parameters', () => {
|
||||
it('should stringify a transform value with a function with multiple paramters', () => {
|
||||
const output = stringifyParsedValue({
|
||||
type: 'transform',
|
||||
values: new Map([
|
||||
|
||||
@@ -34,7 +34,7 @@ export class CodeBlock {
|
||||
.codeToHtml(this.code(), {
|
||||
cssVariablePrefix: '--shiki-',
|
||||
lang: this.language(),
|
||||
theme: this.theme.resolvedTheme() === 'dark' ? 'github-dark' : 'github-light',
|
||||
theme: this.theme.theme() === 'light' ? 'github-light' : 'github-dark',
|
||||
})
|
||||
.then((hightlightedHtml) => {
|
||||
return this.sanitizer.bypassSecurityTrustHtml(hightlightedHtml);
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
/*!
|
||||
* @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 {AnimationDefinition, Styles} from '../../animation';
|
||||
import {AnimationRule} from '../../animation/types';
|
||||
|
||||
/**
|
||||
* CONSTANTS
|
||||
*/
|
||||
|
||||
// Represents percentage of the total.
|
||||
// Avoid using large waves (the total shouldn't be too big as well).
|
||||
// Check meteorShower function for more details.
|
||||
const FIRST_WAVE_METEORS = 0.05;
|
||||
const SECOND_WAVE_METEORS = 0.15;
|
||||
const THIRD_WAVE_METEORS = 0.25;
|
||||
|
||||
// Use to increase or decrease the animation duration (i.e. a fine tuning parameter).
|
||||
// Employed by `timeframe()` and `at()`.
|
||||
const TIMING_MULTIPLIER = 1.55;
|
||||
|
||||
export const ANIM_TIMESTEP = 10; // In milliseconds
|
||||
|
||||
/**
|
||||
* SELECTORS
|
||||
*/
|
||||
|
||||
const BANNERS_LAYER_ID = 'banners';
|
||||
const ADEV_BANNER = `${BANNERS_LAYER_ID} >> .adev-banner`;
|
||||
const LEARN_ANGULAR_BTN = `${BANNERS_LAYER_ID} >> .learn-angular`;
|
||||
|
||||
const LOGO_LAYER_ID = 'logo';
|
||||
const LOGO = `${LOGO_LAYER_ID} >> .logo`;
|
||||
const SHIELD = `${LOGO_LAYER_ID} >> .shield`;
|
||||
const SHIELD_MIDDLE = `${LOGO_LAYER_ID} >> .shield-middle`;
|
||||
const SHIELD_BOTTOM_A_ARC = `${LOGO_LAYER_ID} >> .shield-bottom-a-arc`;
|
||||
const SHIELD_BOTTOM_EXTENSION = `${LOGO_LAYER_ID} >> .shield-bottom-extension`;
|
||||
const CAPITAL_A_LETTER = `${LOGO_LAYER_ID} >> .capt-a-letter`;
|
||||
const N_LETTER = `${LOGO_LAYER_ID} >> .n-letter`;
|
||||
const G_LETTER = `${LOGO_LAYER_ID} >> .g-letter`;
|
||||
const U_LETTER = `${LOGO_LAYER_ID} >> .u-letter`;
|
||||
const L_LETTER = `${LOGO_LAYER_ID} >> .l-letter`;
|
||||
const A_LETTER = `${LOGO_LAYER_ID} >> .a-letter`;
|
||||
const R_LETTER = `${LOGO_LAYER_ID} >> .r-letter`;
|
||||
|
||||
const UWU_LAYER_ID = 'uwu';
|
||||
|
||||
const WORKS_AT_ANY_SCALE_LAYER_ID = 'works-at-any-scale';
|
||||
|
||||
const METEOR_FIELD_LAYER_ID = 'meteor-field';
|
||||
const METEOR_FIELD = `${METEOR_FIELD_LAYER_ID} >> .field`;
|
||||
const METEORS = `${METEOR_FIELD_LAYER_ID} >> .meteor`;
|
||||
const METEOR_ID = (id: number) => `${METEOR_FIELD_LAYER_ID} >> .mt-${id}`;
|
||||
|
||||
const LOVED_BY_MILLIONS_LAYER_ID = 'loved-by-millions';
|
||||
|
||||
const BUILD_FOR_EVERYONE_LAYER_ID = 'build-for-everyone';
|
||||
const BUILD_FOR_EVERYONE_TITLE = `${BUILD_FOR_EVERYONE_LAYER_ID} >> .title`;
|
||||
|
||||
/**
|
||||
* ANIMATION/HELPER FUNCTIONS
|
||||
*/
|
||||
|
||||
// Timing functions (they employ `TIMING_MULTIPLIER`) – Use when setting the time of a rule.
|
||||
const at = (at: number): number => at * TIMING_MULTIPLIER;
|
||||
const timeframe = (from: number, to: number): [number, number] => [
|
||||
from * TIMING_MULTIPLIER,
|
||||
to * TIMING_MULTIPLIER,
|
||||
];
|
||||
|
||||
/** Duration: 1 second */
|
||||
function hideLetter(selector: string, startTime: number): AnimationRule<Styles> {
|
||||
return {
|
||||
selector,
|
||||
timeframe: timeframe(startTime, startTime + 1),
|
||||
from: {
|
||||
opacity: '1',
|
||||
},
|
||||
to: {
|
||||
opacity: '0',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Duration: 1 to 2 seconds */
|
||||
function showMeteor(selector: string, startTime: number): AnimationRule<Styles> {
|
||||
const randomizedStartTime = startTime + Math.random(); // Up to +1 second (excl.)
|
||||
return {
|
||||
selector,
|
||||
timeframe: timeframe(randomizedStartTime, randomizedStartTime + 1),
|
||||
from: {
|
||||
opacity: '0',
|
||||
transform: 'translate(200%, 200%) scale(0.3)',
|
||||
},
|
||||
to: {
|
||||
opacity: '1',
|
||||
transform: 'translate(0, 0) scale(1)',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Duration: 1 to 2 seconds */
|
||||
function meteorShower(
|
||||
startTime: number,
|
||||
size: number,
|
||||
total: number,
|
||||
inUse: Set<number>,
|
||||
): AnimationDefinition {
|
||||
const animations: AnimationRule<Styles>[] = [];
|
||||
|
||||
while (animations.length < size) {
|
||||
// We pick a random meteor ID.
|
||||
// If `inUse` is nearly full relative to `total`,
|
||||
// we might run into a excessive amount of iterations
|
||||
// until we fill `animation`. This is why we should keep
|
||||
// the wave sizes (and their total) relatively small.
|
||||
const id = Math.round(Math.random() * (total - 1) + 1);
|
||||
|
||||
if (!inUse.has(id)) {
|
||||
animations.push(showMeteor(METEOR_ID(id), startTime));
|
||||
inUse.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
/**
|
||||
* DEFINITION
|
||||
*/
|
||||
|
||||
/** Generate the animation definition for the home page animation. */
|
||||
export function generateHomeAnimationDefinition(
|
||||
isUwu: boolean,
|
||||
meteorCount: number,
|
||||
): AnimationDefinition {
|
||||
// Banners and buttons layer
|
||||
// *************************
|
||||
const bannersLayerAnim: AnimationDefinition = [
|
||||
{
|
||||
selector: ADEV_BANNER,
|
||||
timeframe: timeframe(2, 3),
|
||||
from: {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
to: {
|
||||
transform: 'translateY(-200px)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: LEARN_ANGULAR_BTN,
|
||||
timeframe: timeframe(2.5, 3.5),
|
||||
from: {
|
||||
opacity: '1',
|
||||
},
|
||||
to: {
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: LEARN_ANGULAR_BTN,
|
||||
at: at(4),
|
||||
styles: {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Logo layer animation
|
||||
// ********************
|
||||
const logoLayerAnim: AnimationDefinition = [
|
||||
{
|
||||
selector: LOGO,
|
||||
timeframe: timeframe(0, 5),
|
||||
from: {
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
to: {
|
||||
transform: 'translateX(467px)', // Value based on the 1280x400 SVG view box
|
||||
},
|
||||
},
|
||||
hideLetter(R_LETTER, 1),
|
||||
hideLetter(A_LETTER, 1.5),
|
||||
hideLetter(L_LETTER, 2),
|
||||
hideLetter(U_LETTER, 2.5),
|
||||
hideLetter(G_LETTER, 3),
|
||||
hideLetter(N_LETTER, 3.5),
|
||||
// Make sure that the last letter disappears at the end of layer transition,
|
||||
// i.e. 4 + 1 = 5th second end time
|
||||
hideLetter(CAPITAL_A_LETTER, 4),
|
||||
{
|
||||
selector: SHIELD_MIDDLE,
|
||||
timeframe: timeframe(5.5, 5.6),
|
||||
from: {
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(0)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: SHIELD_BOTTOM_A_ARC,
|
||||
timeframe: timeframe(5.5, 5.6),
|
||||
from: {
|
||||
transform: 'scaleY(1)',
|
||||
},
|
||||
to: {
|
||||
transform: 'scaleY(0)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: SHIELD_BOTTOM_EXTENSION,
|
||||
timeframe: timeframe(5.5, 5.6),
|
||||
from: {
|
||||
transform: 'scale(0)',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: SHIELD,
|
||||
timeframe: timeframe(5.5, 10),
|
||||
from: {
|
||||
transform: 'scale(1) rotate(0deg)',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(50) rotate(-360deg)',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// "UwU logo" layer animation
|
||||
// **************************
|
||||
const uwuLayerAnimation: AnimationDefinition = [
|
||||
{
|
||||
selector: UWU_LAYER_ID,
|
||||
timeframe: timeframe(0, 5.5),
|
||||
from: {
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(0)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: UWU_LAYER_ID,
|
||||
timeframe: timeframe(4, 5.5),
|
||||
from: {
|
||||
opacity: '1',
|
||||
},
|
||||
to: {
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// "Works at any scale" layer animation
|
||||
// ************************************
|
||||
const waasLayerAnim: AnimationDefinition = [
|
||||
{
|
||||
selector: WORKS_AT_ANY_SCALE_LAYER_ID,
|
||||
timeframe: timeframe(5.7, 8), // Make sure it appears after SHIELD_MIDDLE disappears.
|
||||
from: {
|
||||
transform: 'scale(0.1)',
|
||||
opacity: '0',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(1)',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: WORKS_AT_ANY_SCALE_LAYER_ID,
|
||||
timeframe: timeframe(11, 12.5),
|
||||
from: {
|
||||
transform: 'scale(1)',
|
||||
opacity: '1',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(1.5)',
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Meteor field layer animation
|
||||
// ****************************
|
||||
const firstWaveSize = meteorCount * FIRST_WAVE_METEORS;
|
||||
const secondWaveSize = meteorCount * SECOND_WAVE_METEORS;
|
||||
const thirdWaveSize = meteorCount * THIRD_WAVE_METEORS;
|
||||
|
||||
const meteorsInUse = new Set<number>();
|
||||
const firstWave = meteorShower(8, firstWaveSize, meteorCount, meteorsInUse);
|
||||
const secondWave = meteorShower(10, secondWaveSize, meteorCount, meteorsInUse);
|
||||
const thirdWave = meteorShower(12, thirdWaveSize, meteorCount, meteorsInUse);
|
||||
const lastWaveStart = 16;
|
||||
|
||||
// For the last wave, just use the remaining meteors (don't use `meteorShower`).
|
||||
const lastWave: AnimationRule<Styles>[] = [];
|
||||
for (let id = 1; id <= meteorCount; id++) {
|
||||
if (!meteorsInUse.has(id)) {
|
||||
lastWave.push(showMeteor(METEOR_ID(id), lastWaveStart));
|
||||
}
|
||||
}
|
||||
|
||||
const meteorFieldLayerAnim: AnimationDefinition = [
|
||||
{
|
||||
selector: METEOR_FIELD,
|
||||
at: at(7),
|
||||
styles: {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: METEOR_FIELD,
|
||||
timeframe: timeframe(8, 18),
|
||||
from: {
|
||||
transform: 'scale(1.42)',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
},
|
||||
...firstWave,
|
||||
...secondWave,
|
||||
...thirdWave,
|
||||
...lastWave,
|
||||
{
|
||||
selector: METEORS,
|
||||
timeframe: timeframe(19.5, 21),
|
||||
from: {
|
||||
transform: 'translate(0, 0) scale(1)',
|
||||
},
|
||||
to: {
|
||||
transform: 'translate(-200%, -200%) scale(0.3)',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: METEOR_FIELD,
|
||||
timeframe: timeframe(19.5, 21),
|
||||
from: {
|
||||
opacity: '1',
|
||||
},
|
||||
to: {
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: METEOR_FIELD,
|
||||
at: at(22),
|
||||
styles: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// "Loved by millions" layer animation
|
||||
// ***********************************
|
||||
const lovedByMillionsAnim: AnimationDefinition = [
|
||||
{
|
||||
selector: LOVED_BY_MILLIONS_LAYER_ID,
|
||||
timeframe: timeframe(14, 15.5),
|
||||
from: {
|
||||
transform: 'scale(0.75)',
|
||||
opacity: '0',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(1)',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: LOVED_BY_MILLIONS_LAYER_ID,
|
||||
timeframe: timeframe(19, 20.5),
|
||||
from: {
|
||||
transform: 'scale(1)',
|
||||
opacity: '1',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(1.5)',
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// "Build for everyone" layer
|
||||
// **************************
|
||||
const buildForEveryoneAnim: AnimationDefinition = [
|
||||
{
|
||||
selector: BUILD_FOR_EVERYONE_LAYER_ID,
|
||||
timeframe: timeframe(22, 25),
|
||||
from: {
|
||||
transform: 'scale(0.75)',
|
||||
opacity: '0',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(1)',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: BUILD_FOR_EVERYONE_TITLE,
|
||||
timeframe: timeframe(23, 25),
|
||||
from: {
|
||||
'background-position-x': '100%',
|
||||
},
|
||||
to: {
|
||||
'background-position-x': '0',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: BUILD_FOR_EVERYONE_LAYER_ID,
|
||||
timeframe: timeframe(29, 31.5),
|
||||
from: {
|
||||
opacity: '1',
|
||||
},
|
||||
to: {
|
||||
opacity: '0',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
...bannersLayerAnim,
|
||||
...(!isUwu ? logoLayerAnim : uwuLayerAnimation),
|
||||
...waasLayerAnim,
|
||||
...meteorFieldLayerAnim,
|
||||
...lovedByMillionsAnim,
|
||||
...buildForEveryoneAnim,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<div class="animation" [class.reduced-motion]="reducedMotion()">
|
||||
@if (!isUwu()) {
|
||||
<!-- Angular logo -->
|
||||
<div adevAnimationLayer layerId="logo" class="logo-layer layer">
|
||||
<svg class="svg" viewBox="0 0 1280 400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Do not remove `gradientTransform` attribute. Safari needs it in order to start the animation. -->
|
||||
<linearGradient
|
||||
id="gradient"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(0 640 125)"
|
||||
>
|
||||
<stop offset="0" stop-color="#ed0100" />
|
||||
<stop offset="0.25" stop-color="#e449b4" />
|
||||
<stop offset="0.5" stop-color="#ae3bfc" />
|
||||
<stop offset="1" stop-color="#7f15fd" />
|
||||
<animateTransform
|
||||
attributeName="gradientTransform"
|
||||
type="rotate"
|
||||
from="0 640 125"
|
||||
to="360 640 125"
|
||||
dur="6.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g class="wrapper" fill="url(#gradient)">
|
||||
<g class="logo">
|
||||
<g class="shield">
|
||||
<path
|
||||
class="shield-base"
|
||||
d="M311.8,101.7L302,255.6L208.3,53.8L311.8,101.7ZM246.9,300.7L176.1,341.1L105.3,300.7L246.9,300.7ZM50.1,255.6L40.4,101.7L143.9,53.8L50.1,255.6Z"
|
||||
/>
|
||||
<path class="shield-middle" d="M176.1,130.4L213.2,220.6L139,220.6L176.1,130.4Z" />
|
||||
<path
|
||||
class="shield-bottom-a-arc"
|
||||
d="M106.419,301.339L105.3,300.7L119.7,265.8L232.5,265.8L246.9,300.7L245.781,301.339L106.419,301.339Z"
|
||||
/>
|
||||
<path
|
||||
class="shield-bottom-extension"
|
||||
d="M245.78,301.339L106.42,301.339L68.889,279.923L283.311,279.923L245.78,301.339Z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
class="capt-a-letter letter"
|
||||
d="M515.4,288L553.1,288L484.4,92.7L440.7,92.7L372,288L409.7,288L425.8,239.9L499.2,239.9L515.4,288ZM435.4,211.4L461.8,132.8L463.3,132.8L489.7,211.4L435.4,211.4Z"
|
||||
/>
|
||||
<path
|
||||
class="n-letter letter"
|
||||
d="M590.4,202.2L590.4,288L555.9,288L555.9,141.6L588.9,141.6L588.9,166.5L590.6,166.5C594,158.3 599.4,151.8 606.8,147C614.2,142.2 623.4,139.8 634.3,139.8C644.4,139.8 653.2,142 660.8,146.3C668.3,150.6 674.2,156.9 678.3,165.1C682.5,173.3 684.5,183.2 684.4,194.9L684.4,288L649.9,288L649.9,200.1C649.9,190.3 647.4,182.7 642.3,177.1C637.2,171.6 630.3,168.8 621.4,168.8C615.4,168.8 610,170.1 605.3,172.8C600.6,175.4 597,179.2 594.3,184.2C591.8,189.1 590.4,195.1 590.4,202.2Z"
|
||||
/>
|
||||
<path
|
||||
class="g-letter letter"
|
||||
d="M760.5,345.9C748.1,345.9 737.5,344.2 728.6,340.9C719.7,337.6 712.6,333.1 707.2,327.5C701.8,321.9 698,315.7 696,308.9L727.1,301.4C728.5,304.3 730.5,307.1 733.2,309.9C735.9,312.7 739.5,315.1 744,317C748.5,318.9 754.3,319.8 761.2,319.8C771,319.8 779.1,317.4 785.5,312.7C791.9,308 795.1,300.2 795.1,289.4L795.1,261.7L793.4,261.7C791.6,265.3 789,268.9 785.6,272.7C782.2,276.5 777.7,279.6 772.2,282.1C766.6,284.6 759.7,285.9 751.3,285.9C740.1,285.9 729.9,283.2 720.7,277.9C711.6,272.6 704.3,264.6 699,254.1C693.6,243.5 690.9,230.3 690.9,214.3C690.9,198.2 693.6,184.6 699,173.5C704.4,162.4 711.6,154 720.8,148.2C730,142.4 740.2,139.6 751.4,139.6C760,139.6 767.1,141 772.6,143.9C778.2,146.8 782.6,150.3 785.9,154.3C789.2,158.3 791.7,162.1 793.4,165.7L795.3,165.7L795.3,141.5L829.3,141.5L829.3,290.3C829.3,302.8 826.3,313.2 820.3,321.4C814.3,329.6 806.2,335.7 795.8,339.8C785.4,343.9 773.6,345.9 760.5,345.9ZM760.8,258.8C768.1,258.8 774.3,257 779.5,253.5C784.6,249.9 788.6,244.8 791.2,238.2C793.9,231.5 795.2,223.5 795.2,214.2C795.2,205 793.9,196.9 791.2,190C788.6,183.1 784.7,177.7 779.6,173.8C774.5,170 768.2,168 760.8,168C753.1,168 746.7,170 741.5,174C736.4,178 732.5,183.5 729.9,190.4C727.3,197.4 726,205.3 726,214.1C726,223.1 727.3,230.9 730,237.6C732.6,244.3 736.5,249.5 741.7,253.2C746.8,256.9 753.2,258.8 760.8,258.8Z"
|
||||
/>
|
||||
<path
|
||||
class="u-letter letter"
|
||||
d="M933.8,226.4L933.8,141.6L968.3,141.6L968.3,288L934.8,288L934.8,262L933.3,262C930,270.2 924.6,276.9 917,282.1C909.5,287.3 900.2,289.9 889.2,289.9C879.6,289.9 871.1,287.8 863.8,283.5C856.5,279.2 850.7,273 846.6,264.7C842.5,256.5 840.4,246.5 840.4,234.8L840.4,141.6L875,141.6L875,229.5C875,238.8 877.5,246.2 882.6,251.6C887.7,257.1 894.4,259.8 902.6,259.8C907.7,259.8 912.6,258.6 917.4,256.1C922.2,253.6 926.1,249.9 929.2,245C932.3,240 933.8,233.8 933.8,226.4Z"
|
||||
/>
|
||||
<rect class="l-letter letter" x="979.6" y="92.7" width="34.5" height="195.3" />
|
||||
<path
|
||||
class="a-letter letter"
|
||||
d="M1068.9,290.9C1059.6,290.9 1051.3,289.2 1043.9,285.9C1036.5,282.6 1030.6,277.6 1026.4,271.1C1022.1,264.6 1020,256.5 1020,246.9C1020,238.6 1021.5,231.8 1024.6,226.4C1027.7,221 1031.8,216.7 1037.1,213.4C1042.4,210.2 1048.3,207.7 1055,206C1061.6,204.3 1068.5,203.1 1075.6,202.3C1084.2,201.4 1091.1,200.6 1096.5,199.9C1101.8,199.2 1105.7,198 1108.2,196.5C1110.6,194.9 1111.9,192.5 1111.9,189.2L1111.9,188.6C1111.9,181.4 1109.8,175.9 1105.5,171.9C1101.2,168 1095.1,166 1087.1,166C1078.6,166 1072,167.8 1067,171.5C1062.1,175.2 1058.8,179.5 1057,184.6L1024.8,180C1027.3,171.1 1031.5,163.7 1037.4,157.6C1043.2,151.6 1050.4,147.1 1058.8,144.1C1067.3,141.1 1076.6,139.6 1086.8,139.6C1093.9,139.6 1100.9,140.4 1107.9,142.1C1114.9,143.8 1121.3,146.5 1127.1,150.3C1132.9,154.1 1137.5,159.2 1141.1,165.7C1144.6,172.2 1146.4,180.3 1146.4,190L1146.4,288L1113.2,288L1113.2,267.9L1112.1,267.9C1110,272 1107.1,275.8 1103.3,279.3C1099.5,282.8 1094.8,285.7 1089.1,287.8C1083.2,289.9 1076.6,290.9 1068.9,290.9ZM1077.9,265.6C1084.8,265.6 1090.8,264.2 1095.9,261.5C1101,258.7 1104.9,255.1 1107.7,250.5C1110.5,245.9 1111.8,240.9 1111.8,235.5L1111.8,218.2C1110.7,219.1 1108.9,219.9 1106.3,220.7C1103.7,221.5 1100.9,222.1 1097.7,222.7C1094.5,223.3 1091.4,223.8 1088.3,224.2C1085.2,224.6 1082.5,225 1080.2,225.3C1075.1,226 1070.4,227.1 1066.4,228.7C1062.3,230.3 1059.1,232.5 1056.8,235.3C1054.4,238.1 1053.3,241.8 1053.3,246.2C1053.3,252.6 1055.6,257.4 1060.3,260.6C1064.8,264 1070.7,265.6 1077.9,265.6Z"
|
||||
/>
|
||||
<path
|
||||
class="r-letter letter"
|
||||
d="M1156.9,288L1156.9,141.6L1190.4,141.6L1190.4,166L1191.9,166C1194.6,157.5 1199.2,151 1205.7,146.4C1212.2,141.8 1219.7,139.5 1228.1,139.5C1230,139.5 1232.2,139.6 1234.5,139.7C1236.9,139.9 1238.9,140.1 1240.5,140.4L1240.5,172.1C1239,171.6 1236.7,171.1 1233.6,170.7C1230.5,170.3 1227.4,170.1 1224.5,170.1C1218.2,170.1 1212.6,171.5 1207.6,174.2C1202.6,176.9 1198.7,180.6 1195.8,185.4C1192.9,190.2 1191.5,195.7 1191.5,201.9L1191.5,288L1156.9,288Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- UwU logo -->
|
||||
<div adevAnimationLayer layerId="uwu" class="uwu-layer layer">
|
||||
<img src="assets/images/uwu.png" alt="Angular logo" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Works at any scale -->
|
||||
<div adevAnimationLayer layerId="works-at-any-scale" class="works-at-any-scale-layer layer">
|
||||
<h2>Works at any scale</h2>
|
||||
<p>
|
||||
Angular lets you start small on a well-lit path and supports you as your team and apps grow.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Meteor field -->
|
||||
@if (meteorFieldData(); as meteorFieldData) {
|
||||
<div adevAnimationLayer layerId="meteor-field" class="meteor-field-layer layer">
|
||||
<div
|
||||
class="field"
|
||||
[style.width]="meteorFieldData.width + 'px'"
|
||||
[style.height]="meteorFieldData.height + 'px'"
|
||||
[style.marginLeft]="meteorFieldData.marginLeft + 'px'"
|
||||
[style.marginTop]="meteorFieldData.marginTop + 'px'"
|
||||
>
|
||||
@for (type of meteors(); track $index) {
|
||||
<div [class]="['meteor', 'type-' + type, 'mt-' + ($index + 1)]"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Loved by millions -->
|
||||
<div adevAnimationLayer layerId="loved-by-millions" class="loved-by-millions-layer layer">
|
||||
<h2>Loved by millions</h2>
|
||||
<p>
|
||||
Join the millions of developers all over the world building with Angular in a thriving and
|
||||
friendly community.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Build for everyone -->
|
||||
<div adevAnimationLayer layerId="build-for-everyone" class="build-for-everyone-layer layer">
|
||||
<h2 class="title">Build for everyone</h2>
|
||||
<p>
|
||||
Rely on Angular's built-in hydration, internationalization, security, and accessibility
|
||||
support to build for everyone around the world.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,403 @@
|
||||
@use 'sass:math';
|
||||
@use '@angular/docs/styles/media-queries' as mq;
|
||||
|
||||
/*
|
||||
* Global transition.
|
||||
*
|
||||
* Scrolling via mouse scroll wheel will result in a non-continuous scrollY,
|
||||
* unlike a touchpad, which in turn results in "gaps" in the animation calculations.
|
||||
* Therefore, in order to keep things smooth, we are systematically adding CSS
|
||||
* transitions to the CSS properties that else look jagged when scrolling.
|
||||
*/
|
||||
$transition: 200ms linear;
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
.animation {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
.layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
h2 {
|
||||
font-size: 4vw;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
z-index: 1;
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
& {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
color: var(--quaternary-contrast);
|
||||
font-size: clamp(1rem, 1vw, 2rem);
|
||||
line-height: 1.5;
|
||||
width: clamp(345px, 50%, 600px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2,
|
||||
p {
|
||||
background-color: var(--page-background);
|
||||
box-shadow: 0 0 20px 20px var(--page-background);
|
||||
|
||||
@include mq.for-desktop-down {
|
||||
box-shadow: 0 0 10px 10px var(--page-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.banners-layer {
|
||||
z-index: 10;
|
||||
|
||||
.adev-banner-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
position: absolute;
|
||||
transition:
|
||||
background 0.3s ease,
|
||||
border 0.3s ease,
|
||||
transform $transition;
|
||||
top: var(--layout-padding);
|
||||
left: calc(var(--layout-padding) + var(--primary-nav-width));
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
top: 6rem;
|
||||
left: var(--layout-padding);
|
||||
/* Assuming the container width is identical to the viewport width (mobile device). */
|
||||
max-width: calc(100% - var(--layout-padding) * 2);
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
& {
|
||||
top: 5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid var(--senary-contrast);
|
||||
background: var(--page-background);
|
||||
border-radius: 0.25rem;
|
||||
padding: 10px;
|
||||
max-width: 100%;
|
||||
width: fit-content;
|
||||
box-sizing: border-box;
|
||||
|
||||
h1,
|
||||
p {
|
||||
display: inline;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
background-image: var(--red-to-pink-to-purple-horizontal-gradient);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
width: fit-content;
|
||||
font-weight: 500;
|
||||
box-shadow: none;
|
||||
position: relative;
|
||||
|
||||
&.adev-banner-cta {
|
||||
color: var(--tertiary-contrast);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
transform: scaleX(0);
|
||||
height: 1px;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
background: var(--red-to-pink-to-purple-horizontal-gradient);
|
||||
transform-origin: bottom right;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.adev-banner-cta {
|
||||
&::after {
|
||||
transform: scaleX(1);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.learn-angular {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 5%;
|
||||
transform: translateX(-50%);
|
||||
transition: opacity $transition;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
|
||||
button {
|
||||
font-size: 1rem;
|
||||
padding: 1rem 1.75rem;
|
||||
|
||||
&::after {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.adev-arrow {
|
||||
transform: rotate(45deg);
|
||||
border: solid var(--primary-contrast);
|
||||
border-width: 0 2px 2px 0;
|
||||
display: inline-block;
|
||||
padding: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-layer {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
|
||||
.svg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 2;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
transform: scale(0.55);
|
||||
transform-origin: 50% 50%;
|
||||
|
||||
@include mq.for-tablet-down {
|
||||
& {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-extra-large-desktop-up {
|
||||
& {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
transition: transform $transition;
|
||||
|
||||
.shield {
|
||||
/* `transform-origin` values are calculated based on the 1280x400 SVG view box */
|
||||
|
||||
&,
|
||||
.shield-middle {
|
||||
transform-origin: 177px 200px;
|
||||
transition: transform $transition;
|
||||
}
|
||||
|
||||
.shield-bottom-a-arc {
|
||||
transform-origin: 0 302px;
|
||||
transition: transform $transition;
|
||||
}
|
||||
|
||||
.shield-bottom-extension {
|
||||
transform-origin: 177px 340px;
|
||||
transition: transform $transition;
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
.letter {
|
||||
transition: opacity $transition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uwu-layer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
transform $transition,
|
||||
opacity $transition;
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
width: max(calc(700 * min(100vw, 2560px) / 1470), 350px);
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.works-at-any-scale-layer,
|
||||
.loved-by-millions-layer,
|
||||
.build-for-everyone-layer {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
transition:
|
||||
transform $transition,
|
||||
opacity $transition;
|
||||
}
|
||||
|
||||
.works-at-any-scale-layer {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.loved-by-millions-layer,
|
||||
.build-for-everyone-layer {
|
||||
transform: scale(0.75);
|
||||
}
|
||||
|
||||
.build-for-everyone-layer {
|
||||
.title {
|
||||
color: transparent;
|
||||
display: inline-block;
|
||||
background: linear-gradient(110deg, #f31a5b 0, #8737e9 50%, #3a373f 0%);
|
||||
background-size: 205% 100%;
|
||||
background-clip: text;
|
||||
background-size: 205% 100%;
|
||||
background-position-x: 100%;
|
||||
transition: background-position-x $transition;
|
||||
}
|
||||
}
|
||||
|
||||
.meteor-field-layer {
|
||||
overflow: hidden;
|
||||
|
||||
.field {
|
||||
--math-pi: #{math.$pi}rad;
|
||||
|
||||
position: absolute;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: var(--meteor-gap);
|
||||
display: none;
|
||||
transform: scale(1.42);
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: 50% 50%;
|
||||
transition:
|
||||
opacity $transition,
|
||||
transform $transition;
|
||||
|
||||
.meteor {
|
||||
position: relative;
|
||||
width: var(--meteor-width);
|
||||
height: var(--meteor-height);
|
||||
opacity: 0;
|
||||
transform: translate(200%, 200%) scale(0.3);
|
||||
transition:
|
||||
opacity $transition,
|
||||
transform $transition;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: var(--meteor-tail-length);
|
||||
border-radius: 2px;
|
||||
transform-origin: top center;
|
||||
transform: rotate(var(--meteor-tilt-angle));
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@mixin meteor-gradient($startColor, $endColor) {
|
||||
background: linear-gradient(
|
||||
calc(var(--math-pi) + var(--meteor-tilt-angle)),
|
||||
$startColor 0,
|
||||
$endColor 66%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&.type-1::after {
|
||||
@include meteor-gradient(rgb(228, 49, 85), rgb(219, 64, 219));
|
||||
}
|
||||
|
||||
&.type-2::after {
|
||||
@include meteor-gradient(rgb(152, 56, 226), rgb(217, 18, 167));
|
||||
}
|
||||
|
||||
&.type-3::after {
|
||||
@include meteor-gradient(rgb(214, 53, 150), rgb(229, 91, 229));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.reduced-motion {
|
||||
position: relative;
|
||||
|
||||
.banners-layer {
|
||||
height: 100vh;
|
||||
|
||||
@include mq.for-phone-only {
|
||||
.adev-banner {
|
||||
top: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-up {
|
||||
.adev-banner {
|
||||
left: var(--layout-padding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-layer {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
margin-top: -75px;
|
||||
}
|
||||
}
|
||||
|
||||
.works-at-any-scale-layer,
|
||||
.loved-by-millions-layer,
|
||||
.build-for-everyone-layer {
|
||||
position: relative;
|
||||
height: 120vh;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.build-for-everyone-layer > .title {
|
||||
background-position-x: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*!
|
||||
* @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 {
|
||||
afterNextRender,
|
||||
Component,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
inject,
|
||||
Injector,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {isIos, shouldReduceMotion, WINDOW} from '@angular/docs';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
import {AnimationCreatorService, AnimationLayerDirective} from '../../animation';
|
||||
import {AnimationScrollHandler} from '../../animation/plugins/animation-scroll-handler';
|
||||
import {ANIM_TIMESTEP, generateHomeAnimationDefinition} from './animation-definition';
|
||||
|
||||
export const METEOR_HW_RATIO = 1.42; // Height to width ratio
|
||||
export const METEOR_GAP_RATIO = 1.33; // Use 0.7 for WebGL-like field. Renders a lot of elements though.
|
||||
|
||||
// A map with screen size to meteor width
|
||||
export const METEOR_WIDTH_MAP = [
|
||||
[800, 60],
|
||||
[1100, 90],
|
||||
];
|
||||
|
||||
export const METEOR_WIDTH_DEFAULT = 120; // For screens larger than 1100px
|
||||
|
||||
type MeteorDimensions = {
|
||||
width: number;
|
||||
height: number;
|
||||
tailLength: number;
|
||||
gap: number;
|
||||
tiltAngle: number; // In radians
|
||||
};
|
||||
|
||||
type MeteorFieldData = {
|
||||
width: number;
|
||||
height: number;
|
||||
count: number;
|
||||
marginLeft: number;
|
||||
marginTop: number;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'adev-home-animation',
|
||||
imports: [AnimationLayerDirective, RouterLink],
|
||||
templateUrl: './home-animation.component.html',
|
||||
styleUrl: './home-animation.component.scss',
|
||||
})
|
||||
export class HomeAnimationComponent {
|
||||
private readonly win = inject(WINDOW);
|
||||
private readonly animCreator = inject(AnimationCreatorService);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly elementRef = inject(ElementRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
readonly animationLayers = viewChildren(AnimationLayerDirective);
|
||||
|
||||
readonly ctaLink = isIos ? 'overview' : 'tutorials/learn-angular';
|
||||
|
||||
readonly isUwu = input.required<boolean>();
|
||||
readonly ready = output<boolean>();
|
||||
|
||||
readonly reducedMotion = signal<boolean>(shouldReduceMotion());
|
||||
readonly meteorFieldData = signal<MeteorFieldData | null>(null);
|
||||
readonly meteors = signal<number[]>([]);
|
||||
|
||||
constructor() {
|
||||
if (!this.reducedMotion()) {
|
||||
this.initAnimation();
|
||||
} else {
|
||||
this.ready.emit(true);
|
||||
}
|
||||
}
|
||||
|
||||
private initAnimation() {
|
||||
// Limitation: Meteor dimensions won't change on page resize
|
||||
const meteorDimensions = this.calculateMeteorDimensions();
|
||||
const data = this.calculateMeteorFieldData(meteorDimensions);
|
||||
this.setCssVariables(meteorDimensions);
|
||||
this.meteorFieldData.set(data);
|
||||
|
||||
// Generate a meteor field. The number represents the type [1, 3]
|
||||
this.meteors.set(new Array(data.count).fill(1).map(() => Math.round(Math.random() * 2 + 1)));
|
||||
|
||||
afterNextRender({
|
||||
read: () => {
|
||||
const animation = this.animCreator
|
||||
.createAnimation(this.animationLayers(), {timestep: ANIM_TIMESTEP})
|
||||
.define(generateHomeAnimationDefinition(this.isUwu(), this.meteors().length))
|
||||
.addPlugin(new AnimationScrollHandler(this.elementRef, this.injector));
|
||||
|
||||
this.ready.emit(true);
|
||||
this.destroyRef.onDestroy(() => animation.dispose());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Calculate the dimensions and sizes of a meteor – width, height, tail, tilt angle, etc. */
|
||||
private calculateMeteorDimensions(): MeteorDimensions {
|
||||
let width = METEOR_WIDTH_DEFAULT;
|
||||
|
||||
for (const [screenSize, meteorWidth] of METEOR_WIDTH_MAP) {
|
||||
if (this.win.innerWidth <= screenSize) {
|
||||
width = meteorWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const height = width * METEOR_HW_RATIO;
|
||||
const gap = width * METEOR_GAP_RATIO;
|
||||
|
||||
// Pythagorean theorem + some trigonometry
|
||||
const tailLength = Math.sqrt(width * width + height * height);
|
||||
const tiltAngle = -Math.asin(width / tailLength);
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
gap,
|
||||
tailLength,
|
||||
tiltAngle,
|
||||
};
|
||||
}
|
||||
|
||||
/** Calculate the number of meteors and size of the field. */
|
||||
private calculateMeteorFieldData(meteorDim: MeteorDimensions): MeteorFieldData {
|
||||
const mW = meteorDim.width + meteorDim.gap;
|
||||
const mH = meteorDim.height + meteorDim.gap;
|
||||
let rows = 1;
|
||||
let cols = 1;
|
||||
|
||||
while (cols * mW - meteorDim.gap <= this.win.innerWidth) {
|
||||
cols++;
|
||||
}
|
||||
while (rows * mH - meteorDim.gap <= this.win.innerHeight) {
|
||||
rows++;
|
||||
}
|
||||
|
||||
const width = cols * mW - meteorDim.gap;
|
||||
const height = rows * mH - meteorDim.gap;
|
||||
|
||||
return {
|
||||
count: rows * cols,
|
||||
width,
|
||||
height,
|
||||
marginLeft: -(width - this.win.innerWidth) / 2,
|
||||
marginTop: -(height - this.win.innerHeight) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
private setCssVariables({width, height, tailLength, tiltAngle, gap}: MeteorDimensions) {
|
||||
const styleRef = this.elementRef.nativeElement.style;
|
||||
styleRef.setProperty('--meteor-width', width + 'px');
|
||||
styleRef.setProperty('--meteor-height', height + 'px');
|
||||
styleRef.setProperty('--meteor-tail-length', tailLength + 'px');
|
||||
styleRef.setProperty('--meteor-tilt-angle', tiltAngle + 'rad');
|
||||
styleRef.setProperty('--meteor-gap', gap + 'px');
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
list-style: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
@@ -708,7 +708,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Advanced,
|
||||
step: 'wtf',
|
||||
action:
|
||||
'Support for web tracing framework in Angular was deprecated in version 8. You should stop using any of the `wtf*` APIs. To do performance tracing, we recommend using [browser performance tools](https://developer.chrome.com/docs/lighthouse/performance/user-timings).',
|
||||
'Support for web tracing framework in Angular was deprecated in version 8. You should stop using any of the `wtf*` APIs. To do performance tracing, we recommend using [browser performance tools](https://developers.google.com/web/tools/lighthouse/audits/user-timing).',
|
||||
},
|
||||
{
|
||||
possibleIn: 800,
|
||||
@@ -2542,14 +2542,6 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
possibleIn: 1900,
|
||||
step: '19.0.0-update-fakeasync-to-flush-pending-timers',
|
||||
},
|
||||
{
|
||||
action:
|
||||
'Remove the `allowSignalWrites` option from `effect()` calls. The option is deprecated and no longer required, as signal writes inside effects are allowed by default.',
|
||||
level: ApplicationComplexity.Medium,
|
||||
necessaryAsOf: 1900,
|
||||
possibleIn: 1900,
|
||||
step: '19.0.0-remove-allow-signal-writes',
|
||||
},
|
||||
{
|
||||
action:
|
||||
"In the application's project directory, run `ng update @angular/core@20 @angular/cli@20` to update your application to Angular v20.",
|
||||
@@ -2780,14 +2772,6 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
possibleIn: 2100,
|
||||
step: '21.0.0_ng_update',
|
||||
},
|
||||
{
|
||||
possibleIn: 2100,
|
||||
necessaryAsOf: 2100,
|
||||
level: ApplicationComplexity.Advanced,
|
||||
step: '21.0.0-safe-resource-url-audio-src',
|
||||
action:
|
||||
'If you use `SafeResourceUrl` values with `audio[src]` bindings, be aware that `audio[src]` is no longer sanitized in Angular v21. Existing uses of `bypassSecurityTrustResourceUrl` may therefore produce the `SafeValue must use [property]=binding` message. Remove the unnecessary sanitization and bind the URL directly instead.',
|
||||
},
|
||||
|
||||
{
|
||||
possibleIn: 2100,
|
||||
@@ -2797,15 +2781,6 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
step: 'update @angular/material',
|
||||
action: 'Run `ng update @angular/material@21`.',
|
||||
},
|
||||
{
|
||||
possibleIn: 2100,
|
||||
necessaryAsOf: 2100,
|
||||
level: ApplicationComplexity.Medium,
|
||||
material: true,
|
||||
step: '21.0.0-cdk-overlay-top-layer-stacking',
|
||||
action:
|
||||
"CDK overlays can now render in the browser's native top layer, causing elements that previously appeared above Material overlays via `z-index` to render beneath them. You can restore the previous behavior by providing `OVERLAY_DEFAULT_CONFIG` from `@angular/cdk/overlay` with the value `{usePopover: false}`.",
|
||||
},
|
||||
{
|
||||
possibleIn: 2100,
|
||||
necessaryAsOf: 2100,
|
||||
|
||||
@@ -147,12 +147,8 @@
|
||||
<h3>Before you update</h3>
|
||||
@for (r of beforeRecommendations; track $index) {
|
||||
<div class="adev-recommendation-item">
|
||||
<mat-checkbox #checkbox [aria-labelledby]="'adev-before-recommendation-' + $index" />
|
||||
<div
|
||||
class="adev-recommendation-content"
|
||||
[id]="'adev-before-recommendation-' + $index"
|
||||
(click)="toggleRecommendation($event, checkbox)"
|
||||
>
|
||||
<mat-checkbox />
|
||||
<div class="adev-recommendation-content">
|
||||
<div [innerHTML]="r.renderedStep"></div>
|
||||
<span class="adev-complexity-badge" [class]="'adev-complexity-' + r.level">
|
||||
{{ getComplexityLevelName(r.level) }}
|
||||
@@ -175,12 +171,8 @@
|
||||
|
||||
@for (r of duringRecommendations; track $index) {
|
||||
<div class="adev-recommendation-item">
|
||||
<mat-checkbox #checkbox [aria-labelledby]="'adev-during-recommendation-' + $index" />
|
||||
<div
|
||||
class="adev-recommendation-content"
|
||||
[id]="'adev-during-recommendation-' + $index"
|
||||
(click)="toggleRecommendation($event, checkbox)"
|
||||
>
|
||||
<mat-checkbox />
|
||||
<div class="adev-recommendation-content">
|
||||
<div [innerHTML]="r.renderedStep"></div>
|
||||
<span class="adev-complexity-badge" [class]="'adev-complexity-' + r.level">
|
||||
{{ getComplexityLevelName(r.level) }}
|
||||
@@ -197,12 +189,8 @@
|
||||
<h3>After you update</h3>
|
||||
@for (r of afterRecommendations; track $index) {
|
||||
<div class="adev-recommendation-item">
|
||||
<mat-checkbox #checkbox [aria-labelledby]="'adev-after-recommendation-' + $index" />
|
||||
<div
|
||||
class="adev-recommendation-content"
|
||||
[id]="'adev-after-recommendation-' + $index"
|
||||
(click)="toggleRecommendation($event, checkbox)"
|
||||
>
|
||||
<mat-checkbox />
|
||||
<div class="adev-recommendation-content">
|
||||
<div [innerHTML]="r.renderedStep"></div>
|
||||
<span class="adev-complexity-badge" [class]="'adev-complexity-' + r.level">
|
||||
{{ getComplexityLevelName(r.level) }}
|
||||
|
||||
@@ -65,7 +65,6 @@ h4 {
|
||||
transform: translateY(-0.7rem);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
width: $ver-dropdown-width;
|
||||
box-sizing: border-box;
|
||||
background: var(--page-background);
|
||||
@@ -137,7 +136,6 @@ h4 {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.adev-complexity-badge {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user