Compare commits

..

35 Commits

Author SHA1 Message Date
Alan Agius d366e4bd02 release: cut the v22.1.0 release 2026-07-29 22:39:16 +02:00
Kam 7825e607c8 docs: invoke signal inputs in adev examples and tutorials
Several adev example and tutorial files read a signal input as a bare
reference (this.foo) instead of invoking it (this.foo()). Because an
InputSignal is a function object, the bare reference is always truthy
and never yields the underlying value, so the surrounding guard or
binding silently did the wrong thing:

- animations open-close(.1/.3): the `!this.logging` guard in
  onAnimationEvent was always false, so the early return never fired.
- form-validation forbidden-name.directive: the `this.forbiddenName`
  ternary condition was always truthy, so validation ran even when no
  forbidden name was configured.
- first-app steps 12 and 14 housing-location: `housingLocation.photo`
  read `.photo` off the signal function (undefined), leaving the
  listing image src empty.

Invoke the signals so the examples reflect correct signal-input usage.

(cherry picked from commit 0d26130a4c)
2026-07-29 09:59:22 -07:00
SkyZeroZx c73dd603a4 refactor(forms): modernize signal forms tests to rely on whenStable
Rely on zoneless test scheduling instead of manually triggering change detection. Keep Signals Forms tests aligned with the async-first testing pattern.

(cherry picked from commit 59f6ef690b)
2026-07-29 09:57:42 -07:00
Shuaib Hasan Akib 098d28febf docs: fix self-referential link in defer block description
Replace broken  link with inline code
formatting. The link was pointing to the page itself, as no dedicated block concept page exists at that URL.
corrects 'everytime' to 'every time'.

Update tools/manual_api_docs/blocks/let.md

Co-authored-by: Matthieu Riegler <kyro38@gmail.com>
(cherry picked from commit 21eed5f8d4)
2026-07-29 09:45:17 -07:00
Kam fcaf15a0a4 fix(docs-infra): fix undefined CSS custom properties in shared-docs styles
Two custom properties in the shared-docs styles reference tokens that are
defined nowhere, so the declarations are invalid at computed-value time.

_kbd.scss sets the <kbd> text color with var(---tertiary-contrast); the extra
leading dash points at an undefined property with no fallback, so the color
resolves to the inherited value instead of the intended --tertiary-contrast.

_colors.scss builds --light-pink-to-light-purple-horizontal-gradient from
var(--light-purple), which is not defined anywhere (the sibling token is
--light-violet, defined in the same file), invalidating the gradient. The
gradient is not currently referenced, so this corrects a latent malformed
declaration rather than a visible bug.

Point both at the defined tokens: --tertiary-contrast and --light-violet.

(cherry picked from commit e70994bdc3)
2026-07-29 09:44:41 -07:00
Shuaib Hasan Akib 79a37f3d0b refactor(common): replaces the deprecated positional subscribe arguments with the
recommended observer object

(cherry picked from commit ec87f04200)
2026-07-29 09:43:57 -07:00
SkyZeroZx 2591fa6bd9 docs: clarify behavior of credentials option during SSR on Node.js
(cherry picked from commit 90ac011fa9)
2026-07-29 09:41:33 -07:00
Kam a7d67f9ccc fix(docs-infra): fix malformed --webgl-page-background declaration
In the light theme block of _colors.scss, --webgl-page-background and
--webgl-gray-unfilled were written on a single line without a separating
semicolon, and --webgl-page-background wrapped --page-background in an
invalid nested var(). As a result --webgl-page-background got a garbage
value and --webgl-gray-unfilled was never defined in the light theme.

Split them into two well-formed declarations and drop the nested var(),
matching the structure already used in the dark theme block.

(cherry picked from commit 91822538fe)
2026-07-29 09:04:14 -07:00
Kam 9658ad0ca8 fix(docs-infra): define missing win95 variables in aria autocomplete examples
The retro-themed aria autocomplete examples style the clear button with
var(--win95-gray), var(--win95-dark-gray), var(--win95-light), and
var(--win95-shadow), but none of the three example stylesheets defines
those custom properties. The variables resolve to nothing, so the button
falls back to inherited theme colors — color: var(--win95-shadow) becomes
the light-theme text color in dark mode, leaving the button unreadable.

Define the four variables in :host of each example, matching the values
already used by the sibling aria tree examples, so the clear button keeps
its Windows 95 look in both light and dark themes.

(cherry picked from commit 8cd4c89c29)
2026-07-29 09:02:58 -07:00
Kam f5a968b003 fix(docs-infra): replace undefined --gray-50 in retro aria examples
The retro variants of the toolbar, select, and multiselect aria examples
tint the pressed button background with
color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50)), but
--gray-50 is defined nowhere. Per the CSS spec, a var() with no fallback
pointing at an undefined property invalidates the whole color-mix(), so the
declaration is dropped and the pressed-state tint never applies.

Define a local --retro-pressed-tint token in each example's :host (#fbfbfb,
the value $gray-50 resolved to) with a :host-context(.docs-dark-mode) override
to #151417, and use it for the pressed-state mix. This keeps the example
self-contained rather than depending on adev's global token scope, while
staying theme-aware in both light and dark, following the same token-flipping
pattern used in code-editor.component.scss.

(cherry picked from commit 48c1e5bb0f)
2026-07-29 09:00:00 -07:00
Angular Robot b11cdbd758 build: update pnpm to v11.17.0
See associated pull request for more information.
2026-07-29 08:57:34 -07:00
Kristiyan Kostadinov bbbd357bd2 fix(forms): add utility to assert that value is a field tree
Adds the `isFieldTree` utility that allows users to assert whether a value is a field tree. This is something that has come up on Material recently and will be useful for users as well.

Fixes #69984.

(cherry picked from commit 2a141847a5)
2026-07-29 08:53:29 -07:00
Matthew Beck b569f6e82b test(compiler-cli): add compliance case for @HostListener on a property
`@HostListener` is not limited to methods — it is equally valid on a property
holding a function, which is the idiomatic way to keep `this` bound:

    @HostListener('window:beforeunload', ['$event'])
    private onUnload = (event: BeforeUnloadEvent) => {...};

Every existing host-listener compliance case declares the handler as a method,
so the property form was uncovered. This adds a case exercising both a public
and a private function-valued property, one of them with a global (`window:`)
event target, and locks in the emitted chained `ɵɵlistener` calls plus
`ɵɵresolveWindow`.

Verified against all four compliance modes (full, partial/linked,
declaration-only); GOLDEN_PARTIAL.js regenerated via the golden update rule.

(cherry picked from commit d44b3224d9)
2026-07-29 08:52:58 -07:00
Matthieu Riegler 6dcb534e12 refactor(common): remove duplicate helper function
We have `useAutoTick` in our private shared utils.

(cherry picked from commit 36474f7011)
2026-07-29 08:49:21 -07:00
Maikel van Dort a3fb4fe052 docs: remove semicolon text node
(cherry picked from commit 9373d22a48)
2026-07-29 08:48:39 -07:00
Angular Robot 77c5815faf build: lock file maintenance
See associated pull request for more information.
2026-07-29 08:47:16 -07:00
Matthieu Riegler ada038fca8 refactor(core): Migrate more tests off fakeAsync
This will prevent to polute the agent context with outdated/bad practices.

(cherry picked from commit c1025a0510)
2026-07-29 08:46:06 -07:00
Alan Agius f6fa9eca7d docs: update default value for strictTemplates
Updates the strictTemplates documentation in Angular compiler options to note that the default is true, replacing the reference to the obsolete ng new --strict flag.

(cherry picked from commit 2bcd12a6a5)
2026-07-29 08:42:16 -07:00
Jaime Burgos ec16a3d6c6 fix(http): enable xsrf for root-provided HttpClient
Include the XSRF interceptor in the root token factory so the automatically provided HttpClient retains the documented default protection without requiring provideHttpClient().

(cherry picked from commit de240a5d0e)
2026-07-29 08:40:14 -07:00
SkyZeroZx 39e362eea5 fix(http): match header values exactly when deleting
Normalize value-specific HttpHeaders deletions before filtering. The string overload previously used String#indexOf and removed shorter values contained within the requested deletion value, potentially widening outgoing request metadata.

Preserve delete-all behavior only when no value is supplied, and cover string, array, and empty-string deletion.

(cherry picked from commit f33ee95045)
2026-07-29 08:39:13 -07:00
SkyZeroZx be46ca8696 fix(http): preserve immutability of materialized clones
Prevent lazy HttpHeaders and HttpParams clones from reusing value arrays owned by a materialized source. Append and value-specific delete operations previously mutated those shared arrays, violating the immutable API contract and allowing request metadata to bleed into later requests.

Share value arrays until an update mutates a specific header or parameter, then copy only that array. Cover the affected append and delete paths with regression tests that materialize the source first.

(cherry picked from commit ff02a16749)
2026-07-29 08:39:12 -07:00
Jaime Burgos 5496765f97 docs: Adds HTTP communication guidance to Angular Skills
(cherry picked from commit 840f071566)
2026-07-29 08:37:19 -07:00
SkyZeroZx 091456a214 fix(core): account for namespaces in host binding sanitization (#69558)
Make runtime URL sanitizer selection namespace-aware so SVG and MathML host bindings match the security schema.

Cover SVG href/xlink:href and MathML href host binding cases, including dynamic hostElement resolution.

PR Close #69558
2026-07-29 08:36:34 -07:00
SkyZeroZx 23cf1a828b fix(core): sanitize host bindings on concrete hosts (#69558)
Host binding sanitization previously used the declaring directive or component selector to choose a compile-time security context. The same host binding can execute on a different concrete element through hostDirectives, inherited host bindings, dynamic directives, or createComponent hostElement usage.

Compute host binding security contexts against possible concrete hosts and defer URL versus ResourceURL selection to runtime when necessary. Resolve dynamic root host TNodes to their native tag before sanitizer and security-sensitive attribute checks.

Fixes angular#69550

PR Close #69558
2026-07-29 08:36:34 -07:00
Suraj Yadav 5d5b2ea72d fix(migrations): correctly detect then/else keywords in control flow migration
The control flow migration determines whether an `*ngIf` uses a `then`
and/or `else` clause by regex matching the raw microsyntax string for
the literal keywords `then`/`else`. The regexes only checked that the
keyword was preceded by a non-word character, but not that it was
followed by one.

As a result, a template reference name that merely starts with `then`
(e.g. `else thenBlock`) or `else` was misidentified as the `then`/`else`
keyword itself. This caused the migration to take the wrong code path
(e.g. then+else instead of else-only), which in turn made
`getTemplateName()` compute a `slice(start, end)` with `start > end`,
producing an empty template name. That empty placeholder was never
resolved and was silently emitted as an invalid
`<ng-template [ngTemplateOutlet]=""></ng-template>`, dropping the
original template content without any warning.

Add a negative lookahead `(?![\w\d])` to both regexes so `then`/`else`
are only matched as whole keywords, not as a prefix of a longer
template reference name.

Fixes #69914

(cherry picked from commit 5ad8231397)
2026-07-24 13:56:23 -07:00
Matthew Beck 1caafa6d8a test(compiler-cli): format compliance TEST_CASES.json with prettier
Reformats the TEST_CASES.json files touched by the following change so they
satisfy the repo's prettier check (short inputFiles/files arrays collapsed to a
single line). Pure formatting; the parsed JSON is unchanged. Split into its own
commit so the coverage change that follows is easy to review.

(cherry picked from commit 5245ca5ba7)
2026-07-24 13:55:37 -07:00
Kristiyan Kostadinov a99fb915c0 fix(language-service): account for strictTemplates being enabled by default
We were raising the suggestion about enabling `strictTemplates` when `strictTemplates` is ommitted, however the option is now enabled by default.

Fixes #69905.

(cherry picked from commit e606a020e9)
2026-07-24 13:45:29 -07:00
Angular Robot da08a8fa59 build: update all github actions
See associated pull request for more information.
2026-07-24 13:44:01 -07:00
Ben Hong b2978dbfe1 docs: modernize directives guides (#69822)
Co-authored-by: Matthieu Riegler <kyro38@gmail.com>

PR Close #69822
2026-07-24 10:36:59 -07:00
Ben Hong 522041cefe docs: smooth out directives guide narrative flow (#69822)
PR Close #69822
2026-07-24 10:36:59 -07:00
Kam 125a794891 docs: use https for external links in adev
Several guides, examples, and the update-guide recommendations linked to
external resources over insecure http. Switch them to https.

(cherry picked from commit 28d59e8d63)
2026-07-24 08:28:12 -07:00
mfstapert 50f990a67a docs: update attribute testing guide to include canonical example with local component
(cherry picked from commit 738b8fe9d2)
2026-07-24 08:27:20 -07:00
Jens Kuehlers 2562246429 docs: add v23 release and change to yearly release cycle
## Summary

This PR adds v22.x and v23 release dates. It also changes Angular's release cadence to a yearly cycle.

## Why we are making this change

The community has long requested less frequent major releases due to the impact of breaking changes and upgrades for their projects as well as for enterprise customers. Additionally, a longer release cycle provides increased API stability for developers using agentic workflows, while still delivering a reasonable cadence of API upgrades and migrations.

(cherry picked from commit 25dbe79507)
2026-07-24 08:26:37 -07:00
Kristiyan Kostadinov 350ea416b6 refactor(core): align navigation types with built in ones
Aligns our clone of the navigation API types with the built-in TypeScript types. This is related to an internal issue.

(cherry picked from commit d79b3b65e9)
2026-07-24 08:26:05 -07:00
Pawel Kozlowski 1b85b3cd86 release: cut the v22.1.0-rc.0 release 2026-07-22 16:41:22 +02:00
1469 changed files with 67950 additions and 65904 deletions
+1 -2
View File
@@ -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
View File
@@ -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
}
}
}
+4 -4
View File
@@ -21,17 +21,17 @@ jobs:
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@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}}'
+1 -2
View File
@@ -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 }}
+3 -3
View File
@@ -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
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@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
+3 -4
View File
@@ -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 -2
View File
@@ -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 }}
+2 -4
View File
@@ -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 }}
+5 -10
View File
@@ -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
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Initialize environment
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@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: |
+1 -1
View File
@@ -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 }}
+52
View File
@@ -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
View File
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"strict": true,
"target": "es2020",
"module": "Node16",
"noEmit": true,
+1 -1
View File
@@ -1 +1 @@
24.21.0
22.23.1
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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&apos;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&apos;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&apos;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&apos;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 &amp;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
View File
@@ -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")
+2938 -278
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -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
View File
@@ -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"
}
}
+22 -21
View File
@@ -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)));
+1
View File
@@ -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);
}
}
+3
View File
@@ -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

+9 -9
View File
@@ -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({');
});
});
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/** 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');
});
});
@@ -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);
});
});
@@ -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',
);
});
}
});
@@ -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
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 -18
View File
@@ -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"],
)
+1 -1
View File
@@ -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,
)
);
+1 -1
View File
@@ -122,7 +122,7 @@
}
}
:not(.docs-card-grid) > .docs-card {
:not(.docs-card-container) .docs-card {
margin: 1rem 0;
}
-1
View File
@@ -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;
+3 -1
View File
@@ -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 -18
View File
@@ -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']);
});
});
});
+1 -4
View File
@@ -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,
+1 -2
View File
@@ -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');
}
}
+1 -1
View File
@@ -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}]),
),
],
-2
View File
@@ -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 {
+1 -1
View File
@@ -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