mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2b903957a | |||
| bd72571583 | |||
| 628f3f99db | |||
| 79ac570c26 | |||
| 276c12a349 | |||
| 104ac117ea | |||
| c2a74548d9 | |||
| e7d1d8def3 | |||
| 4a390b6c73 | |||
| 5a486a65cf | |||
| c822bf8e76 | |||
| 31e2c99007 | |||
| f8b6bd0cbe | |||
| 5349b027b8 | |||
| 8f8d67dc57 | |||
| 23153a06dd | |||
| fd3b4413f3 | |||
| 670d1660c4 | |||
| 8ca0237d99 | |||
| 3576019c0f | |||
| 05d022d5e6 | |||
| 1df1697c6e | |||
| 05476ea21b | |||
| f38169d0a7 | |||
| 6d7446bc6c | |||
| c25c84d7dc | |||
| f4160c2073 | |||
| 93c6dc6395 | |||
| 76431ed3e3 | |||
| b4ec3cc4e4 | |||
| 84e79f5add | |||
| 2eecf0d9a2 | |||
| da57d1af73 | |||
| 58ea4d2ee8 | |||
| 277ade97ac | |||
| aeb9b819d2 | |||
| 657d532ad1 | |||
| 8dafd2413a | |||
| 796b9a714a | |||
| dd551b1ff3 | |||
| 84adb2fb3b | |||
| 98343ea35e | |||
| c4226a10d0 | |||
| 5e545069ac | |||
| ecf0bb4668 | |||
| 0d54c5f0e2 | |||
| 8f6ca1d9f9 | |||
| 1f51c61372 | |||
| f5e9a862c2 | |||
| 0c8a2858b9 | |||
| a79e8afbad | |||
| 7b7faef739 | |||
| 95afc09206 | |||
| 23cb9a4291 | |||
| 0a863ba03f | |||
| e923d88398 | |||
| 150f0b3c30 | |||
| fc5f9d03cf | |||
| 498d9cdda4 | |||
| fd8d29a157 | |||
| e2e9a9a531 | |||
| 35fd8b567a | |||
| 4e9648d184 | |||
| 54a0541013 | |||
| 9c0587f755 | |||
| ac06941bcf | |||
| 907415c801 | |||
| 225a56677e | |||
| 944181869d | |||
| 3d3e6cbf20 | |||
| 2158bf8bb8 | |||
| 1b46b6db18 | |||
| f23f3e7f5f | |||
| 9414da5b7e | |||
| 8078abe893 | |||
| 661e800302 | |||
| c960a6e492 | |||
| 2eeb4b5666 | |||
| 8f19cf8d84 | |||
| 50a9ed190d | |||
| e437980659 | |||
| d3ab1ea5f2 | |||
| fe7b26e403 | |||
| 70e4c7fbab | |||
| a25f74238e | |||
| 61ad0f8b90 | |||
| a86c3214c2 | |||
| 706cfecaee | |||
| 63cb3f4fa8 | |||
| 787cd875b0 | |||
| 1a151f2dae | |||
| 6b112e6c6c | |||
| 7d58b798c6 | |||
| ab1c84eed9 | |||
| 103a44f690 | |||
| de9092eb23 | |||
| 0b0b34565a | |||
| b17a7e062f | |||
| 7c976c3453 | |||
| eb6bce0ff5 | |||
| 30e486a41c | |||
| a73d5435d8 | |||
| b9d3fe3b44 | |||
| c9ecf9e40b | |||
| 2f213de593 | |||
| ec32591dd7 | |||
| 02b7f5e4f7 | |||
| 31bf694599 | |||
| bdfb60f3e3 | |||
| e99b92a1f0 | |||
| 5c24de5ee8 | |||
| fad5def327 | |||
| de2eab6820 | |||
| ea1aeb3d45 | |||
| 35f0663ce8 | |||
| 2eeeabb760 | |||
| 87372893ea | |||
| 631f60d1f9 | |||
| 390efd51e7 | |||
| c2cedd1954 | |||
| 3c4deaa52b | |||
| b5eea233df | |||
| c0cb6040f8 | |||
| fb901f7eda | |||
| 17da2c392e | |||
| 6cf0100fe4 | |||
| d2137928e8 | |||
| 1d4783c01c | |||
| 9ee99c7cc0 | |||
| 5b8a403220 | |||
| 9c8f2dab7d | |||
| 9f1a78ca74 | |||
| d45440eda4 | |||
| eb7c593fda | |||
| 48802fd7ed | |||
| 5cea3576cf | |||
| 913bb6ad14 | |||
| 79f1cd462e | |||
| 4ea3cafdc8 | |||
| 94b9a3ef70 | |||
| c5afb88d9a | |||
| f58f9c892e | |||
| 7f33cbeb6c | |||
| dd630ef955 | |||
| 13fd2b1aa3 |
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.gemini/skills
|
||||
@@ -1,178 +0,0 @@
|
||||
---
|
||||
name: adev-writing-guide
|
||||
description: Comprehensive writing guide for Angular documentation (adev). Covers Google Technical Writing standards, Angular-specific markdown extensions, code blocks, and components. You MUST use this skill any time you plan to create, edit, or review documentation files in `adev/` or `adev/src/content`.
|
||||
---
|
||||
|
||||
# Angular Documentation (adev) Writing Guide
|
||||
|
||||
This skill provides comprehensive guidelines for authoring content in `adev/src/content`. It combines Google's technical writing standards with Angular-specific markdown conventions, components, and best practices.
|
||||
|
||||
## I. Google Technical Writing Guidelines
|
||||
|
||||
### Tone and Content
|
||||
|
||||
- **Be conversational and friendly:** Maintain a helpful yet professional tone. Avoid being overly casual.
|
||||
- **Write accessibly:** Ensure documentation is understandable to a diverse global audience, including non-native English speakers.
|
||||
- **Audience-first:** Focus on what the user needs to do, not just what the system does.
|
||||
- **Avoid pre-announcing:** Do not mention unreleased features or make unsupported claims.
|
||||
- **Use descriptive link text:** Link text should clearly indicate the destination (e.g., avoid "click here").
|
||||
|
||||
### Language and Grammar
|
||||
|
||||
- **Use second person ("you"):** Address the reader directly.
|
||||
- **Prefer active voice:** Clearly state who or what is performing the action (e.g., "The system generates a token" vs "A token is generated").
|
||||
- **Standard American English:** Use standard American spelling and punctuation.
|
||||
- **Conditional clauses first:** Place "if" or "when" clauses before the instruction (e.g., "If you encounter an error, check the logs").
|
||||
- **Define terms:** Introduce new or unfamiliar terms/acronyms upon first use.
|
||||
- **Consistent terminology:** Use the same term for the same concept throughout the document.
|
||||
- **Conciseness:** Aim for one idea per sentence. Keep sentences short.
|
||||
|
||||
### Formatting and Organization
|
||||
|
||||
- **Sentence case for headings:** Capitalize only the first word and proper nouns in titles and headings.
|
||||
- **Lists:**
|
||||
- **Numbered lists:** Use for sequential steps or prioritized items.
|
||||
- **Bulleted lists:** Use for unordered collections of items.
|
||||
- **Description lists:** Use for term-definition pairs.
|
||||
- **Serial commas:** Use the Oxford comma (comma before the last item in a list of three or more).
|
||||
- **Code formatting:** Use code font for code-related text (filenames, variables, commands).
|
||||
- **UI Elements:** formatting user interface elements in **bold**.
|
||||
- **Date formatting:** Use unambiguous formats (e.g., "September 4, 2024" rather than "9/4/2024").
|
||||
- **Structure:** Use logical hierarchy with clear introductions and navigation. Headings should be task-based where possible.
|
||||
|
||||
### Images and Code Samples
|
||||
|
||||
- **Images:** Use simple, clear illustrations to enhance understanding.
|
||||
- **Captions:** Write captions that support the image.
|
||||
- **Code Samples:**
|
||||
- Ensure code is correct and builds without errors.
|
||||
- Follow language-specific conventions.
|
||||
- **Comments:** Focus on _why_, not _what_. Avoid commenting on obvious code.
|
||||
|
||||
### Reference Hierarchy
|
||||
|
||||
1. Project-specific style guidelines (if any exist in `CONTRIBUTING.md` or similar).
|
||||
2. Google Developer Documentation Style Guide.
|
||||
3. Merriam-Webster (spelling).
|
||||
4. Chicago Manual of Style (non-technical).
|
||||
5. Microsoft Writing Style Guide (technical).
|
||||
|
||||
---
|
||||
|
||||
## II. Angular Documentation Specifics
|
||||
|
||||
### Code Blocks
|
||||
|
||||
Use the appropriate language identifier for syntax highlighting:
|
||||
|
||||
- **TypeScript (Angular):** Use `angular-ts` when TypeScript code examples contain inline templates.
|
||||
- **HTML (Angular):** Use `angular-html` for Angular templates.
|
||||
- **TypeScript (Generic):** Use `ts` for plain TypeScript.
|
||||
- **HTML (Generic):** Use `html` for plain HTML.
|
||||
- **Shell/Terminal:** Use `shell` or `bash`.
|
||||
- **Mermaid Diagrams:** Use `mermaid`.
|
||||
|
||||
#### Attributes
|
||||
|
||||
You can enhance code blocks with attributes in curly braces `{}` after the language identifier:
|
||||
|
||||
- `header="Title"`: Adds a title to the code block.
|
||||
- `linenums`: Enables line numbering.
|
||||
- `highlight="[1, 3-5]"`: Highlights specific lines.
|
||||
- `hideCopy`: Hides the copy button.
|
||||
- `prefer`: Marks code as a preferred example (green border/check).
|
||||
- `avoid`: Marks code as an example to avoid (red border/cross).
|
||||
|
||||
**Example:**
|
||||
|
||||
````markdown
|
||||
```angular-ts {header:"My Component", linenums, highlight="[2]"}
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: '<h1>Hello</h1>',
|
||||
})
|
||||
export class App {}
|
||||
```
|
||||
````
|
||||
|
||||
#### `<docs-code>` Component
|
||||
|
||||
For more advanced code block features, use the `<docs-code>` component:
|
||||
|
||||
- `path`: Path to a source file (e.g., `adev/src/content/examples/...`).
|
||||
- `header`: Custom header text.
|
||||
- `language`: Language identifier (e.g., `angular-ts`).
|
||||
- `linenums`: Boolean attribute.
|
||||
- `highlight`: Array of line numbers/ranges (e.g., `[[3,7], 9]`).
|
||||
- `diff`: Path to diff file.
|
||||
- `visibleLines`: Range of lines to show initially (collapsible).
|
||||
- `region`: Region to extract from source file.
|
||||
- `preview`: Boolean. Renders a live preview (StackBlitz). _Only works with standalone examples._
|
||||
- `hideCode`: Boolean. Collapses code by default.
|
||||
|
||||
**Multifile Example:**
|
||||
|
||||
```html
|
||||
<docs-code-multifile path="..." preview>
|
||||
<docs-code path="..." />
|
||||
<docs-code path="..." />
|
||||
</docs-code-multifile>
|
||||
```
|
||||
|
||||
### Alerts / Admonitions
|
||||
|
||||
Use specific keywords followed by a colon for alerts. These render as styled blocks.
|
||||
|
||||
- `NOTE:` For ancillary information.
|
||||
- `TIP:` For helpful hints or shortcuts.
|
||||
- `IMPORTANT:` For crucial information.
|
||||
- `CRITICAL:` For warnings about potential data loss or severe issues.
|
||||
- `TODO`: For incomplete documentation.
|
||||
- `QUESTION:` To pose a question to the reader.
|
||||
- `SUMMARY:` For section summaries.
|
||||
- `TLDR:` For concise summaries.
|
||||
- `HELPFUL:` For best practices.
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||
TIP: Use `ng serve` to run your application locally.
|
||||
```
|
||||
|
||||
### Custom Components
|
||||
|
||||
- **Cards (`<docs-card>`):**
|
||||
- Must be inside `<docs-card-container>`.
|
||||
- Attributes: `title`, `link`, `href`.
|
||||
- **Callouts (`<docs-callout>`):**
|
||||
- Attributes: `title`, `important`, `critical`.
|
||||
- **Pills (`<docs-pill>`):**
|
||||
- Must be inside `<docs-pill-row>`.
|
||||
- Attributes: `title`, `href`.
|
||||
- **Steps / Workflow (`<docs-step>`):**
|
||||
- Must be inside `<docs-workflow>`.
|
||||
- Attributes: `title`.
|
||||
- **Tabs (`<docs-tab>`):**
|
||||
- Must be inside `<docs-tab-group>`.
|
||||
- Attributes: `label`.
|
||||
- **Videos (`<docs-video>`):**
|
||||
- Attributes: `src` (YouTube embed URL), `alt`.
|
||||
|
||||
### Images
|
||||
|
||||
Use standard markdown syntax with optional attributes for sizing and loading behavior.
|
||||
|
||||
- `#small`, `#medium`: Append to image URL for sizing.
|
||||
- `{loading: 'lazy'}`: Add attribute for lazy loading.
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
- Use markdown headers (`#`, `##`, `###`).
|
||||
- Ensure a logical hierarchy (don't skip levels).
|
||||
- `h2` and `h3` are most common for content structure.
|
||||
@@ -1,135 +0,0 @@
|
||||
---
|
||||
name: PR Review
|
||||
description: Guidelines and tools for reviewing pull requests in the Angular repository.
|
||||
---
|
||||
|
||||
# PR Review Guidelines
|
||||
|
||||
When reviewing a pull request for the `angular` repository, follow these essential guidelines to ensure high-quality contributions:
|
||||
|
||||
1. **Context & Ecosystem**:
|
||||
- Keep in mind that this is the core Angular framework. Changes here can impact millions of developers.
|
||||
- Be mindful of backwards compatibility. Breaking changes require strict approval processes and deprecation periods.
|
||||
|
||||
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.
|
||||
- **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.
|
||||
- **Testing**: Ensure all new logic has comprehensive tests, including edge cases. **Do NOT run tests locally** as part of your review process. CI handles this automatically, and running tests locally is redundant and inefficient.
|
||||
- **API Design**: Ensure new public APIs are well-designed, consistent with existing APIs, and properly documented.
|
||||
- **Payload Size**: Pay attention to the impact of changes on the final client payload size.
|
||||
|
||||
3. **Execution Workflow**:
|
||||
Determine the appropriate review method. If the user explicitly asks for a `remote` or `local` review in their request, that takes precedence (e.g. "leave comments on the PR" implies `remote`). Otherwise, use the GitHub MCP or available scripts to determine if the review should be `local` or `remote`.
|
||||
|
||||
**Common Review Practices (Applies to both Local and Remote)**
|
||||
- **Preparation & Checklist**:
|
||||
- First, create a task list (e.g., in `task.md`) that you can easily reference containing **all** the review requirements from the "Key Focus Areas" section (Commit Messages, Performance, Testing, etc.), along with any specific review notes or requests from the user.
|
||||
- Before doing an in-depth review, expand this list into more detailed items of what you plan to explore and verify in the PR.
|
||||
- As you conduct the review, check off items in this list, adding your assessment or findings underneath each item.
|
||||
- At the end of your review, refer back to the checklist to ensure every single requirement was completely verified.
|
||||
- **Fetch PR Metadata Safely**: When you need to read the PR description or context, do NOT use `gh pr view <PR_NUMBER>` by itself, as its default GraphQL query may fail due to lack of `read:org` and `read:discussion` token scopes. Instead, use `read_url_content` on the PR URL or use `gh pr view <PR_NUMBER> --json title,body,state,author`.
|
||||
- **Check Existing Comments First**: Before formulating feedback, use the GitHub MCP or available scripts to fetch existing comments on the PR. Review this feedback to avoid duplicate comments, and incorporate its insights into your own review process.
|
||||
- **Constructive Feedback**: Provide clear, actionable, and polite feedback. Explain the _why_ behind your suggestions or edits. Do **NOT** leave inline comments purely to praise, agree with, or acknowledge a correct implementation detail, as this clutters the review. If you want to praise the PR, do so in the single general PR comment.
|
||||
|
||||
**A. Local Code Review (If the PR is owned by the author requesting the review)**
|
||||
- **Checkout**: Check out the PR branch locally (if it doesn't already exist, fetch it). If checking out the branch fails due to a worktree claim (e.g. "fatal: '<branch>' is already used by worktree at '<path>'"), do the review in that directory.
|
||||
- **Review & Edit**: Execute the review directly on the code. Instead of adding inline PR comments for suggestions, format the codebase or apply the edits directly to the files.
|
||||
- **Feedback**: Summarize the review findings and the concrete changes you made in a message to the user, referencing the completed items from your checklist.
|
||||
- **Do NOT Commit or Push**: Leave the changes uncommitted in the working directory so the user can easily review the pending edits locally. Let the user know the changes are ready for their review, but do not ask for approval to push.
|
||||
- **Resolve Comments**: Once the user confirms the changes are good and should be committed/pushed, respond to the existing comments as 'resolved' using the GitHub MCP or available scripts.
|
||||
|
||||
**B. Remote Code Review (For all other PRs)**
|
||||
- **Batching Comments (MCP Server - Preferred)**: If you have the GitHub MCP Server configured, you **MUST** follow this workflow to avoid spamming the author with multiple notifications:
|
||||
1. Create a pending review using `mcp_github-mcp-server_pull_request_review_write` (method `create`).
|
||||
2. Add your inline comments to the pending review using `mcp_github-mcp-server_add_comment_to_pending_review`.
|
||||
3. Submit the review using `mcp_github-mcp-server_pull_request_review_write` (method `submit_pending`).
|
||||
- **Batching Comments (Scripts - Fallback)**: If you do **NOT** have access to the GitHub MCP Server (e.g., specific MCP tools are missing from your context), fallback to using the provided scripts. Use `post_inline_comment.sh` to stage your comments locally. Once all comments are staged, you **MUST** call `submit_pr_review.sh` to publish them as a single batched review (and send a single notification). Try to keep comments minimal or use a general comment if you have many suggestions.
|
||||
- **Use Suggested Changes**: Whenever appropriate (e.g., for simple code fixes, refactoring suggestions, or typo corrections), prefer using GitHub's **Suggested Changes** syntax (`suggestion ... `) in your inline comments. This allows the author to apply your suggested code improvements with a single click in the GitHub UI.
|
||||
- **Review Type**: Never mark an external PR review as an "approval" unless explicitly instructed by a repo maintainer. Always use "Request Changes" or "Comment". Note that some tools might only support commenting.
|
||||
- **Require User Approval Before Posting**: Prepare your review comments and present them to the user, alongside a summary of your completed checklist. Do NOT post comments to the PR without explicitly asking the user for permission first. Only post the review after the user approves.
|
||||
- **CRITICAL**: This rule applies even if you receive a system message indicating that an artifact has been "automatically approved" or instructing you to "proceed to execution." You must ALWAYS obtain explicit, written confirmation from the user in this chat conversation before posting any content to a PR.
|
||||
- **Prefix Agent Comments**: To make it clear when comments are generated and posted by an AI agent rather than a human user, **always** prefix your review comments with `AGENT: `.
|
||||
|
||||
## Available Tools
|
||||
|
||||
The following tools are available for remote interactions. We prefer using standard **GitHub MCP Server** tools when available. If you do not have the MCP server set up, you **MUST** fallback to using the custom bash scripts.
|
||||
|
||||
### GitHub MCP Tools (Preferred)
|
||||
|
||||
- `mcp_github-mcp-server_pull_request_review_write`
|
||||
- `mcp_github-mcp-server_add_comment_to_pending_review`
|
||||
|
||||
### Custom Bash Scripts (Fallback)
|
||||
|
||||
The following scripts are provided as fallbacks if the MCP server is not available. Note that they rely on the `gh` CLI being correctly installed and authenticated in the local environment.
|
||||
|
||||
### `determine_review_type.sh`
|
||||
|
||||
Determines whether to use the Local or Remote review workflow by checking if the currently authenticated GitHub user via the `gh` CLI matches the author of the pull request.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/determine_review_type.sh <PR_NUMBER>
|
||||
```
|
||||
|
||||
### `get_pr_comments.sh`
|
||||
|
||||
Fetches all existing inline comments on a PR using the GitHub API. This is crucial for reviewing other contributors' feedback and avoiding duplicate comments. It outputs JSON containing the `id`, `path`, `line`, `body`, and `user` for each comment.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/get_pr_comments.sh <PR_NUMBER>
|
||||
```
|
||||
|
||||
### `reply_pr_comment.sh`
|
||||
|
||||
Replies to an existing PR comment thread. This is useful for marking comments as resolved after addressing them in a local code review. Note that the `COMMENT_ID` must be the ID of the top-level comment in the thread.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/reply_pr_comment.sh <PR_NUMBER> <COMMENT_ID> <REPLY_BODY>
|
||||
```
|
||||
|
||||
### `post_inline_comment.sh`
|
||||
|
||||
The GitHub CLI `gh pr review` command does not natively support adding inline comments to specific lines of code via its standard flags. This script wraps the GitHub API to stage comments locally. They will not be published until you call `submit_pr_review.sh`.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/post_inline_comment.sh <PR_NUMBER> <FILE_PATH> <LINE_NUMBER> <COMMENT_BODY>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/post_inline_comment.sh 12345 "packages/core/src/render3/instructions/element.ts" 42 "AGENT: Consider the performance implications here."
|
||||
```
|
||||
|
||||
### `submit_pr_review.sh`
|
||||
|
||||
Submits all locally staged inline comments as a single batched review via the GitHub Pull Request Reviews API.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/submit_pr_review.sh <PR_NUMBER> <EVENT_TYPE> [BODY]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `EVENT_TYPE`: Must be `COMMENT`, `APPROVE`, or `REQUEST_CHANGES`. Never use `APPROVE` for external PRs.
|
||||
- `BODY`: (Optional) A general summary comment for the review.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
.agent/skills/pr_review/scripts/submit_pr_review.sh 12345 COMMENT "AGENT: I have left a few inline suggestions for your consideration."
|
||||
```
|
||||
@@ -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).
|
||||
@@ -1,7 +0,0 @@
|
||||
# Router PR Review Guidelines
|
||||
|
||||
When reviewing pull requests that modify the Angular Router (`packages/router`), pay special attention to the following:
|
||||
|
||||
- **Timing Sensitivity**: The router is extremely sensitive to timing changes. Any changes that alter the asynchronous timing of navigations, resolvers, or guards are almost always breaking changes and must be scrutinized carefully.
|
||||
- **Testing Practices**: Tests should usually use the `RouterTestingHarness`. Many existing tests are older and do not use this harness. Do not blindly follow the shape of existing tests when writing or reviewing new ones; encourage the use of modern testing utilities.
|
||||
- **Feature Justification**: Changes to router core code should be well-justified. Consider whether the change is proven to be a core developer ask, such as resolving a highly upvoted GitHub issue or addressing a critical bug.
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# determine_review_type.sh <PR_NUMBER>
|
||||
# Determines if the PR should be reviewed locally or remotely based on author.
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: determine_review_type.sh <PR_NUMBER>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER=$1
|
||||
|
||||
# Get current authenticated user
|
||||
CURRENT_USER=$(gh api user -q .login 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Could not determine current GitHub user. Are you logged in to gh?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get PR author
|
||||
PR_AUTHOR=$(gh pr view "$PR_NUMBER" --json author -q .author.login 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Could not retrieve PR information for $PR_NUMBER."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_USER" = "$PR_AUTHOR" ]; then
|
||||
echo "local"
|
||||
else
|
||||
echo "remote"
|
||||
fi
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# get_pr_comments.sh
|
||||
# Fetches existing inline comments on a PR to avoid duplicate reviews.
|
||||
# Usage: ./get_pr_comments.sh <PR_NUMBER>
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Usage: $0 <PR_NUMBER>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$1"
|
||||
|
||||
# Ensure gh cli is installed
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI could not be found. Please install and authenticate."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current repository (e.g., angular/angular)
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
# Fetch comments
|
||||
gh api \
|
||||
--paginate \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPO}/pulls/${PR_NUMBER}/comments" \
|
||||
--jq '.[] | {id: .id, path: .path, line: .line, body: .body, user: .user.login}'
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# post_inline_comment.sh
|
||||
# Adds an inline comment to a specific line in a PR via the GitHub API.
|
||||
# Usage: ./post_inline_comment.sh <PR_NUMBER> <FILE_PATH> <LINE_NUMBER> <COMMENT_BODY>
|
||||
|
||||
if [ "$#" -lt 4 ]; then
|
||||
echo "Usage: $0 <PR_NUMBER> <FILE_PATH> <LINE_NUMBER> <COMMENT_BODY>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$1"
|
||||
FILE_PATH="$2"
|
||||
LINE="$3"
|
||||
BODY="$4"
|
||||
|
||||
# Ensure gh cli is installed
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI could not be found. Please install and authenticate."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current repository (e.g., angular/angular)
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
echo "Staging inline comment for PR #${PR_NUMBER} on ${FILE_PATH}:${LINE}..."
|
||||
|
||||
COMMENT_FILE="/tmp/angular_pr_${PR_NUMBER}_comments.json"
|
||||
if [ ! -f "$COMMENT_FILE" ]; then
|
||||
echo "[]" > "$COMMENT_FILE"
|
||||
fi
|
||||
|
||||
# Append the new comment to the JSON array
|
||||
jq --arg path "${FILE_PATH}" --argjson line "${LINE}" --arg body "${BODY}" \
|
||||
'. += [{"path": $path, "line": $line, "body": $body}]' "$COMMENT_FILE" > "${COMMENT_FILE}.tmp" && mv "${COMMENT_FILE}.tmp" "$COMMENT_FILE"
|
||||
|
||||
echo "Comment successfully staged locally. Remember to call submit_pr_review.sh when finished to publish all comments as a single review!"
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# reply_pr_comment.sh <PR_NUMBER> <COMMENT_ID> <REPLY_BODY>
|
||||
# Replies to an existing PR comment thread. Note: COMMENT_ID must be the ID of the top-level comment in the thread you are replying to.
|
||||
|
||||
if [ "$#" -lt 3 ]; then
|
||||
echo "Usage: reply_pr_comment.sh <PR_NUMBER> <COMMENT_ID> <REPLY_BODY>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$1"
|
||||
COMMENT_ID="$2"
|
||||
BODY="$3"
|
||||
|
||||
# Ensure gh cli is installed
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI could not be found. Please install and authenticate."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current repository (e.g., angular/angular)
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
# Reply to the thread using the provided comment ID
|
||||
gh api \
|
||||
--silent \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/${REPO}/pulls/${PR_NUMBER}/comments/${COMMENT_ID}/replies" \
|
||||
-f body="$BODY"
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# submit_pr_review.sh
|
||||
# Submits a batched PR review using comments previously staged by post_inline_comment.sh
|
||||
# Usage: ./submit_pr_review.sh <PR_NUMBER> <EVENT_TYPE> [BODY]
|
||||
# EVENT_TYPE must be COMMENT, APPROVE, or REQUEST_CHANGES
|
||||
|
||||
if [ "$#" -lt 2 ]; then
|
||||
echo "Usage: $0 <PR_NUMBER> <EVENT_TYPE> [BODY]"
|
||||
echo "EVENT_TYPE must be COMMENT, APPROVE, or REQUEST_CHANGES"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PR_NUMBER="$1"
|
||||
EVENT="$2"
|
||||
BODY="${3:-}"
|
||||
COMMENT_FILE="/tmp/angular_pr_${PR_NUMBER}_comments.json"
|
||||
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "Error: gh CLI could not be found. Please install and authenticate."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
||||
|
||||
# Check if there are staged comments
|
||||
COMMENTS="[]"
|
||||
if [ -f "$COMMENT_FILE" ]; then
|
||||
COMMENTS=$(cat "$COMMENT_FILE")
|
||||
fi
|
||||
|
||||
echo "Submitting review for PR #${PR_NUMBER}..."
|
||||
|
||||
# Create the payload
|
||||
PAYLOAD_FILE="/tmp/angular_pr_${PR_NUMBER}_payload.json"
|
||||
jq -n --arg event "$EVENT" --arg body "$BODY" --argjson comments "$COMMENTS" \
|
||||
'{event: $event, body: $body, comments: $comments}' > "$PAYLOAD_FILE"
|
||||
|
||||
# Post the review using the GitHub Pull Request Reviews API
|
||||
gh api \
|
||||
--method POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"/repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
|
||||
--input "$PAYLOAD_FILE"
|
||||
|
||||
echo "Review submitted successfully!"
|
||||
rm -f "$COMMENT_FILE"
|
||||
rm -f "$PAYLOAD_FILE"
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
name: reference-signal-forms
|
||||
description: Explains the mental model and architecture of the code under `packages/forms/signals`. You MUST use this skill any time you plan to work with code in `packages/forms/signals`
|
||||
---
|
||||
|
||||
# Signal Forms Architecture
|
||||
|
||||
The `packages/forms/signals` directory contains the signal-based forms API for Angular.
|
||||
This system differs significantly from the existing Reactive and Template-driven forms.
|
||||
|
||||
## Mental Model
|
||||
|
||||
1. **Model-Driven**: The system is built around a `WritableSignal<T>` which serves as the **single source of truth**.
|
||||
Unlike Reactive Forms where the `FormControl` holds the value, here the `Signal` holds the value.
|
||||
The form is merely a _view_ or _projection_ of that signal, adding form-specific state (validity, dirty, touched).
|
||||
|
||||
2. **Proxy-Based Traversal**: The form API (`form(signal)`) returns a `FieldTree`. This object is a **Proxy**.
|
||||
It allows accessing nested fields (e.g., `myForm.user.name`) without manually creating control groups.
|
||||
Accessing a property on the proxy lazily resolves or creates the corresponding `FieldNode`.
|
||||
|
||||
3. **Schema-Based Logic**: Validation, disabled state, and other metadata are defined separately via **Schemas**.
|
||||
Schemas are applied to the form structure using functions like `apply`, `applyEach` (for arrays), and `applyWhen`.
|
||||
This separates the _structure_ of the data from the _rules_ governing it.
|
||||
|
||||
4. **Directives as Glue**: The `[formField]` directive binds a DOM element (native input or custom control) to a `FieldNode`.
|
||||
It handles:
|
||||
- Syncing the value between the DOM and the Signal.
|
||||
- Reflecting state (valid, touched, etc.) to the UI.
|
||||
- Handling user interaction events (blur, input).
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. `FieldNode` (`src/field/node.ts`)
|
||||
|
||||
The central internal class representing a single field in the form graph. It aggregates several state managers:
|
||||
|
||||
- `structure`: Manages parent/child relationships and signal slicing.
|
||||
- `validationState`: Computes `valid`, `invalid`, `errors` signals.
|
||||
- `nodeState`: Tracks `touched`, `dirty`, and derived logical state.
|
||||
- `metadataState`: Stores metadata like `min`, `max`, `required`.
|
||||
- `submitState`: Tracks submission status and server errors.
|
||||
|
||||
### 2. `FieldValidationState` (`src/field/validation.ts`)
|
||||
|
||||
Implements `ValidationState` and 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`)
|
||||
|
||||
The bridge between the `FieldNode` and the DOM.
|
||||
|
||||
- Selector: `[formField]`
|
||||
- It supports:
|
||||
- **Native Elements**: `<input>`, `<select>`, `<textarea>`.
|
||||
- **Custom Controls**: Components implementing `FormValueControl` or `FormCheckboxControl`.
|
||||
- **Legacy Interop**: Components implementing `ControlValueAccessor` (via `InteropNgControl`).
|
||||
|
||||
### 4. `Schema` (`src/api/structure.ts` & `src/api/rules`)
|
||||
|
||||
Defines the behavior.
|
||||
|
||||
- Created via `schema(fn)`.
|
||||
- Applied via `apply(path, schema)`.
|
||||
- Rules include validators (`required`, `pattern`, `min`, `max`) and state modifiers (`disabled`, `hidden`).
|
||||
|
||||
## Data Flow
|
||||
|
||||
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.
|
||||
|
||||
## Usage Example (Conceptual)
|
||||
|
||||
```typescript
|
||||
// 1. Define Model
|
||||
const user = signal({name: '', age: 0});
|
||||
|
||||
// 2. Define Schema
|
||||
const userRules = schema((u) => {
|
||||
required(u.name);
|
||||
min(u.age, 18);
|
||||
});
|
||||
|
||||
// 3. Create Form
|
||||
const userForm = form(user, userRules); // OR apply(userForm, userRules)
|
||||
|
||||
// 4. Bind in Template
|
||||
// <input [formField]="userForm.name">
|
||||
```
|
||||
|
||||
## Important Files
|
||||
|
||||
- `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.
|
||||
|
||||
## Supplemental Information
|
||||
|
||||
- [Compiler & Core Integration](references/integration.md): Details how `[formField]` hooks into type-checking and the runtime.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Signal Forms Integration: Compiler & Core
|
||||
|
||||
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)
|
||||
|
||||
The `packages/compiler` package contains specific logic to support `[formField]`. This is primarily handled in `src/typecheck/ops/signal_forms.ts`.
|
||||
|
||||
### Key Mechanisms:
|
||||
|
||||
- **Detection**: The compiler identifies a directive as a "Field Directive" if it has the `ɵNgFieldDirective` property or comes from `@angular/forms/signals`.
|
||||
- **Synthetic Binding Expansion**: When you write `<input [formField]="mySignal" />`, the type checker doesn't just check `formField`. It synthetically expands this into a set of bindings for validation:
|
||||
- `[value]="mySignal()"` (or `checked` for checkboxes)
|
||||
- `[disabled]="mySignal.disabled()"`
|
||||
- `[required]="mySignal.required()"`
|
||||
- ...and so on.
|
||||
- This ensures that `mySignal` (the `FieldNode`) has all the necessary properties to drive the form control.
|
||||
- **Conflict Detection**: It actively prevents "double binding". If you bind `[formField]`, you are _banned_ from also binding `[value]`, `[disabled]`, `[required]`, etc., as the signal form should be the single source of truth.
|
||||
- **Element Type Validation**:
|
||||
- **Native Elements**: Checks that the signal's value type matches the element type (e.g., `<input type="checkbox">` requires a `boolean` signal).
|
||||
- **Custom Controls**: Detects if a custom component is a "Form Value Control" (has a `value` input/output) or "Form Checkbox Control" (has a `checked` input/output) and validates against that.
|
||||
|
||||
### Relevant Files:
|
||||
|
||||
- `packages/compiler/src/typecheck/ops/signal_forms.ts`: The core Signal Forms type-checking logic, including `TcbNativeFieldOp`.
|
||||
|
||||
## 2. Core Runtime Integration
|
||||
|
||||
The `packages/core` package provides the low-level instructions that power the `FormField` directive. This allows it to do things normal directives cannot, like efficiently syncing state without change detection overhead for every property.
|
||||
|
||||
### 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:
|
||||
- `ɵɵ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:
|
||||
- **Access the Element**: Get direct access to the native element or component instance.
|
||||
- **Set Inputs**: Write directly to inputs of _other directives_ on the same node (e.g., setting the `value` input of a custom control).
|
||||
- **Listen to Outputs**: Subscribe to outputs of other directives (e.g., `valueChange`).
|
||||
- **Bypass Templates**: It effectively acts as a "meta-directive" that manages the bindings for you, bypassing the need for explicit template syntax for every property.
|
||||
|
||||
### 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.
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
description: Find and fix flaky tests in the repository
|
||||
---
|
||||
|
||||
Investigate flaky tests in the repo and propose fixes to improve stability.
|
||||
High-level process:
|
||||
|
||||
1. Run tests in the repo to look for flakes.
|
||||
- Consider using Bazel's `--runs_per_test` flag to easily find
|
||||
flakes.
|
||||
- Be cognizant of not exhausting all the resources on the current
|
||||
machine, run a subset of tests at a time such as
|
||||
`bazel test //packages/core/...`.
|
||||
2. Once you find some flakes, focus on one at a time.
|
||||
3. Create a new branch named `flakes/${relevantNameFromTest}`.
|
||||
4. Reproduce the flake to the best of your ability.
|
||||
- Consider using `--test_env JASMINE_RANDOM_SEED=1234` to
|
||||
replicate the broken test ordering.
|
||||
5. Debug the test to understand the failure mode.
|
||||
- Consider temporarily disabling / skipping other tests with `xit`
|
||||
and `fit` to narrow down where the flake might be coming from if
|
||||
multiple tests are influencing each other.
|
||||
- Consider temporarily ignoring Firefox tests with
|
||||
`--test_tag_filters -firefox` if the flake does not appear to be
|
||||
browser specific.
|
||||
- Consider using `--test_sharding_strategy disabled` to run the
|
||||
test in a single shard.
|
||||
- Try to understand why the test was _flaky_, not just why it
|
||||
_failed_. Understanding the inconsistency is important to
|
||||
finding the correct fix.
|
||||
6. Attempt a fix and validate with `--runs_per_test`.
|
||||
- Iterate on the fix until you have something which appears to
|
||||
work.
|
||||
- If you find yourself stuck and not making meaningful progress,
|
||||
note down what you've learned/where you're struggling, commit
|
||||
what you have, look for another flake to fix, and continue. At
|
||||
the end, surface to the user what you failed to fix.
|
||||
- Don't try to make significant changes to Angular's runtime
|
||||
behavior, focus just on making the test pass/fail consistently.
|
||||
7. Commit the change with relevant details in the commit message and
|
||||
move on to the next test.
|
||||
- Be sure to include your theory of why the test was flaky and
|
||||
how this fix eliminates or reduces that flakiness.
|
||||
8. Iterate as many times as the user requests you to (default 5
|
||||
branches if not otherwise specified).
|
||||
9. Once you can't find any flaky tests or have iterated as many times
|
||||
as requested, stop and inform the user what you found and fixed.
|
||||
|
||||
Additional notes:
|
||||
|
||||
- Multiple fixes including the same/related files can go in the same
|
||||
commit or multiple commits on the same branch.
|
||||
- Distinct test fixes should go in different branches, make a new one
|
||||
for each investigation.
|
||||
- You may push these branches to `origin`, but do not create PRs for
|
||||
them.
|
||||
@@ -159,6 +159,16 @@ common --incompatible_allow_tags_propagation
|
||||
# )
|
||||
build --nosandbox_default_allow_network
|
||||
|
||||
##################################
|
||||
# Saucelabs tests settings #
|
||||
# Turn on these settings with #
|
||||
# --config=saucelabs #
|
||||
##################################
|
||||
|
||||
# For saucelabs tests we don't want to enable flaky test attempts. Karma has its own integrated
|
||||
# retry mechanism and we do not want to retry unnecessarily if Karma already tried multiple times.
|
||||
test:saucelabs --flaky_test_attempts=1
|
||||
|
||||
################
|
||||
# Flag Aliases #
|
||||
################
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
8.8.0
|
||||
8.6.0
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Language: JavaScript
|
||||
BasedOnStyle: Google
|
||||
ColumnLimit: 100
|
||||
+1
-2
@@ -7,5 +7,4 @@ code_review:
|
||||
help: false
|
||||
summary: false
|
||||
code_review: false
|
||||
ignore_patterns:
|
||||
- pnpm-lock.yaml
|
||||
ignore_patterns: []
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
name: adev-writing-guide
|
||||
description: Comprehensive writing guide for Angular documentation (adev). Covers Google Technical Writing standards, Angular-specific markdown extensions, code blocks, and components. Use when authoring or reviewing content in adev/src/content.
|
||||
---
|
||||
|
||||
# Angular Documentation (adev) Writing Guide
|
||||
|
||||
This skill provides comprehensive guidelines for authoring content in `adev/src/content`. It combines Google's technical writing standards with Angular-specific markdown conventions, components, and best practices.
|
||||
|
||||
## I. Google Technical Writing Guidelines
|
||||
|
||||
### Tone and Content
|
||||
|
||||
- **Be conversational and friendly:** Maintain a helpful yet professional tone. Avoid being overly casual.
|
||||
- **Write accessibly:** Ensure documentation is understandable to a diverse global audience, including non-native English speakers.
|
||||
- **Audience-first:** Focus on what the user needs to do, not just what the system does.
|
||||
- **Avoid pre-announcing:** Do not mention unreleased features or make unsupported claims.
|
||||
- **Use descriptive link text:** Link text should clearly indicate the destination (e.g., avoid "click here").
|
||||
|
||||
### Language and Grammar
|
||||
|
||||
- **Use second person ("you"):** Address the reader directly.
|
||||
- **Prefer active voice:** Clearly state who or what is performing the action (e.g., "The system generates a token" vs "A token is generated").
|
||||
- **Standard American English:** Use standard American spelling and punctuation.
|
||||
- **Conditional clauses first:** Place "if" or "when" clauses before the instruction (e.g., "If you encounter an error, check the logs").
|
||||
- **Define terms:** Introduce new or unfamiliar terms/acronyms upon first use.
|
||||
- **Consistent terminology:** Use the same term for the same concept throughout the document.
|
||||
- **Conciseness:** Aim for one idea per sentence. Keep sentences short.
|
||||
|
||||
### Formatting and Organization
|
||||
|
||||
- **Sentence case for headings:** Capitalize only the first word and proper nouns in titles and headings.
|
||||
- **Lists:**
|
||||
- **Numbered lists:** Use for sequential steps or prioritized items.
|
||||
- **Bulleted lists:** Use for unordered collections of items.
|
||||
- **Description lists:** Use for term-definition pairs.
|
||||
- **Serial commas:** Use the Oxford comma (comma before the last item in a list of three or more).
|
||||
- **Code formatting:** Use code font for code-related text (filenames, variables, commands).
|
||||
- **UI Elements:** formatting user interface elements in **bold**.
|
||||
- **Date formatting:** Use unambiguous formats (e.g., "September 4, 2024" rather than "9/4/2024").
|
||||
- **Structure:** Use logical hierarchy with clear introductions and navigation. Headings should be task-based where possible.
|
||||
|
||||
### Images and Code Samples
|
||||
|
||||
- **Images:** Use simple, clear illustrations to enhance understanding.
|
||||
- **Captions:** Write captions that support the image.
|
||||
- **Code Samples:**
|
||||
- Ensure code is correct and builds without errors.
|
||||
- Follow language-specific conventions.
|
||||
- **Comments:** Focus on _why_, not _what_. Avoid commenting on obvious code.
|
||||
|
||||
### Reference Hierarchy
|
||||
|
||||
1. Project-specific style guidelines (if any exist in `CONTRIBUTING.md` or similar).
|
||||
2. Google Developer Documentation Style Guide.
|
||||
3. Merriam-Webster (spelling).
|
||||
4. Chicago Manual of Style (non-technical).
|
||||
5. Microsoft Writing Style Guide (technical).
|
||||
|
||||
---
|
||||
|
||||
## II. Angular Documentation Specifics
|
||||
|
||||
### Code Blocks
|
||||
|
||||
Use the appropriate language identifier for syntax highlighting:
|
||||
|
||||
- **TypeScript (Angular):** Use `angular-ts` when TypeScript code examples contain inline templates.
|
||||
- **HTML (Angular):** Use `angular-html` for Angular templates.
|
||||
- **TypeScript (Generic):** Use `ts` for plain TypeScript.
|
||||
- **HTML (Generic):** Use `html` for plain HTML.
|
||||
- **Shell/Terminal:** Use `shell` or `bash`.
|
||||
- **Mermaid Diagrams:** Use `mermaid`.
|
||||
|
||||
#### Attributes
|
||||
|
||||
You can enhance code blocks with attributes in curly braces `{}` after the language identifier:
|
||||
|
||||
- `header="Title"`: Adds a title to the code block.
|
||||
- `linenums`: Enables line numbering.
|
||||
- `highlight="[1, 3-5]"`: Highlights specific lines.
|
||||
- `hideCopy`: Hides the copy button.
|
||||
- `prefer`: Marks code as a preferred example (green border/check).
|
||||
- `avoid`: Marks code as an example to avoid (red border/cross).
|
||||
|
||||
**Example:**
|
||||
|
||||
````markdown
|
||||
```angular-ts {header:"My Component", linenums, highlight="[2]"}
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: '<h1>Hello</h1>',
|
||||
})
|
||||
export class App {}
|
||||
```
|
||||
````
|
||||
|
||||
#### `<docs-code>` Component
|
||||
|
||||
For more advanced code block features, use the `<docs-code>` component:
|
||||
|
||||
- `path`: Path to a source file (e.g., `adev/src/content/examples/...`).
|
||||
- `header`: Custom header text.
|
||||
- `language`: Language identifier (e.g., `angular-ts`).
|
||||
- `linenums`: Boolean attribute.
|
||||
- `highlight`: Array of line numbers/ranges (e.g., `[[3,7], 9]`).
|
||||
- `diff`: Path to diff file.
|
||||
- `visibleLines`: Range of lines to show initially (collapsible).
|
||||
- `region`: Region to extract from source file.
|
||||
- `preview`: Boolean. Renders a live preview (StackBlitz). _Only works with standalone examples._
|
||||
- `hideCode`: Boolean. Collapses code by default.
|
||||
|
||||
**Multifile Example:**
|
||||
|
||||
```html
|
||||
<docs-code-multifile path="..." preview>
|
||||
<docs-code path="..." />
|
||||
<docs-code path="..." />
|
||||
</docs-code-multifile>
|
||||
```
|
||||
|
||||
### Alerts / Admonitions
|
||||
|
||||
Use specific keywords followed by a colon for alerts. These render as styled blocks.
|
||||
|
||||
- `NOTE:` For ancillary information.
|
||||
- `TIP:` For helpful hints or shortcuts.
|
||||
- `IMPORTANT:` For crucial information.
|
||||
- `CRITICAL:` For warnings about potential data loss or severe issues.
|
||||
- `TODO`: For incomplete documentation.
|
||||
- `QUESTION:` To pose a question to the reader.
|
||||
- `SUMMARY:` For section summaries.
|
||||
- `TLDR:` For concise summaries.
|
||||
- `HELPFUL:` For best practices.
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||
TIP: Use `ng serve` to run your application locally.
|
||||
```
|
||||
|
||||
### Custom Components
|
||||
|
||||
- **Cards (`<docs-card>`):**
|
||||
- Must be inside `<docs-card-container>`.
|
||||
- Attributes: `title`, `link`, `href`.
|
||||
- **Callouts (`<docs-callout>`):**
|
||||
- Attributes: `title`, `important`, `critical`.
|
||||
- **Pills (`<docs-pill>`):**
|
||||
- Must be inside `<docs-pill-row>`.
|
||||
- Attributes: `title`, `href`.
|
||||
- **Steps / Workflow (`<docs-step>`):**
|
||||
- Must be inside `<docs-workflow>`.
|
||||
- Attributes: `title`.
|
||||
- **Tabs (`<docs-tab>`):**
|
||||
- Must be inside `<docs-tab-group>`.
|
||||
- Attributes: `label`.
|
||||
- **Videos (`<docs-video>`):**
|
||||
- Attributes: `src` (YouTube embed URL), `alt`.
|
||||
|
||||
### Images
|
||||
|
||||
Use standard markdown syntax with optional attributes for sizing and loading behavior.
|
||||
|
||||
- `#small`, `#medium`: Append to image URL for sizing.
|
||||
- `{loading: 'lazy'}`: Add attribute for lazy loading.
|
||||
|
||||
**Example:**
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
- Use markdown headers (`#`, `##`, `###`).
|
||||
- Ensure a logical hierarchy (don't skip levels).
|
||||
- `h2` and `h3` are most common for content structure.
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: reference-signal-forms
|
||||
description: Explains the mental model and architecture of the code under `packages/forms/signals`. You MUST use this skill any time you plan to work with code in `packages/forms/signals`
|
||||
---
|
||||
|
||||
# Signal Forms Architecture
|
||||
|
||||
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
|
||||
|
||||
1. **Model-Driven**: The system is built around a `WritableSignal<T>` which serves as the **single source of truth**.
|
||||
Unlike Reactive Forms where the `FormControl` holds the value, here the `Signal` holds the value.
|
||||
The form is merely a _view_ or _projection_ of that signal, adding form-specific state (validity, dirty, touched).
|
||||
|
||||
2. **Proxy-Based Traversal**: The form API (`form(signal)`) returns a `FieldTree`. This object is a **Proxy**.
|
||||
It allows accessing nested fields (e.g., `myForm.user.name`) without manually creating control groups.
|
||||
Accessing a property on the proxy lazily resolves or creates the corresponding `FieldNode`.
|
||||
|
||||
3. **Schema-Based Logic**: Validation, disabled state, and other metadata are defined separately via **Schemas**.
|
||||
Schemas are applied to the form structure using functions like `apply`, `applyEach` (for arrays), and `applyWhen`.
|
||||
This separates the _structure_ of the data from the _rules_ governing it.
|
||||
|
||||
4. **Directives as Glue**: The `[formField]` directive binds a DOM element (native input or custom control) to a `FieldNode`.
|
||||
It handles:
|
||||
- Syncing the value between the DOM and the Signal.
|
||||
- Reflecting state (valid, touched, etc.) to the UI.
|
||||
- Handling user interaction events (blur, input).
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. `FieldNode` (`src/field/node.ts`)
|
||||
|
||||
The central internal class representing a single field in the form graph. It aggregates several state managers:
|
||||
|
||||
- `structure`: Manages parent/child relationships and signal slicing.
|
||||
- `validationState`: Computes `valid`, `invalid`, `errors` signals.
|
||||
- `nodeState`: Tracks `touched`, `dirty`, `pristine`.
|
||||
- `metadataState`: Stores metadata like `min`, `max`, `required`.
|
||||
- `submitState`: Tracks submission status and server errors.
|
||||
|
||||
### 2. `ValidationState` (`src/field/validation.ts`)
|
||||
|
||||
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_directive.ts`)
|
||||
|
||||
The bridge between the `FieldNode` and the DOM.
|
||||
|
||||
- Selector: `[formField]`
|
||||
- It supports:
|
||||
- **Native Elements**: `<input>`, `<select>`, `<textarea>`.
|
||||
- **Custom Controls**: Components implementing `FormUiControl` or `FormValueControl`.
|
||||
- **Legacy Interop**: Components implementing `ControlValueAccessor` (via `InteropNgControl`).
|
||||
|
||||
### 4. `Schema` (`src/api/structure.ts` & `src/api/rules`)
|
||||
|
||||
Defines the behavior.
|
||||
|
||||
- Created via `schema(fn)`.
|
||||
- Applied via `apply(path, schema)`.
|
||||
- Rules include validators (`required`, `pattern`, `min`, `max`) and state modifiers (`disabled`, `hidden`).
|
||||
|
||||
## Data Flow
|
||||
|
||||
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.
|
||||
|
||||
## Usage Example (Conceptual)
|
||||
|
||||
```typescript
|
||||
// 1. Define Model
|
||||
const user = signal({name: '', age: 0});
|
||||
|
||||
// 2. Define Schema
|
||||
const userRules = schema((u) => {
|
||||
required(u.name);
|
||||
min(u.age, 18);
|
||||
});
|
||||
|
||||
// 3. Create Form
|
||||
const userForm = form(user, userRules); // OR apply(userForm, userRules)
|
||||
|
||||
// 4. Bind in Template
|
||||
// <input [formField]="userForm.name">
|
||||
```
|
||||
|
||||
## Important Files
|
||||
|
||||
- `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_directive.ts`: The `[formField]` directive.
|
||||
|
||||
## Supplemental Information
|
||||
|
||||
- [Compiler & Core Integration](references/integration.md): Details how `[formField]` hooks into type-checking and the runtime.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Signal Forms Integration: Compiler & Core
|
||||
|
||||
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-CLI Integration (Type Checking)
|
||||
|
||||
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:
|
||||
|
||||
- **Detection**: The compiler identifies a directive as a "Field Directive" if it has the `ɵNgFieldDirective` property or comes from `@angular/forms/signals`.
|
||||
- **Synthetic Binding Expansion**: When you write `<input [formField]="mySignal" />`, the type checker doesn't just check `formField`. It synthetically expands this into a set of bindings for validation:
|
||||
- `[value]="mySignal()"` (or `checked` for checkboxes)
|
||||
- `[disabled]="mySignal.disabled()"`
|
||||
- `[required]="mySignal.required()"`
|
||||
- ...and so on.
|
||||
- This ensures that `mySignal` (the `FieldNode`) has all the necessary properties to drive the form control.
|
||||
- **Conflict Detection**: It actively prevents "double binding". If you bind `[formField]`, you are _banned_ from also binding `[value]`, `[disabled]`, `[required]`, etc., as the signal form should be the single source of truth.
|
||||
- **Element Type Validation**:
|
||||
- **Native Elements**: Checks that the signal's value type matches the element type (e.g., `<input type="checkbox">` requires a `boolean` signal).
|
||||
- **Custom Controls**: Detects if a custom component is a "Form Value Control" (has a `value` input/output) or "Form Checkbox Control" (has a `checked` input/output) and validates against that.
|
||||
|
||||
### Relevant Files:
|
||||
|
||||
- `packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts`: The core logic for `TcbNativeFieldOp` and `SignalFormFieldOp`.
|
||||
|
||||
## 2. Core Runtime Integration
|
||||
|
||||
The `packages/core` package provides the low-level instructions that power the `FormField` directive. This allows it to do things normal directives cannot, like efficiently syncing state without change detection overhead for every property.
|
||||
|
||||
### Key Mechanisms:
|
||||
|
||||
- **`ɵ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:
|
||||
- **Access the Element**: Get direct access to the native element or component instance.
|
||||
- **Set Inputs**: Write directly to inputs of _other directives_ on the same node (e.g., setting the `value` input of a custom control).
|
||||
- **Listen to Outputs**: Subscribe to outputs of other directives (e.g., `valueChange`).
|
||||
- **Bypass Templates**: It effectively acts as a "meta-directive" that manages the bindings for you, bypassing the need for explicit template syntax for every property.
|
||||
|
||||
### Relevant Files:
|
||||
|
||||
- `packages/core/src/render3/instructions/control.ts`: Implementation of `ɵɵcontrol` instructions.
|
||||
- `packages/forms/signals/src/directive/form_field_directive.ts`: The directive that implements the hook.
|
||||
@@ -43,5 +43,3 @@ c702e8af0b2144d97b93171dc2806ed1a0346762
|
||||
6d3f5752f204a5a30f3c09fbf6d4a510a4522bcb
|
||||
3fa7b2b136696464e0a91b5ec25c6adf272d4d6b
|
||||
ad65f44877f22caadbc8b977b5b720c3c86fcc45
|
||||
# commit that changed formatting
|
||||
698b0288bee60b8c5926148b79b5b93f454098db
|
||||
@@ -11,6 +11,7 @@ esbuild_checked_in(
|
||||
config = "esbuild.conf.js",
|
||||
entry_point = ":lib/main.mts",
|
||||
external = [
|
||||
"undici",
|
||||
"pnpapi",
|
||||
],
|
||||
metafile = False,
|
||||
@@ -35,5 +36,7 @@ ts_project(
|
||||
"//:node_modules/@actions/github",
|
||||
"//:node_modules/@angular/ng-dev",
|
||||
"//:node_modules/@types/node",
|
||||
"//:node_modules/@types/tmp",
|
||||
"//:node_modules/tmp",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {writeFileSync, mkdtempSync} from 'node:fs';
|
||||
import {join} from 'node:path';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {fileSync} from 'tmp';
|
||||
import {writeSync} from 'node:fs';
|
||||
import {getInput, setSecret} from '@actions/core';
|
||||
|
||||
let credentialFilePath: undefined | string;
|
||||
|
||||
export function getCredentialFilePath(): string {
|
||||
if (credentialFilePath === undefined) {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), 'credential-'));
|
||||
const filePath = join(tmpDir, 'credential.json');
|
||||
writeFileSync(filePath, getInput('serviceKey', {required: true}));
|
||||
setSecret(filePath);
|
||||
credentialFilePath = filePath;
|
||||
const tmpFile = fileSync({postfix: '.json'});
|
||||
writeSync(tmpFile.fd, getInput('serviceKey', {required: true}));
|
||||
setSecret(tmpFile.name);
|
||||
credentialFilePath = tmpFile.name;
|
||||
}
|
||||
return credentialFilePath;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import {
|
||||
fetchLongTermSupportBranchesFromNpm,
|
||||
ActiveReleaseTrains,
|
||||
AuthenticatedGitClient,
|
||||
ReleaseConfig,
|
||||
} from '@angular/ng-dev';
|
||||
import {fetchLongTermSupportBranchesFromNpm, ActiveReleaseTrains} from '@angular/ng-dev';
|
||||
import {ReleaseConfig} from '@angular/ng-dev';
|
||||
import {AuthenticatedGitClient} from '@angular/ng-dev';
|
||||
|
||||
export interface Deployment {
|
||||
branch: string;
|
||||
|
||||
+1228
-33074
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
name: 'Saucelabs legacy test job'
|
||||
description: 'Runs tests against Saucelabs (outside of Bazel)'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Starting Saucelabs tunnel service
|
||||
shell: bash
|
||||
run: ./tools/saucelabs/sauce-service.sh run &
|
||||
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
|
||||
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
|
||||
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
|
||||
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
|
||||
# test in all supported browsers on Saucelabs anyway until this job can be removed.
|
||||
- name: Preparing Bazel-generated fixtures required in legacy tests
|
||||
shell: bash
|
||||
run: |
|
||||
# Locale files are needed for i18n tests running within Saucelabs. These are added
|
||||
# directly as sources so that the TypeScript compilation of `/packages/tsconfig.json`
|
||||
# can succeed. Note that the base locale and currencies files are checked-in, so
|
||||
# we do not need to re-generate those through Bazel.
|
||||
mkdir -p packages/common/locales/extra
|
||||
cp dist/bin/packages/common/locales/*.ts packages/common/locales
|
||||
cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra
|
||||
- name: Build bundle of tests to run on Saucelabs
|
||||
shell: bash
|
||||
run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs
|
||||
- name: Wait and confirm Saucelabs tunnel has connected
|
||||
shell: bash
|
||||
run: ./tools/saucelabs/sauce-service.sh ready-wait
|
||||
- name: Running tests on Saucelabs.
|
||||
shell: bash
|
||||
run: KARMA_WEB_TEST_MODE=SL_REQUIRED pnpm karma start ./karma-js.conf.js --single-run
|
||||
- name: Stop Saucelabs tunnel service
|
||||
shell: bash
|
||||
run: ./tools/saucelabs/sauce-service.sh stop
|
||||
@@ -21,21 +21,19 @@ jobs:
|
||||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'adev: preview'))
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build adev
|
||||
# `snapshot-build` config is used to stamp the exact version with sha in the footer.
|
||||
run: pnpm bazel build //adev:build.production --config=snapshot-build
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY || 'bypassed_for_forks' }}
|
||||
triggering-label: 'adev: preview'
|
||||
pull-number: '${{github.event.pull_request.number}}'
|
||||
artifact-build-revision: '${{github.event.pull_request.head.sha}}'
|
||||
deploy-directory: './dist/bin/adev/dist/browser'
|
||||
|
||||
@@ -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]
|
||||
@@ -30,23 +29,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: '${{secrets.GITHUB_TOKEN}}'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Configure Firebase deploy target
|
||||
working-directory: ./
|
||||
run: |
|
||||
# We can use `npx` as the Firebase deploy actions uses it too.
|
||||
# Use stable version release
|
||||
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}}
|
||||
npx -y firebase-tools@latest target:clear --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs
|
||||
npx -y firebase-tools@latest target:apply --config adev/firebase.json --project ${{env.PREVIEW_PROJECT}} hosting angular-docs ${{env.PREVIEW_SITE}}
|
||||
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/previews/upload-artifacts-to-firebase@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
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]
|
||||
|
||||
@@ -15,9 +14,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.repository.fork == false
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/branch-manager@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -25,34 +25,20 @@ jobs:
|
||||
token: '${{secrets.BENCHMARK_POST_RESULTS_GITHUB_TOKEN}}'
|
||||
reactions: 'rocket'
|
||||
|
||||
- uses: alessbell/pull-request-comment-branch@ef3408c9757d05f89cb525036383033a313758a0 # v2.1.0
|
||||
- uses: alessbell/pull-request-comment-branch@aad01d65d6982b8eacabed5e9a684cd8ceb98da6 # v1.1
|
||||
id: comment-branch
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Specify repository as the PR branch might be from a fork.
|
||||
repository: ${{steps.comment-branch.outputs.head_owner}}/${{steps.comment-branch.outputs.head_repo}}
|
||||
# Checkout the pull request and assume it being trusted given we've checked
|
||||
# that the action was triggered by a team member.
|
||||
ref: ${{steps.comment-branch.outputs.head_sha}}
|
||||
|
||||
# We cannot use `angular/dev-infra/github-actions/npm/checkout-and-setup-node` here
|
||||
# because it does not support checking out from a fork (as it lacks a `repository` input).
|
||||
# Thus, we checkout and setup Node/pnpm manually.
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
ref: ${{steps.comment-branch.outputs.head_ref}}
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
bazelrc: ./.bazelrc.user
|
||||
|
||||
@@ -63,10 +49,7 @@ jobs:
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: pnpm benchmarks prepare-for-github-action "$COMMENT_BODY"
|
||||
|
||||
- env:
|
||||
COMPARE_SHA: ${{steps.info.outputs.compareSha}}
|
||||
BENCHMARK_TARGET: ${{steps.info.outputs.benchmarkTarget}}
|
||||
run: pnpm benchmarks run-compare "$COMPARE_SHA" "$BENCHMARK_TARGET"
|
||||
- run: pnpm benchmarks run-compare ${{steps.info.outputs.compareSha}} ${{steps.info.outputs.benchmarkTarget}}
|
||||
id: benchmark
|
||||
name: Running benchmark
|
||||
|
||||
|
||||
+38
-28
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Check code lint
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Validate angular robot configuration
|
||||
run: pnpm ng-dev ngbot verify
|
||||
- name: Validate agent skills
|
||||
run: pnpm ng-dev misc validate-skills
|
||||
run: pnpm ng-dev ai skills validate
|
||||
- name: Confirm code builds with typescript as expected
|
||||
run: pnpm check-tooling-setup
|
||||
|
||||
@@ -41,13 +41,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Test build
|
||||
run: pnpm devtools:build:chrome
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@09090944bd8aaa2a517cefb6e38bf1aae336b42b # v7.4.3
|
||||
uses: cypress-io/github-action@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5
|
||||
with:
|
||||
command: pnpm devtools:test:e2e
|
||||
start: pnpm bazel run //devtools/src:devserver
|
||||
@@ -69,11 +69,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -85,11 +85,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -102,11 +102,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -121,11 +121,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -138,11 +138,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: echo "https://${{secrets.SNAPSHOT_BUILDS_GITHUB_TOKEN}}:@github.com" > ${HOME}/.git_credentials
|
||||
@@ -154,11 +154,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
google_credential: ${{ secrets.RBE_TRUSTED_BUILDS_USER }}
|
||||
- name: Install node modules
|
||||
@@ -188,21 +188,31 @@ jobs:
|
||||
- run: pnpm -C packages/zone.js jest:test
|
||||
- run: pnpm -C packages/zone.js jest:nodetest
|
||||
- run: pnpm -C packages/zone.js vitest:test
|
||||
- run: pnpm -C packages/zone.js vitest-globals:test
|
||||
- run: pnpm -C packages/zone.js electrontest
|
||||
- run: pnpm -C packages/zone.js/test/typings test
|
||||
|
||||
# saucelabs:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
# steps:
|
||||
# - name: Initialize environment
|
||||
# uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b5a3609f89c06eb4037dce22a93641213a5d1508
|
||||
# - name: Install node modules
|
||||
# run: pnpm install --frozen-lockfile
|
||||
# - uses: ./.github/actions/saucelabs-legacy
|
||||
|
||||
adev-deploy:
|
||||
needs: [adev]
|
||||
if: needs.adev.result == 'success'
|
||||
runs-on: ubuntu-latest-8core
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build adev
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Setting `persist-credentials: false` prevents the github-action account from being the
|
||||
# account that is attempted to be used for authentication, instead the remote is set to
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
ANGULAR_READONLY_GITHUB_TOKEN: ${{ secrets.READONLY_GITHUB_TOKEN }}
|
||||
|
||||
- name: Create a PR (if necessary)
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.ANGULAR_ROBOT_ACCESS_TOKEN }}
|
||||
push-to-fork: 'angular-robot/angular'
|
||||
|
||||
@@ -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,8 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: angular/dev-infra/github-actions/labeling/pull-request@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
labels: '{"requires: TGP": ["packages/core/primitives/**/{*,.*}"]}'
|
||||
@@ -24,14 +24,15 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: angular/dev-infra/github-actions/post-approval-changes@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
issue_labels:
|
||||
if: github.event_name == 'issues'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: angular/dev-infra/github-actions/labeling/issue@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/labeling/issue@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
google-generative-ai-key: ${{ secrets.GOOGLE_GENERATIVE_AI_KEY }}
|
||||
|
||||
@@ -1,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]
|
||||
|
||||
@@ -14,8 +13,8 @@ jobs:
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: angular/dev-infra/github-actions/google-internal-tests@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
run-tests-guide-url: http://go/angular-g3sync-start
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Manual jobs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
|
||||
jobs:
|
||||
# Bazel saucelabs job resides in `manual.yml` because it's currently unstable, but
|
||||
# kept as "runnable" for debugging/stabilization effort purposes.
|
||||
bazel-saucelabs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
JOBS: 2
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Saucelabs Variables
|
||||
uses: angular/dev-infra/github-actions/saucelabs@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Set up Sauce Tunnel Daemon
|
||||
run: pnpm bazel run //tools/saucelabs-daemon/background-service -- $JOBS &
|
||||
env:
|
||||
SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
- name: Run all saucelabs bazel tests
|
||||
run: |
|
||||
TESTS=$(./node_modules/.bin/bazelisk query --output label '(kind(karma_web_test, ...) intersect attr("tags", "saucelabs", ...)) except attr("tags", "fixme-saucelabs", ...)')
|
||||
pnpm bazel test --config=saucelabs --jobs=$JOBS ${TESTS}
|
||||
@@ -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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
- uses: angular/dev-infra/github-actions/unified-status-check@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -20,7 +21,7 @@ jobs:
|
||||
workflows: ${{ steps.workflows.outputs.workflows }}
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- 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@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
# We utilize the google-github-actions/auth action to allow us to get an active credential using workflow
|
||||
@@ -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}}
|
||||
|
||||
+34
-24
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Check code lint
|
||||
@@ -33,13 +33,13 @@ jobs:
|
||||
- name: Confirm code builds with typescript as expected
|
||||
run: pnpm check-tooling-setup
|
||||
- name: Validate agent skills
|
||||
run: pnpm ng-dev misc validate-skills
|
||||
run: pnpm ng-dev ai skills validate
|
||||
- name: Check commit message
|
||||
run: pnpm ng-dev commit-message validate-range ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}
|
||||
- name: Check code format
|
||||
run: pnpm ng-dev format changed --check ${{ github.event.pull_request.base.sha }}
|
||||
- name: Check Package Licenses
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/linting/licenses@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
allow-dependencies-licenses: 'pkg:npm/google-protobuf@'
|
||||
|
||||
@@ -47,13 +47,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
with:
|
||||
disable-package-manager-cache: true
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run unit tests
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Test build
|
||||
run: pnpm devtools:build:chrome
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@09090944bd8aaa2a517cefb6e38bf1aae336b42b # v7.4.3
|
||||
uses: cypress-io/github-action@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5
|
||||
with:
|
||||
command: pnpm devtools:test:e2e
|
||||
start: pnpm bazel run //devtools/src:devserver
|
||||
@@ -73,11 +73,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run CI tests for framework
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
ASPECT_RULES_JS_FROZEN_PNPM_LOCK: '1'
|
||||
- name: Upload GRPC logs (for debugging of RBE issues)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
path: /tmp/rbe-grpc.log
|
||||
retention-days: 1
|
||||
@@ -97,11 +97,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel Remote Caching
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run integration CI tests for framework
|
||||
@@ -112,11 +112,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run tests
|
||||
@@ -129,11 +129,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run tests
|
||||
@@ -144,11 +144,11 @@ jobs:
|
||||
labels: ubuntu-latest
|
||||
steps:
|
||||
- name: Initialize environment
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/setup@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Setup Bazel RBE
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
uses: angular/dev-infra/github-actions/bazel/configure-remote@e4dcda13e469f50a6bd92667e2ded8b98173bf2c
|
||||
- name: Install node modules
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: |
|
||||
@@ -174,6 +174,16 @@ jobs:
|
||||
- run: pnpm -C packages/zone.js jest:test
|
||||
- run: pnpm -C packages/zone.js jest:nodetest
|
||||
- run: pnpm -C packages/zone.js vitest:test
|
||||
- run: pnpm -C packages/zone.js vitest-globals:test
|
||||
- run: pnpm -C packages/zone.js electrontest
|
||||
- run: pnpm -C packages/zone.js/test/typings test
|
||||
|
||||
# saucelabs:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# SAUCE_TUNNEL_IDENTIFIER: angular-framework-${{ github.run_number }}
|
||||
# steps:
|
||||
# - name: Initialize environment
|
||||
# uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@b5a3609f89c06eb4037dce22a93641213a5d1508
|
||||
# - name: Install node modules
|
||||
# run: pnpm install --frozen-lockfile
|
||||
# - uses: ./.github/actions/saucelabs-legacy
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Publish Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
workflow_dispatch: # Allow manual trigger for verification
|
||||
|
||||
# Ensure only one release runs at a time PER BRANCH to avoid race conditions.
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read # Required to checkout the repository
|
||||
id-token: write # Required for NPM provenance in reusable workflow
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: angular/dev-infra/.github/workflows/reusable-release.yml@183403ae13b785698eaf13c819dda55b9fed430b # main
|
||||
secrets:
|
||||
wombot-token: ${{ secrets.WOMBOT_TOKEN }}
|
||||
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
|
||||
@@ -0,0 +1,52 @@
|
||||
name: OpenSSF Scorecard
|
||||
on:
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecards analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Needed to publish results
|
||||
id-token: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: 'Checkout code'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run analysis'
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
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@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -10,7 +10,6 @@
|
||||
"packages/compiler-cli/private/bazel.ts",
|
||||
"packages/compiler-cli/private/localize.ts",
|
||||
"packages/compiler-cli/private/tooling.ts",
|
||||
"packages/compiler-cli/private/hybrid_analysis.ts",
|
||||
"packages/compiler-cli/private/babel.d.ts",
|
||||
"packages/compiler-cli/src/bin/**",
|
||||
"packages/core/schematics/utils/tsurge/helpers/angular_devkit/**",
|
||||
|
||||
+2
-16
@@ -7,14 +7,7 @@ export const release = {
|
||||
publishRegistry: 'https://wombat-dressing-room.appspot.com',
|
||||
representativeNpmPackage: '@angular/core',
|
||||
npmPackages: [
|
||||
{
|
||||
name: '@angular/animations',
|
||||
deprecated: {
|
||||
version: '>=20.2.0-next.3',
|
||||
message:
|
||||
'@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.',
|
||||
},
|
||||
},
|
||||
{name: '@angular/animations'},
|
||||
{name: '@angular/common'},
|
||||
{name: '@angular/compiler-cli'},
|
||||
{name: '@angular/compiler'},
|
||||
@@ -24,14 +17,7 @@ export const release = {
|
||||
{name: '@angular/language-server'},
|
||||
{name: '@angular/language-service'},
|
||||
{name: '@angular/localize'},
|
||||
{
|
||||
name: '@angular/platform-browser-dynamic',
|
||||
deprecated: {
|
||||
version: '>=20.1.0-next.0',
|
||||
message:
|
||||
'@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.',
|
||||
},
|
||||
},
|
||||
{name: '@angular/platform-browser-dynamic'},
|
||||
{name: '@angular/platform-browser'},
|
||||
{name: '@angular/platform-server'},
|
||||
{name: '@angular/router'},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "es2020",
|
||||
"module": "Node16",
|
||||
"noEmit": true,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Yarn Berry doesn't check engines at all, so pnpm shouldn't either.
|
||||
engine-strict = false
|
||||
|
||||
# Disabling pnpm [hoisting](https://pnpm.io/npmrc#hoist) by setting `hoist=false` is recommended on
|
||||
# projects using rules_js so that pnpm outside of Bazel lays out a node_modules tree similar to what
|
||||
# rules_js lays out under Bazel (without a hidden node_modules/.pnpm/node_modules)
|
||||
hoist=false
|
||||
|
||||
# Avoid pnpm auto-installing peer dependencies. We want to be explicit about our versions used
|
||||
# for peer dependencies, avoiding potential mismatches. In addition, it ensures we can continue
|
||||
# to rely on peer dependency placeholders substituted via Bazel.
|
||||
auto-install-peers=false
|
||||
+1
-1
@@ -53,7 +53,7 @@ function readPackage(pkg, context) {
|
||||
if (pkg.version === '0.0.0-PLACEHOLDER' && localAngularPackages.has(key)) {
|
||||
pkg.dependencies = {
|
||||
...pkg.dependencies,
|
||||
[key]: 'workspace:*',
|
||||
[key]: 'workspace: *',
|
||||
};
|
||||
|
||||
delete pkg.peerDependencies[key];
|
||||
|
||||
@@ -38,10 +38,6 @@ adev/src/content/aria/**/*.json
|
||||
adev/src/content/cli/**/*.json
|
||||
adev/src/content/cdk/**/*.json
|
||||
|
||||
# Example package.json containing docregion (`// #docregion`) markers, which are
|
||||
# not valid JSON and cannot be processed by Prettier's json-stringify parser.
|
||||
adev/src/content/examples/schematics-for-libraries/projects/my-lib/package.json
|
||||
|
||||
# Antigravity rules
|
||||
.agent/rules/agents.md
|
||||
|
||||
|
||||
+47
-36
@@ -36,9 +36,8 @@
|
||||
|
||||
version: 3
|
||||
|
||||
availability:
|
||||
users_unavailable:
|
||||
- devversion
|
||||
#availability:
|
||||
# users_unavailable: []
|
||||
|
||||
# Meta field that goes unused by PullApprove to allow for defining aliases to be
|
||||
# used throughout the config.
|
||||
@@ -100,11 +99,11 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- ~alxhub
|
||||
- ~AndrewKushnir
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- ~thePunderWoman
|
||||
- thePunderWoman
|
||||
- kirjs
|
||||
- JoostK
|
||||
- ~amishne
|
||||
@@ -120,7 +119,6 @@ groups:
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files.exclude('packages/core/primitives/*'), [
|
||||
'.agent/skills/**/{*,.*}',
|
||||
'contributing-docs/public-api-surface.md',
|
||||
'dev-app/**/{*,.*}',
|
||||
'integration/**/{*,.*}',
|
||||
@@ -138,7 +136,6 @@ groups:
|
||||
'packages/platform-browser/**/{*,.*}',
|
||||
'packages/platform-browser-dynamic/**/{*,.*}',
|
||||
'packages/platform-server/**/{*,.*}',
|
||||
'packages/private/**/{*,.*}',
|
||||
'packages/ssr/**/{*,.*}',
|
||||
'packages/router/**/{*,.*}',
|
||||
'packages/service-worker/**/{*,.*}',
|
||||
@@ -148,12 +145,12 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- ~alxhub
|
||||
- ~AndrewKushnir
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- kirjs
|
||||
- ~thePunderWoman
|
||||
- thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- JeanMeche
|
||||
- ~amishne
|
||||
@@ -203,13 +200,12 @@ groups:
|
||||
users:
|
||||
- ~JiaLiPassion
|
||||
- ~alxhub
|
||||
- ~AndrewKushnir
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- crisbeto
|
||||
- devversion
|
||||
- kirjs
|
||||
- alan-agius4
|
||||
- ~thePunderWoman
|
||||
- thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- ~amishne
|
||||
- ~leonsenft
|
||||
@@ -247,20 +243,20 @@ groups:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'adev/**/{*,.*}',
|
||||
'tools/manual_api_docs/blocks/*.md',
|
||||
'tools/manual_api_docs/elements/*.md',
|
||||
'tools/manual_api_docs/blocks/**/*.md',
|
||||
'tools/manual_api_docs/elements/**/*.md',
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- alan-agius4
|
||||
- ~alxhub
|
||||
- ~AndrewKushnir
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- bencodezen
|
||||
- crisbeto
|
||||
- kirjs
|
||||
- JeanMeche
|
||||
- ~thePunderWoman
|
||||
- thePunderWoman
|
||||
- devversion
|
||||
- josephperrott
|
||||
- ~pkozlowski-opensource
|
||||
@@ -286,14 +282,13 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- AleksanderBodurri
|
||||
- csmick
|
||||
- ~devversion
|
||||
- dgp1130
|
||||
- eduhmc
|
||||
- hawkgs
|
||||
- JeanMeche
|
||||
- hybrist
|
||||
- ~josephperrott
|
||||
- ~milomg
|
||||
- JeanMeche
|
||||
- milomg
|
||||
|
||||
# =========================================================
|
||||
# Dev-infra
|
||||
@@ -303,10 +298,9 @@ groups:
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files
|
||||
.exclude('.pullapprove.yml')
|
||||
.exclude('tools/manual_api_docs/blocks/*.md')
|
||||
.exclude('tools/manual_api_docs/elements/*.md')
|
||||
.exclude('.agent/**/*.md'),
|
||||
.exclude('.pullapprove.yml')
|
||||
.exclude('tools/manual_api_docs/blocks/**/*.md')
|
||||
.exclude('tools/manual_api_docs/elements/**/*.md'),
|
||||
[
|
||||
'{*,.*}',
|
||||
'.agent/**/{*,.*}',
|
||||
@@ -323,14 +317,18 @@ groups:
|
||||
'modules/{*,.*}',
|
||||
'packages/{*,.*}',
|
||||
'packages/examples/test-utils/**/{*,.*}',
|
||||
'packages/private/**/{*,.*}',
|
||||
'packages/examples/{*,.*}',
|
||||
'scripts/**/{*,.*}',
|
||||
'third_party/**/{*,.*}',
|
||||
'tools/contributing-stats/**/{*,.*}',
|
||||
'tools/gulp-tasks/**/{*,.*}',
|
||||
'tools/legacy-saucelabs/**/{*,.*}',
|
||||
'tools/manual_api_docs/**/{*,.*}',
|
||||
'tools/pnpm-patches/**/{*,.*}',
|
||||
'tools/rxjs/**/{*,.*}',
|
||||
'tools/saucelabs-daemon/**/{*,.*}',
|
||||
'tools/saucelabs/**/{*,.*}',
|
||||
'tools/symbol-extractor/**/{*,.*}',
|
||||
'tools/testing/**/{*,.*}',
|
||||
'tools/tslint/**/{*,.*}',
|
||||
@@ -374,10 +372,10 @@ groups:
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- ~AndrewKushnir
|
||||
- AndrewKushnir
|
||||
- ~alxhub
|
||||
- atscott
|
||||
- ~thePunderWoman
|
||||
- thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- kirjs
|
||||
- crisbeto
|
||||
@@ -410,11 +408,10 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- ~alxhub
|
||||
- ~AndrewKushnir
|
||||
- AndrewKushnir
|
||||
- atscott
|
||||
- kirjs
|
||||
- JeanMeche
|
||||
- ~thePunderWoman
|
||||
- thePunderWoman
|
||||
- ~pkozlowski-opensource
|
||||
- ~amishne
|
||||
- ~leonsenft
|
||||
@@ -437,7 +434,7 @@ groups:
|
||||
])
|
||||
reviewers:
|
||||
users:
|
||||
- MarkTechson
|
||||
- marktechson
|
||||
- kirjs
|
||||
- ~JeanMeche
|
||||
- ~dgp1130
|
||||
@@ -459,10 +456,10 @@ groups:
|
||||
reviewers:
|
||||
users:
|
||||
- ~alxhub
|
||||
- ~AndrewKushnir
|
||||
- AndrewKushnir
|
||||
- andrewseguin
|
||||
- dgp1130
|
||||
- ~thePunderWoman
|
||||
- thePunderWoman
|
||||
- josephperrott
|
||||
|
||||
# =========================================================
|
||||
@@ -481,12 +478,26 @@ groups:
|
||||
users:
|
||||
- ~pkozlowski-opensource # Pawel Kozlowski
|
||||
- ~alxhub # Alex Rickabaugh
|
||||
- ~thePunderWoman # Jessica Janiuk
|
||||
- ~AndrewKushnir # Andrew Kushnir
|
||||
- thePunderWoman # Jessica Janiuk
|
||||
- AndrewKushnir # Andrew Kushnir
|
||||
- atscott # Andrew Scott
|
||||
|
||||
# External team required reviews
|
||||
primitives-shared:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files, [
|
||||
'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
|
||||
|
||||
+21
-1
@@ -1,3 +1,4 @@
|
||||
load("@aspect_rules_js//js:defs.bzl", "js_library")
|
||||
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
|
||||
load("@devinfra//bazel/validation:defs.bzl", "validate_ts_version_matching")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
@@ -7,6 +8,8 @@ package(default_visibility = ["//visibility:public"])
|
||||
|
||||
exports_files([
|
||||
"LICENSE",
|
||||
"karma-js.conf.js",
|
||||
"browser-providers.conf.js",
|
||||
"package.json",
|
||||
])
|
||||
|
||||
@@ -22,7 +25,7 @@ filegroup(
|
||||
)
|
||||
|
||||
validate_ts_version_matching(
|
||||
module_bazel = "MODULE.bazel",
|
||||
module_lock_file = "MODULE.bazel.lock",
|
||||
package_json = "package.json",
|
||||
)
|
||||
|
||||
@@ -31,6 +34,14 @@ alias(
|
||||
actual = "//packages:tsconfig-build.json",
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = "browser-providers",
|
||||
srcs = [
|
||||
"browser-providers.conf.d.ts",
|
||||
"browser-providers.conf.js",
|
||||
],
|
||||
)
|
||||
|
||||
copy_to_bin(
|
||||
name = "angularjs_scripts",
|
||||
srcs = [
|
||||
@@ -53,6 +64,15 @@ config_setting(
|
||||
values = {"stamp": "true"},
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "sauce_connect",
|
||||
actual = select({
|
||||
"@devinfra//bazel/constraints:linux_x64": "@sauce_connect_linux_amd64//:bin/sc",
|
||||
"@devinfra//bazel/constraints:macos_x64": "@sauce_connect_mac//:bin/sc",
|
||||
"@devinfra//bazel/constraints:macos_arm64": "@sauce_connect_mac//:bin/sc",
|
||||
}),
|
||||
)
|
||||
|
||||
# When enabled, this flag substitutes dependency versions with snapshot repositories
|
||||
# for all packages in this repository. Note that this does not apply to peer
|
||||
# dependencies, as they must be installed directly.
|
||||
|
||||
+9
-1783
File diff suppressed because it is too large
Load Diff
@@ -4967,9 +4967,9 @@ To learn about the release highlights and our CLI-powered automated update workf
|
||||
- **ivy:** i18n - render legacy message ids in `$localize` if requested ([#32937](https://github.com/angular/angular/issues/32937)) ([bcbf3e4](https://github.com/angular/angular/commit/bcbf3e4))
|
||||
- **language-service:** module definitions on directive hover ([#32763](https://github.com/angular/angular/issues/32763)) ([0d186dd](https://github.com/angular/angular/commit/0d186dd)), closes [#32565](https://github.com/angular/angular/issues/32565)
|
||||
- **ngcc:** expose `--create-ivy-entry-points` option on ivy-ngcc ([#33049](https://github.com/angular/angular/issues/33049)) ([b2b917d](https://github.com/angular/angular/commit/b2b917d)), closes [/github.com/angular/angular/pull/32999#issuecomment-539937368](https://github.com/angular/angular/pull/32999/issues/issuecomment-539937368)
|
||||
- update rxjs peerDependencies minimum requirement to 6.5.3 ([#32812](https://github.com/angular/angular/issues/32812)) ([66658c4](https://github.com/angular/angular/commit/66658c4))
|
||||
- update rxjs peerDependencies minimum requirment to 6.5.3 ([#32812](https://github.com/angular/angular/issues/32812)) ([66658c4](https://github.com/angular/angular/commit/66658c4))
|
||||
- **ivy:** support ng-add in localize package ([#32791](https://github.com/angular/angular/issues/32791)) ([e41cbfb](https://github.com/angular/angular/commit/e41cbfb))
|
||||
- **language-service:** allow retrieving synchronized analyzed NgModules ([#32779](https://github.com/angular/angular/issues/32779)) ([98feee7](https://github.com/angular/angular/commit/98feee7))
|
||||
- **language-service:** allow retreiving synchronized analyzed NgModules ([#32779](https://github.com/angular/angular/issues/32779)) ([98feee7](https://github.com/angular/angular/commit/98feee7))
|
||||
- **service-worker:** remove deprecated `versionedFiles` option ([#32862](https://github.com/angular/angular/issues/32862)) ([5d5c94d](https://github.com/angular/angular/commit/5d5c94d))
|
||||
- **language-service:** expose determining the NgModule of a Directive ([#32710](https://github.com/angular/angular/issues/32710)) ([2846505](https://github.com/angular/angular/commit/2846505)), closes [#32565](https://github.com/angular/angular/issues/32565)
|
||||
- **bazel:** support ts_library targets as entry-points for ng_package ([#32610](https://github.com/angular/angular/issues/32610)) ([217db9b](https://github.com/angular/angular/commit/217db9b))
|
||||
@@ -5114,7 +5114,7 @@ To learn about the release highlights and our CLI-powered automated update workf
|
||||
We assume you will fetch rules_nodejs in your WORKSPACE file, and no other dependencies remain here.
|
||||
Simply remove any calls to this function and the corresponding load statement.
|
||||
- typescript 3.4 and 3.5 are no longer supported, please update to typescript 3.6
|
||||
- We no longer directly have a direct dependency on `tslib`. Instead it is now listed a `peerDependency`.
|
||||
- We no longer directly have a direct depedency on `tslib`. Instead it is now listed a `peerDependency`.
|
||||
|
||||
Users not using the CLI will need to manually install `tslib` via;
|
||||
|
||||
@@ -6617,7 +6617,7 @@ For example:
|
||||
- **forms:** properly handle special properties in FormGroup.get ([#22249](https://github.com/angular/angular/issues/22249)) ([dc3e8aa](https://github.com/angular/angular/commit/dc3e8aa)), closes [#17195](https://github.com/angular/angular/issues/17195)
|
||||
- **platform-server:** avoid clash between server and client style encapsulation attributes ([#24158](https://github.com/angular/angular/issues/24158)) ([e9f2203](https://github.com/angular/angular/commit/e9f2203))
|
||||
- **platform-server:** avoid dependency cycle when using http interceptor ([#24229](https://github.com/angular/angular/issues/24229)) ([2991b1b](https://github.com/angular/angular/commit/2991b1b)), closes [#23023](https://github.com/angular/angular/issues/23023)
|
||||
- **platform-server:** don't reflect innerHTML property to attribute ([#24213](https://github.com/angular/angular/issues/24213)) ([c17098d](https://github.com/angular/angular/commit/c17098d)), closes [#19278](https://github.com/angular/angular/issues/19278)
|
||||
- **platform-server:** don't reflect innerHTML property to attibute ([#24213](https://github.com/angular/angular/issues/24213)) ([c17098d](https://github.com/angular/angular/commit/c17098d)), closes [#19278](https://github.com/angular/angular/issues/19278)
|
||||
- **platform-server:** provide Domino DOM types globally ([#24116](https://github.com/angular/angular/issues/24116)) ([906b3ec](https://github.com/angular/angular/commit/906b3ec)), closes [#23280](https://github.com/angular/angular/issues/23280) [#23133](https://github.com/angular/angular/issues/23133)
|
||||
|
||||
<!-- CHANGELOG SPLIT MARKER -->
|
||||
@@ -9193,7 +9193,7 @@ Note: 4.0.0-beta.0 release also contains all the changes present in the 2.3.1 re
|
||||
- **compiler:** support dotted property binding ([8db184d](https://github.com/angular/angular/commit/8db184d)), closes [angular/flex-layout#34](https://github.com/angular/flex-layout/issues/34)
|
||||
- **compiler:** update to metadata version 3 ([#13464](https://github.com/angular/angular/issues/13464)) ([b9b557c](https://github.com/angular/angular/commit/b9b557c))
|
||||
- **core:** detectChanges() doesn't work on detached instance ([4d6ac9d](https://github.com/angular/angular/commit/4d6ac9d)), closes [#13426](https://github.com/angular/angular/issues/13426) [#13472](https://github.com/angular/angular/issues/13472)
|
||||
- **core:** properly destroy embedded Views attached to ApplicationRef ([#13459](https://github.com/angular/angular/issues/13459)) ([d40bbf4](https://github.com/angular/angular/commit/d40bbf4)), closes [#13062](https://github.com/angular/angular/issues/13062)
|
||||
- **core:** properly destroy embedded Views attatched to ApplicationRef ([#13459](https://github.com/angular/angular/issues/13459)) ([d40bbf4](https://github.com/angular/angular/commit/d40bbf4)), closes [#13062](https://github.com/angular/angular/issues/13062)
|
||||
- **core:** remove logError from logGroup ([#12925](https://github.com/angular/angular/issues/12925)) ([5fab871](https://github.com/angular/angular/commit/5fab871))
|
||||
- **forms:** ensure `select[multiple]` retains selections ([b3dcff0](https://github.com/angular/angular/commit/b3dcff0)), closes [#12527](https://github.com/angular/angular/issues/12527) [#12654](https://github.com/angular/angular/issues/12654)
|
||||
- **forms:** fix Validators.min/maxLength with FormArray ([#13095](https://github.com/angular/angular/issues/13095)) ([7383e4a](https://github.com/angular/angular/commit/7383e4a)), closes [#13089](https://github.com/angular/angular/issues/13089)
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ You can file new issues by selecting from our [new issue templates](https://gith
|
||||
|
||||
### <a name="pr-quality"></a> Contribution Quality
|
||||
|
||||
We strongly value open source contribution and pull requests from community contributors. Please note that every pull request is reviewed and merged by an actual person on the team, which does take time and effort. That is time and effort that does take away from other valuable work. With that in mind we have a minimum set of expectations that are required of any community contribution pull request that is opened.
|
||||
We strongly value open source contribution and pull requests from community contributors. Please note that every pull request is reviewed and merged by an actual person on the team, which does take time and effort. That is time and effort that does take away from other valuable work. With that in mind we have an minimum set of expectations that are required of any community contribution pull request that is opened.
|
||||
|
||||
1. Search [GitHub](https://github.com/angular/angular/pulls) for an open or closed PR that relates to your submission.
|
||||
- You don't want to duplicate existing efforts.
|
||||
@@ -238,7 +238,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
|
||||
- For corporations, we'll need you to
|
||||
[print, sign and one of scan+email, fax or mail the form][corporate-cla].
|
||||
|
||||
If you have more than one GitHub account, or multiple email addresses associated with a single GitHub account, you must sign the CLA using the primary email address of the GitHub account used to author Git commits and send pull requests.
|
||||
If you have more than one GitHub accounts, or multiple email addresses associated with a single GitHub account, you must sign the CLA using the primary email address of the GitHub account used to author Git commits and send pull requests.
|
||||
|
||||
The following documents can help you sort out issues with GitHub accounts and multiple email addresses:
|
||||
|
||||
|
||||
+53
-37
@@ -4,42 +4,42 @@ module(
|
||||
name = "angular",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_pkg", version = "1.3.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 = "rules_pkg", version = "1.2.0")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.7.3")
|
||||
bazel_dep(name = "aspect_rules_ts", version = "3.8.5")
|
||||
bazel_dep(name = "aspect_rules_js", version = "2.9.2")
|
||||
bazel_dep(name = "aspect_rules_esbuild", version = "0.25.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 = "aspect_bazel_lib", version = "2.22.5")
|
||||
bazel_dep(name = "tar.bzl", version = "0.9.0")
|
||||
bazel_dep(name = "yq.bzl", version = "0.3.5")
|
||||
bazel_dep(name = "rules_angular")
|
||||
git_override(
|
||||
module_name = "rules_angular",
|
||||
commit = "5fa856469c642490a6e381a05d7f58f60fad1913",
|
||||
commit = "d746c4f75e42cffe389d1ab077f4639be2bc78d1",
|
||||
remote = "https://github.com/angular/rules_angular.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "devinfra")
|
||||
git_override(
|
||||
module_name = "devinfra",
|
||||
commit = "183403ae13b785698eaf13c819dda55b9fed430b",
|
||||
commit = "e4dcda13e469f50a6bd92667e2ded8b98173bf2c",
|
||||
remote = "https://github.com/angular/dev-infra.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_sass")
|
||||
git_override(
|
||||
module_name = "rules_sass",
|
||||
commit = "bf9d6564f57b4df94b4915086ad0e7ad49a95f37",
|
||||
commit = "1184a80751a21af8348f308abc5b38a41f26850e",
|
||||
remote = "https://github.com/angular/rules_sass.git",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_browsers")
|
||||
git_override(
|
||||
module_name = "rules_browsers",
|
||||
commit = "c37398d63f5e990d618d02f4c6fb2fefe40f1a79",
|
||||
commit = "8231142fc5516323b040883bf774ba8362645387",
|
||||
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.22.1-darwin_arm64": ("node-v22.22.1-darwin-arm64.tar.gz", "node-v22.22.1-darwin-arm64", "679ad4966339e4ef4900f57996714864e4211b898825bb840c3086c419fbcef2"),
|
||||
"22.22.1-darwin_amd64": ("node-v22.22.1-darwin-x64.tar.gz", "node-v22.22.1-darwin-x64", "07b13722d558790fca20bb1ecf61bde24b7a4863111f7be77fc57251a407359a"),
|
||||
"22.22.1-linux_arm64": ("node-v22.22.1-linux-arm64.tar.xz", "node-v22.22.1-linux-arm64", "0f3550d58d45e5d3cf7103d9e3f69937f09fe82fb5dd474c66a5d816fa58c9ee"),
|
||||
"22.22.1-linux_ppc64le": ("node-v22.22.1-linux-ppc64le.tar.xz", "node-v22.22.1-linux-ppc64le", "a65a44cf0224505f052b90357b763dbc1ea9148f4f5f2284f0596cf2000f819b"),
|
||||
"22.22.1-linux_s390x": ("node-v22.22.1-linux-s390x.tar.xz", "node-v22.22.1-linux-s390x", "e08b3a73d0bd840e008f589e4be4a2ef3d4a0c59015f4f20a04ed7fc968042a2"),
|
||||
"22.22.1-linux_amd64": ("node-v22.22.1-linux-x64.tar.xz", "node-v22.22.1-linux-x64", "9a6bc82f9b491279147219f6a18add1e18424dce90d41d2a5fcd69d4924ba3aa"),
|
||||
"22.22.1-windows_amd64": ("node-v22.22.1-win-x64.zip", "node-v22.22.1-win-x64", "877cb93829e14fffbbc7903e7d8037336c9a79f3ea43c5d0b8c2379b79da56de"),
|
||||
},
|
||||
node_version = "24.21.0",
|
||||
node_version = "22.22.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 = "10.30.3",
|
||||
pnpm_version_integrity = "sha512-yWHR4KLY41TsqlFmuCJRZmi39Ey1vZUSLVkN2Bki9gb1RzttI+xKW+Bef80Y6EiNR9l4u+mBhy8RRdBumnQAFw==",
|
||||
)
|
||||
use_repo(pnpm, "pnpm")
|
||||
|
||||
@@ -86,7 +86,6 @@ npm.npm_translate_lock(
|
||||
"//adev:package.json",
|
||||
"//adev/shared-docs:package.json",
|
||||
"//adev/shared-docs/pipeline/api-gen:package.json",
|
||||
"//devtools:package.json",
|
||||
"//integration:package.json",
|
||||
"//modules:package.json",
|
||||
"//packages/animations:package.json",
|
||||
@@ -110,11 +109,7 @@ npm.npm_translate_lock(
|
||||
"//tools/bazel/rules_angular_store:package.json",
|
||||
"//vscode-ng-language-service/integration/project:package.json",
|
||||
],
|
||||
lifecycle_hooks = {
|
||||
# sleep requires node-gyp rebuild but the native module is not needed in Bazel;
|
||||
# disable the install hook to avoid failing builds when node-gyp is unavailable.
|
||||
"sleep": [],
|
||||
},
|
||||
npmrc = "//:.npmrc",
|
||||
pnpm_lock = "//:pnpm-lock.yaml",
|
||||
)
|
||||
use_repo(npm, "npm")
|
||||
@@ -122,20 +117,22 @@ use_repo(npm, "npm")
|
||||
rules_ts_ext = use_extension("@aspect_rules_ts//ts:extensions.bzl", "ext")
|
||||
rules_ts_ext.deps(
|
||||
name = "angular_npm_typescript",
|
||||
# Obtained by: curl --silent https://registry.npmjs.org/typescript/6.0.2 | jq -r '.dist.integrity'
|
||||
ts_integrity = "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
ts_version = "6.0.3",
|
||||
# Obtained by: curl --silent https://registry.npmjs.org/typescript/5.9.3 | jq -r '.dist.integrity'
|
||||
ts_integrity = "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
ts_version = "5.9.3",
|
||||
)
|
||||
use_repo(rules_ts_ext, **{"npm_typescript": "angular_npm_typescript"})
|
||||
|
||||
# TODO: Figure out how to make ng_project update whenever the packages/core::pkg target changes.
|
||||
rules_angular = use_extension("@rules_angular//setup:extensions.bzl", "rules_angular")
|
||||
rules_angular.setup(
|
||||
name = "angular_rules_angular_configurable_deps",
|
||||
angular_compiler_cli = "//:node_modules/@angular/compiler-cli",
|
||||
typescript = "//:node_modules/typescript",
|
||||
|
||||
use_repo_rule("@rules_angular//setup:repositories.bzl", "configurable_deps_repo")(
|
||||
name = "rules_angular_configurable_deps",
|
||||
angular_compiler_cli = "@angular//:node_modules/@angular/compiler-cli",
|
||||
typescript = "@angular//:node_modules/typescript",
|
||||
)
|
||||
use_repo(rules_angular, rules_angular_configurable_deps = "angular_rules_angular_configurable_deps")
|
||||
|
||||
override_repo(rules_angular, "rules_angular_configurable_deps")
|
||||
|
||||
register_toolchains(
|
||||
"@devinfra//bazel/git-toolchain:git_linux_toolchain",
|
||||
@@ -163,3 +160,22 @@ cldr_xml_data(
|
||||
"https://github.com/unicode-org/cldr/releases/download/release-%s/core.zip" % CLDR_VERSION: "d5ee2abac64158c04884a722f8ef4830ea22b6c74aac20185be2838db8eda788",
|
||||
},
|
||||
)
|
||||
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
# Fetch sauce connect (tool to open Saucelabs tunnel for Saucelabs browser tests)
|
||||
http_archive(
|
||||
name = "sauce_connect_linux_amd64",
|
||||
build_file_content = """exports_files(["bin/sc"], visibility = ["//visibility:public"])""",
|
||||
sha256 = "26b9c3630f441b47854b6032f7eca6f1d88d3f62e50ee44c27015d71a5155c36",
|
||||
strip_prefix = "sc-4.8.2-linux",
|
||||
url = "https://saucelabs.com/downloads/sc-4.8.2-linux.tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "sauce_connect_mac",
|
||||
build_file_content = """exports_files(["bin/sc"], visibility = ["//visibility:public"])""",
|
||||
sha256 = "28277ce81ef9ab84f5b87b526258920a8ead44789a5034346e872629bbf38089",
|
||||
strip_prefix = "sc-4.8.2-osx",
|
||||
url = "https://saucelabs.com/downloads/sc-4.8.2-osx.zip",
|
||||
)
|
||||
|
||||
Generated
+3632
-358
File diff suppressed because it is too large
Load Diff
@@ -67,7 +67,7 @@ Install the Angular CLI globally:
|
||||
npm install -g @angular/cli
|
||||
```
|
||||
|
||||
Create a workspace:
|
||||
Create workspace:
|
||||
|
||||
```
|
||||
ng new [PROJECT NAME]
|
||||
|
||||
+1
-3
@@ -16,7 +16,6 @@ exports_files([
|
||||
APPLICATION_FILES = [
|
||||
"//adev/src/assets/images",
|
||||
"//adev/src/assets/images/v21-event:v21-event-images",
|
||||
"//adev/src/assets/images/v22-event:v22-event-images",
|
||||
"//adev/src/assets/others",
|
||||
"//adev/src/assets/previews",
|
||||
"//adev/src/assets:tutorials",
|
||||
@@ -71,6 +70,7 @@ APPLICATION_DEPS = [
|
||||
":node_modules/fflate",
|
||||
":node_modules/marked",
|
||||
":node_modules/ngx-progressbar",
|
||||
":node_modules/open-in-idx",
|
||||
":node_modules/tailwindcss",
|
||||
":node_modules/typescript",
|
||||
":node_modules/@types/dom-navigation",
|
||||
@@ -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",
|
||||
@@ -98,7 +97,6 @@ APPLICATION_DEPS = [
|
||||
":node_modules/@shikijs/langs",
|
||||
":node_modules/@shikijs/engine-oniguruma",
|
||||
":node_modules/@shikijs/core",
|
||||
":node_modules/@shikijs/primitive",
|
||||
":node_modules/@shikijs/engine-javascript",
|
||||
":node_modules/@shikijs/types",
|
||||
":node_modules/@shikijs/vscode-textmate",
|
||||
|
||||
+9
-6
@@ -7,7 +7,9 @@
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
"style": "scss",
|
||||
"standalone": true,
|
||||
"changeDetection": "OnPush"
|
||||
}
|
||||
},
|
||||
"root": ".",
|
||||
@@ -31,8 +33,7 @@
|
||||
"src/robots.txt",
|
||||
"src/llms.txt",
|
||||
"src/assets",
|
||||
"src/assets/images/v21-event",
|
||||
"src/assets/images/v22-event"
|
||||
"src/assets/images/v21-event"
|
||||
],
|
||||
"styles": ["@angular/docs/styles/global-styles.scss", "./src/local-styles.scss"],
|
||||
"scripts": [],
|
||||
@@ -70,16 +71,18 @@
|
||||
"buildTarget": "web-ui:build",
|
||||
"headers": {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "credentialless"
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test",
|
||||
"options": {
|
||||
"options": {
|
||||
"runner": "karma",
|
||||
"browsers": ["ChromeHeadlessNoSandbox"],
|
||||
"include": ["src/app/**/*.spec.ts"]
|
||||
"include": [
|
||||
"src/app/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "credentialless"
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,7 +71,7 @@
|
||||
},
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "credentialless"
|
||||
"value": "require-corp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+54
-55
@@ -1,105 +1,104 @@
|
||||
{
|
||||
"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.48.0",
|
||||
"@algolia/client-search": "5.48.0",
|
||||
"@algolia/requester-browser-xhr": "5.48.0",
|
||||
"@algolia/requester-node-http": "5.48.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": "21.2.0",
|
||||
"@angular/build": "21.2.0",
|
||||
"@angular/cdk": "21.2.0",
|
||||
"@angular/cli": "21.2.0",
|
||||
"@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": "21.2.0",
|
||||
"@angular/platform-browser": "workspace:*",
|
||||
"@angular/platform-server": "workspace:*",
|
||||
"@angular/router": "workspace:*",
|
||||
"@angular/ssr": "22.2.0-next.7",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.11.0",
|
||||
"@angular/ssr": "21.2.0",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/lang-angular": "0.1.4",
|
||||
"@codemirror/lang-css": "6.3.1",
|
||||
"@codemirror/lang-html": "6.4.12",
|
||||
"@codemirror/lang-javascript": "6.2.5",
|
||||
"@codemirror/lang-html": "6.4.11",
|
||||
"@codemirror/lang-javascript": "6.2.4",
|
||||
"@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",
|
||||
"@lezer/common": "1.5.2",
|
||||
"@lezer/css": "1.3.6",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/lint": "6.9.3",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@lezer/common": "1.5.1",
|
||||
"@lezer/css": "1.3.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lezer/html": "1.3.13",
|
||||
"@lezer/javascript": "1.5.4",
|
||||
"@lezer/lr": "1.4.10",
|
||||
"@lezer/lr": "1.4.8",
|
||||
"@lezer/sass": "1.1.0",
|
||||
"@marijn/find-cluster-break": "1.0.4",
|
||||
"@shikijs/core": "^4.0.0",
|
||||
"@shikijs/engine-javascript": "^4.0.0",
|
||||
"@shikijs/engine-oniguruma": "^4.0.0",
|
||||
"@shikijs/langs": "^4.0.0",
|
||||
"@shikijs/primitive": "^4.0.0",
|
||||
"@shikijs/themes": "^4.0.0",
|
||||
"@shikijs/types": "^4.0.0",
|
||||
"@marijn/find-cluster-break": "1.0.2",
|
||||
"@shikijs/core": "^3.21.0",
|
||||
"@shikijs/engine-javascript": "^3.21.0",
|
||||
"@shikijs/engine-oniguruma": "^3.21.0",
|
||||
"@shikijs/langs": "^3.21.0",
|
||||
"@shikijs/themes": "^3.21.0",
|
||||
"@shikijs/types": "^3.21.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@stackblitz/sdk": "1.11.1",
|
||||
"@types/dom-navigation": "1.0.7",
|
||||
"@stackblitz/sdk": "1.11.0",
|
||||
"@types/dom-navigation": "1.0.6",
|
||||
"@types/jasmine": "6.0.0",
|
||||
"@types/jsdom": "30.0.0",
|
||||
"@types/node": "24.13.3",
|
||||
"@typescript/vfs": "1.6.4",
|
||||
"@webcontainer/api": "1.6.4",
|
||||
"@types/jsdom": "27.0.0",
|
||||
"@types/node": "24.10.11",
|
||||
"@typescript/vfs": "1.6.2",
|
||||
"@webcontainer/api": "1.6.1",
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/xterm": "6.0.0",
|
||||
"algoliasearch": "5.57.0",
|
||||
"algoliasearch": "5.48.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",
|
||||
"comma-separated-tokens": "^2.0.3",
|
||||
"crelt": "1.0.7",
|
||||
"diff": "9.0.0",
|
||||
"crelt": "1.0.6",
|
||||
"diff": "8.0.3",
|
||||
"emoji-regex": "10.6.0",
|
||||
"fflate": "0.8.3",
|
||||
"fflate": "0.8.2",
|
||||
"hast-util-to-html": "^9.0.5",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"html-void-elements": "^3.0.0",
|
||||
"jasmine-core": "6.3.0",
|
||||
"jsdom": "30.0.1",
|
||||
"jsdom": "28.0.0",
|
||||
"jasmine-core": "6.1.0",
|
||||
"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": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"ngx-progressbar": "14.0.0",
|
||||
"playwright-core": "1.62.1",
|
||||
"preact": "10.29.8",
|
||||
"preact-render-to-string": "6.7.0",
|
||||
"prettier": "3.9.6",
|
||||
"open-in-idx": "0.1.1",
|
||||
"playwright-core": "1.58.1",
|
||||
"preact": "10.28.3",
|
||||
"preact-render-to-string": "6.6.6",
|
||||
"prettier": "3.8.1",
|
||||
"property-information": "^7.1.0",
|
||||
"rxjs": "7.8.2",
|
||||
"shiki": "4.4.3",
|
||||
"shiki": "3.22.0",
|
||||
"space-separated-tokens": "^2.0.2",
|
||||
"stringify-entities": "^4.0.4",
|
||||
"style-mod": "4.1.3",
|
||||
"tinyglobby": "0.2.17",
|
||||
"tinyglobby": "0.2.15",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "6.0.3",
|
||||
"typescript": "5.9.3",
|
||||
"w3c-keyname": "2.2.8",
|
||||
"zwitch": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "10.5.4",
|
||||
"autoprefixer": "10.4.24",
|
||||
"karma": "~6.4.4",
|
||||
"postcss": "8.5.26",
|
||||
"postcss": "8.5.6",
|
||||
"tailwindcss": "3.4.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +51,4 @@ js_run_binary(
|
||||
outs = ["defined-routes.json"],
|
||||
chdir = package_name(),
|
||||
tool = ":generate_route",
|
||||
# Public so the api-gen rendering pipeline (used from //packages/...) can validate guide
|
||||
# links against the same set of routes defined for the live site.
|
||||
visibility = [
|
||||
"//adev:__subpackages__",
|
||||
"//packages:__subpackages__",
|
||||
"//tools:__subpackages__",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {ALL_ITEMS} from '../../src/app/routing/navigation-entries/index.js';
|
||||
import {extractHeadingIds, findDuplicateIds} from '../../shared-docs/pipeline/shared/heading.mjs';
|
||||
import {getIdFromHeading} from '../../shared-docs/pipeline/shared/heading.mjs';
|
||||
import {NavigationItem} from '@angular/docs';
|
||||
import {writeFileSync, readFileSync} from 'fs';
|
||||
import {join, resolve} from 'path';
|
||||
@@ -20,7 +20,7 @@ const contentRoot = resolve(process.cwd(), '../../src/content');
|
||||
* in a JSON file. This file then used by other bazel targets to know which routes are valid.
|
||||
*/
|
||||
|
||||
function extractRoutes(items: NavigationItem[], duplicatesByPage: Map<string, string[]>): string[] {
|
||||
function extractRoutes(items: NavigationItem[]): string[] {
|
||||
const routes: string[] = [];
|
||||
for (const item of items) {
|
||||
if (item.path && !item.path.startsWith('http')) {
|
||||
@@ -29,11 +29,7 @@ function extractRoutes(items: NavigationItem[], duplicatesByPage: Map<string, st
|
||||
const content = readFileSync(join(contentRoot, `${item.contentPath}.md`), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const headings = extractHeadingIds(content);
|
||||
const duplicates = findDuplicateIds(headings);
|
||||
if (duplicates.length > 0) {
|
||||
duplicatesByPage.set(`${item.contentPath}.md`, duplicates);
|
||||
}
|
||||
const headings = extractHeadings(content);
|
||||
routes.push(
|
||||
...headings.map(
|
||||
(heading) => `${item.path}#${heading.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
@@ -42,28 +38,33 @@ function extractRoutes(items: NavigationItem[], duplicatesByPage: Map<string, st
|
||||
}
|
||||
}
|
||||
if (item.children) {
|
||||
routes.push(...extractRoutes(item.children, duplicatesByPage));
|
||||
routes.push(...extractRoutes(item.children));
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
function extractHeadings(content: string): string[] {
|
||||
const headings = content
|
||||
.split('\n')
|
||||
// Top level heading (H1) are used for the page title only
|
||||
// and yes, headings can have leading spaces
|
||||
.filter((line) => line.trim().startsWith('##'))
|
||||
.map((line) => line.replace(/^#+\s*/, '').trim());
|
||||
|
||||
const stepRegex = /<docs-step[^>]*title="([^"]*)"/g;
|
||||
let match;
|
||||
while ((match = stepRegex.exec(content)) !== null) {
|
||||
headings.push(match[1]);
|
||||
}
|
||||
|
||||
return headings.map((heading: string) => getIdFromHeading(heading));
|
||||
}
|
||||
|
||||
function main() {
|
||||
const allRoutes: string[] = [];
|
||||
const duplicatesByPage = new Map<string, string[]>();
|
||||
|
||||
allRoutes.push(...extractRoutes(ALL_ITEMS, duplicatesByPage));
|
||||
|
||||
if (duplicatesByPage.size > 0) {
|
||||
const details = Array.from(duplicatesByPage)
|
||||
.map(([page, ids]) => ` ${page}: ${ids.map((id) => `#${id}`).join(', ')}`)
|
||||
.join('\n');
|
||||
throw new Error(
|
||||
`Headings must produce a unique anchor id within a page, otherwise every link to the ` +
|
||||
`anchor resolves to the first heading that claims it. Give the later heading its own ` +
|
||||
`id with the \`{#custom-id}\` syntax.\n${details}`,
|
||||
);
|
||||
}
|
||||
allRoutes.push(...extractRoutes(ALL_ITEMS));
|
||||
|
||||
const uniqueRoutes = Array.from(new Set(allRoutes.filter((r) => !!r)));
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
@@ -10,8 +10,6 @@ import {get} from 'node:https';
|
||||
import {posix} from 'node:path';
|
||||
|
||||
const GITHUB_API = 'https://api.github.com/repos/';
|
||||
const SHA_REGEX = /^[0-9a-f]{40}$/i;
|
||||
const BRANCH_REGEX = /^(?!.*\.\.)[a-zA-Z0-9/_.-]+$/;
|
||||
|
||||
export class GithubClient {
|
||||
#token;
|
||||
@@ -32,12 +30,6 @@ export class GithubClient {
|
||||
* @returns Promise<string[]>
|
||||
*/
|
||||
async getAffectedFiles(baseSha, headSha) {
|
||||
if (!SHA_REGEX.test(baseSha)) {
|
||||
throw new Error(`Invalid base SHA: ${baseSha}`);
|
||||
}
|
||||
if (!SHA_REGEX.test(headSha)) {
|
||||
throw new Error(`Invalid head SHA: ${headSha}`);
|
||||
}
|
||||
const {files} = JSON.parse(await this.#httpGet(`${this.#api}/compare/${baseSha}...${headSha}`));
|
||||
return files.map((f) => f.filename);
|
||||
}
|
||||
@@ -49,9 +41,6 @@ export class GithubClient {
|
||||
* @returns Promise<string>
|
||||
*/
|
||||
async getShaForBranch(branch) {
|
||||
if (!BRANCH_REGEX.test(branch)) {
|
||||
throw new Error(`Invalid branch name: ${branch}`);
|
||||
}
|
||||
const sha = await this.#httpGet(`${this.#api}/commits/${branch}`, {
|
||||
headers: {Accept: 'application/vnd.github.VERSION.sha'},
|
||||
});
|
||||
@@ -60,7 +49,7 @@ export class GithubClient {
|
||||
throw new Error(`Unable to extract the SHA for '${branch}'.`);
|
||||
}
|
||||
|
||||
return sha.trim();
|
||||
return sha;
|
||||
}
|
||||
|
||||
#httpGet(url, options = {}) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
//tslint:disable:no-console
|
||||
import assert from 'node:assert';
|
||||
import {execFileSync} from 'node:child_process';
|
||||
import {execSync} from 'node:child_process';
|
||||
import {existsSync, constants as fsConstants} from 'node:fs';
|
||||
import {
|
||||
copyFile,
|
||||
@@ -41,16 +41,6 @@ export async function updateAssets({repo, assetsPath, destPath}) {
|
||||
await readFile(buildInfoPath, 'utf-8'),
|
||||
);
|
||||
|
||||
const shaRegex = /^[0-9a-f]{40}$/i;
|
||||
const branchRegex = /^(?!.*\.\.)[a-zA-Z0-9/_.-]+$/;
|
||||
|
||||
if (!shaRegex.test(storedSha)) {
|
||||
throw new Error(`Invalid SHA in build info: ${storedSha}`);
|
||||
}
|
||||
if (!branchRegex.test(storedBranch)) {
|
||||
throw new Error(`Invalid branch name in build info: ${storedBranch}`);
|
||||
}
|
||||
|
||||
assert(process.env.ANGULAR_READONLY_GITHUB_TOKEN);
|
||||
const githubApi = new GithubClient(
|
||||
repo,
|
||||
@@ -73,10 +63,6 @@ export async function updateAssets({repo, assetsPath, destPath}) {
|
||||
downstreamBranch = storedBranch;
|
||||
}
|
||||
|
||||
if (!shaRegex.test(latestSha)) {
|
||||
throw new Error(`Invalid SHA resolved: ${latestSha}`);
|
||||
}
|
||||
|
||||
console.log(`Comparing ${storedSha}...${latestSha}.`);
|
||||
const affectedFiles = await githubApi.getAffectedFiles(storedSha, latestSha);
|
||||
const changedFiles = affectedFiles.filter((file) => file.startsWith(`${assetsPath}/`));
|
||||
@@ -92,18 +78,14 @@ export async function updateAssets({repo, assetsPath, destPath}) {
|
||||
|
||||
try {
|
||||
const execOptions = {cwd: temporaryDir, stdio: 'inherit'};
|
||||
execFileSync('git', ['init'], execOptions);
|
||||
execFileSync(
|
||||
'git',
|
||||
['remote', 'add', 'origin', `https://github.com/${repo}.git`],
|
||||
execOptions,
|
||||
);
|
||||
execSync('git init', execOptions);
|
||||
execSync(`git remote add origin https://github.com/${repo}.git`, execOptions);
|
||||
// fetch a commit
|
||||
execFileSync('git', ['fetch', 'origin', latestSha], execOptions);
|
||||
execSync(`git fetch origin ${latestSha}`, execOptions);
|
||||
// reset this repository's main branch to the commit of interest
|
||||
execFileSync('git', ['reset', '--hard', 'FETCH_HEAD'], execOptions);
|
||||
execSync('git reset --hard FETCH_HEAD', execOptions);
|
||||
// get sha when files where changed
|
||||
shaWhenFilesChanged = execFileSync('git', ['rev-list', '-1', latestSha, `${assetsPath}/`], {
|
||||
shaWhenFilesChanged = execSync(`git rev-list -1 ${latestSha} "${assetsPath}/"`, {
|
||||
encoding: 'utf8',
|
||||
cwd: temporaryDir,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-algolia-icon',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './algolia-icon.component.html',
|
||||
})
|
||||
export class AlgoliaIcon {}
|
||||
|
||||
@@ -6,16 +6,17 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Component, computed, inject} from '@angular/core';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {NavigationItem} from '../../interfaces/index';
|
||||
import {ChangeDetectionStrategy, Component, inject, computed} from '@angular/core';
|
||||
import {NavigationState} from '../../services/index';
|
||||
import {NavigationItem} from '../../interfaces/index';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-breadcrumb',
|
||||
imports: [RouterLink],
|
||||
templateUrl: './breadcrumb.component.html',
|
||||
styleUrls: ['./breadcrumb.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Breadcrumb {
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
@if (!hasAccepted()) {
|
||||
<div class="docs-cookies-popup docs-invert-mode">
|
||||
<div class="docs-cookies-popup-header">
|
||||
<img
|
||||
class="docs-cookies-popup-angie"
|
||||
src="assets/images/angie/greeting.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p>This site uses cookies from Google to deliver its services and to analyze traffic.</p>
|
||||
</div>
|
||||
<p>This site uses cookies from Google to deliver its services and to analyze traffic.</p>
|
||||
|
||||
<div class="docs-cookies-popup-actions">
|
||||
<div>
|
||||
<a
|
||||
href="https://policies.google.com/technologies/cookies"
|
||||
target="_blank"
|
||||
|
||||
@@ -15,34 +15,15 @@
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
max-width: 265px;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.docs-cookies-popup-header {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.docs-cookies-popup-angie {
|
||||
flex-shrink: 0;
|
||||
width: 52px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.docs-cookies-popup-actions {
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-block-start: 1rem;
|
||||
|
||||
.docs-primary-btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Component, inject, signal} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core';
|
||||
import {LOCAL_STORAGE} from '../../providers/index';
|
||||
import {setCookieConsent} from '../../utils';
|
||||
|
||||
@@ -21,6 +21,7 @@ export const STORAGE_KEY = 'docs-accepts-cookies';
|
||||
selector: 'docs-cookie-popup',
|
||||
templateUrl: './cookie-popup.component.html',
|
||||
styleUrls: ['./cookie-popup.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CookiePopup {
|
||||
private readonly localStorage = inject(LOCAL_STORAGE);
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectionStrategy, Component, inject, input, signal} from '@angular/core';
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
import {Component, inject, input, signal} from '@angular/core';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
@@ -15,7 +15,7 @@ export const CONFIRMATION_DISPLAY_TIME_MS = 1000;
|
||||
|
||||
@Component({
|
||||
selector: 'docs-copy-link-button',
|
||||
template: `<docs-icon></docs-icon>`,
|
||||
template: `<docs-icon>{{ showCopySuccess() ? 'check' : 'link' }}</docs-icon>`,
|
||||
styles: `
|
||||
:host {
|
||||
cursor: pointer;
|
||||
@@ -31,13 +31,6 @@ export const CONFIRMATION_DISPLAY_TIME_MS = 1000;
|
||||
:host(.docs-copy-link-success) {
|
||||
color: var(--bright-blue);
|
||||
}
|
||||
docs-icon::before {
|
||||
content: '\\e250'; /* codepoint for "link" */
|
||||
font-family: 'Material Symbols Outlined';
|
||||
}
|
||||
:host(.docs-copy-link-success) docs-icon::before {
|
||||
content: '\\e668'; /* codepoint for "check" */
|
||||
}
|
||||
`,
|
||||
hostDirectives: [
|
||||
{
|
||||
@@ -52,6 +45,7 @@ export const CONFIRMATION_DISPLAY_TIME_MS = 1000;
|
||||
'[class.docs-copy-link-success]': 'showCopySuccess()',
|
||||
},
|
||||
imports: [IconComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CopyLinkButton {
|
||||
readonly href = input.required<string>();
|
||||
|
||||
+4
-3
@@ -8,13 +8,13 @@
|
||||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {
|
||||
CONFIRMATION_DISPLAY_TIME_MS,
|
||||
CopySourceCodeButton,
|
||||
} from './copy-source-code-button.component';
|
||||
import {ChangeDetectionStrategy, Component, signal} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
|
||||
const SUCCESSFULLY_COPY_CLASS_NAME = 'docs-copy-source-code-button-success';
|
||||
const FAILED_COPY_CLASS_NAME = 'docs-copy-source-code-button-failed';
|
||||
@@ -99,6 +99,7 @@ describe('CopySourceCodeButton', () => {
|
||||
<button docs-copy-source-code></button>
|
||||
`,
|
||||
imports: [CopySourceCodeButton],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class CodeSnippetWrapper {
|
||||
code = signal('');
|
||||
|
||||
+3
-1
@@ -6,8 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
export const REMOVED_LINE_CLASS_NAME = '.line.remove';
|
||||
@@ -32,6 +33,7 @@ export const CONFIRMATION_DISPLAY_TIME_MS = 2000;
|
||||
'[class.docs-copy-source-code-button-success]': 'showCopySuccess()',
|
||||
'[class.docs-copy-source-code-button-failed]': 'showCopyFailure()',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CopySourceCodeButton {
|
||||
private readonly changeDetector = inject(ChangeDetectorRef);
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
*/
|
||||
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {Component, afterNextRender, computed, inject, signal} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
afterNextRender,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-icon',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'class': 'material-symbols-outlined',
|
||||
'[style.font-size.px]': 'fontSize()',
|
||||
|
||||
@@ -24,7 +24,6 @@ ng_project(
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/pipes",
|
||||
"//adev/shared-docs/services",
|
||||
"//adev/shared-docs/utils",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -9,17 +9,11 @@
|
||||
>
|
||||
@for (itemGroup of groupItems(navigationItems, preserveOtherCategoryOrder); track $index) {
|
||||
@let groupLabel = itemGroup[0];
|
||||
@let group = itemGroup[1];
|
||||
@let items = group.items;
|
||||
@let items = itemGroup[1];
|
||||
@let firstItem = items[0];
|
||||
|
||||
@if (groupLabel && collapsableLevel() !== firstItem.level && items.length > 1) {
|
||||
<li class="docs-navigation-group">
|
||||
<span>{{ groupLabel }}</span>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="itemStatus; context: {$implicit: {status: group.status}}"
|
||||
/>
|
||||
</li>
|
||||
<li class="docs-navigation-group">{{ groupLabel }}</li>
|
||||
}
|
||||
@for (item of items; track $index) {
|
||||
<li
|
||||
@@ -33,7 +27,7 @@
|
||||
[href]="item.path"
|
||||
target="_blank"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="isMobile || itemLabel.length < labelTruncationThreshold"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
@@ -59,9 +53,9 @@
|
||||
matrixParams: 'ignored',
|
||||
fragment: 'ignored',
|
||||
}"
|
||||
(click)="emitClickOnLink(item)"
|
||||
(click)="emitClickOnLink()"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="isMobile || itemLabel.length < labelTruncationThreshold"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
@@ -82,7 +76,7 @@
|
||||
<div
|
||||
class="docs-secondary-nav-header"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="isMobile || itemLabel.length < labelTruncationThreshold"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
@@ -112,7 +106,7 @@
|
||||
item.children && item.level === expandableLevel() && !item.isExpanded
|
||||
"
|
||||
[matTooltip]="item.label"
|
||||
[matTooltipDisabled]="isMobile || itemLabel.length < labelTruncationThreshold"
|
||||
[matTooltipDisabled]="itemLabel.length < 27"
|
||||
matTooltipPosition="after"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltipClass]="'API-tooltip'"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
list-style: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: 100dvh;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-block: 1.5rem;
|
||||
@@ -162,12 +162,11 @@ a,
|
||||
}
|
||||
}
|
||||
|
||||
.docs-external-link.docs-faceted-list-item-text {
|
||||
.docs-external-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
gap: 0.5rem;
|
||||
&::after {
|
||||
content: 'open_in_new';
|
||||
@@ -175,11 +174,7 @@ a,
|
||||
font-size: 1.1rem;
|
||||
color: var(--quinary-contrast);
|
||||
transition: color 0.3s ease;
|
||||
margin-inline-end: 0.2rem;
|
||||
}
|
||||
|
||||
a:hover &::after {
|
||||
color: var(--primary-contrast);
|
||||
margin-inline-end: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,12 +188,8 @@ a,
|
||||
}
|
||||
|
||||
.docs-navigation-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
background: var(--septenary-contrast);
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@@ -31,32 +31,6 @@ const navigationItems: NavigationItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const navigationItemsWithCategoryStatus: NavigationItem[] = [
|
||||
{
|
||||
label: 'Forms',
|
||||
level: 1,
|
||||
categoriesStatus: [{'Signal Forms': 'new'}],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
navigationItemsWithCategoryStatus[0].children = [
|
||||
{
|
||||
label: 'Overview',
|
||||
path: 'guide/forms',
|
||||
level: 2,
|
||||
category: 'Signal Forms',
|
||||
parent: navigationItemsWithCategoryStatus[0],
|
||||
},
|
||||
{
|
||||
label: 'Signals',
|
||||
path: 'guide/forms/signals',
|
||||
level: 2,
|
||||
category: 'Signal Forms',
|
||||
parent: navigationItemsWithCategoryStatus[0],
|
||||
},
|
||||
];
|
||||
|
||||
describe('NavigationList', () => {
|
||||
let component: NavigationList;
|
||||
let fixture: ComponentFixture<NavigationList>;
|
||||
@@ -84,20 +58,6 @@ describe('NavigationList', () => {
|
||||
expect(nonClickableItem.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should show category status on grouped navigation headers', async () => {
|
||||
fixture.componentRef.setInput('navigationItems', [...navigationItemsWithCategoryStatus]);
|
||||
await fixture.whenStable();
|
||||
|
||||
const categoryGroup = fixture.debugElement.query(By.css('.docs-navigation-group'));
|
||||
const categoryStatus = fixture.debugElement.query(
|
||||
By.css('.docs-navigation-group .docs-new-item'),
|
||||
);
|
||||
|
||||
expect(categoryGroup.nativeElement.innerText).toContain('Signal Forms');
|
||||
expect(categoryStatus).toBeTruthy();
|
||||
expect(categoryStatus.nativeElement.innerText).toBe('New');
|
||||
});
|
||||
|
||||
it('should append `docs-navigation-list-dropdown` when isDropdownView is true', async () => {
|
||||
fixture.componentRef.setInput('isDropdownView', true);
|
||||
await fixture.whenStable();
|
||||
@@ -193,6 +153,5 @@ describe('NavigationList', () => {
|
||||
class FakeNavigationListState {
|
||||
isOpened = signal(true);
|
||||
activeNavigationItem = signal(navigationItems.at(1));
|
||||
crossCategoryOrigin = signal<NavigationItem | undefined>(undefined);
|
||||
toggleItem(item: NavigationItem) {}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
*/
|
||||
|
||||
import {NgTemplateOutlet} from '@angular/common';
|
||||
import {Component, inject, input, output} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, inject, input, output} from '@angular/core';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {RouterLink, RouterLinkActive} from '@angular/router';
|
||||
import {NavigationItem} from '../../interfaces/index';
|
||||
import {IsActiveNavigationItem} from '../../pipes';
|
||||
import {NavigationState} from '../../services/index';
|
||||
import {isMobile} from '../../utils';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
@Component({
|
||||
@@ -28,6 +27,7 @@ import {IconComponent} from '../icon/icon.component';
|
||||
],
|
||||
templateUrl: './navigation-list.component.html',
|
||||
styleUrls: ['./navigation-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NavigationList {
|
||||
readonly navigationItems = input.required<NavigationItem[]>();
|
||||
@@ -39,15 +39,10 @@ export class NavigationList {
|
||||
|
||||
readonly linkClicked = output<void>();
|
||||
|
||||
protected readonly labelTruncationThreshold = 27;
|
||||
|
||||
private readonly navigationState = inject(NavigationState);
|
||||
private readonly crossCategoryOrigin = this.navigationState.crossCategoryOrigin;
|
||||
|
||||
readonly activeItem = this.navigationState.activeNavigationItem;
|
||||
|
||||
protected readonly isMobile = isMobile;
|
||||
|
||||
toggle(item: NavigationItem): void {
|
||||
if (
|
||||
item.level === 1 &&
|
||||
@@ -56,19 +51,10 @@ export class NavigationList {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const prevParentItem = this.crossCategoryOrigin();
|
||||
if (prevParentItem) {
|
||||
this.crossCategoryOrigin.set(undefined);
|
||||
this.navigationState.toggleItem(prevParentItem);
|
||||
return;
|
||||
}
|
||||
this.navigationState.toggleItem(item);
|
||||
}
|
||||
|
||||
emitClickOnLink(item: NavigationItem): void {
|
||||
if (item.isCrossReferenced) {
|
||||
this.crossCategoryOrigin.set(item.parent);
|
||||
}
|
||||
emitClickOnLink(): void {
|
||||
this.linkClicked.emit();
|
||||
}
|
||||
|
||||
@@ -76,27 +62,14 @@ export class NavigationList {
|
||||
return items.some((item) => !!item.category);
|
||||
}
|
||||
|
||||
private getCategoryStatus(item: NavigationItem, category: string): 'new' | 'updated' | undefined {
|
||||
const categoriesStatus = item.parent?.categoriesStatus;
|
||||
|
||||
if (!categoriesStatus) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return categoriesStatus.find((status) => status[category])?.[category];
|
||||
}
|
||||
|
||||
protected groupItems(
|
||||
items: NavigationItem[],
|
||||
preserveOtherCategoryOrder: boolean,
|
||||
): Map<string, {items: NavigationItem[]; status: 'new' | 'updated' | undefined}> {
|
||||
): Map<string, NavigationItem[]> {
|
||||
const hasCategories = this.hasCategories(items);
|
||||
if (hasCategories) {
|
||||
const others: NavigationItem[] = [];
|
||||
const categorizedItems = new Map<
|
||||
string,
|
||||
{items: NavigationItem[]; status: 'new' | 'updated' | undefined}
|
||||
>();
|
||||
const categorizedItems = new Map<string, NavigationItem[]>();
|
||||
for (const item of items) {
|
||||
const category = item.category || 'Other';
|
||||
if (!preserveOtherCategoryOrder && category === 'Other') {
|
||||
@@ -104,20 +77,16 @@ export class NavigationList {
|
||||
continue;
|
||||
}
|
||||
if (!categorizedItems.has(category)) {
|
||||
categorizedItems.set(category, {
|
||||
items: [],
|
||||
status: this.getCategoryStatus(item, category),
|
||||
});
|
||||
categorizedItems.set(category, []);
|
||||
}
|
||||
categorizedItems.get(category)!.items.push(item);
|
||||
categorizedItems.get(category)!.push(item);
|
||||
}
|
||||
if (others.length) {
|
||||
categorizedItems.set('Other', {items: others, status: undefined});
|
||||
categorizedItems.set('Other', others);
|
||||
}
|
||||
|
||||
return categorizedItems;
|
||||
} else {
|
||||
return new Map([['', {items, status: undefined}]]);
|
||||
return new Map([['', items]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ ng_project(
|
||||
"//adev/shared-docs/pipes",
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/services",
|
||||
"//adev/shared-docs/utils",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<dialog #searchDialog id="docsSearchDialog">
|
||||
<dialog #searchDialog>
|
||||
<div class="docs-search-container" (docsClickOutside)="closeSearchDialog()">
|
||||
<docs-text-field
|
||||
[autofocus]="true"
|
||||
@@ -27,16 +27,14 @@
|
||||
</i>
|
||||
</span>
|
||||
|
||||
<!-- Page title and package badge -->
|
||||
<span class="docs-search-result__label__text">
|
||||
<span [innerHtml]="result.labelHtml"></span>
|
||||
@if (result.package) {
|
||||
<span
|
||||
[innerHTML]="result.package"
|
||||
class="docs-search-result__label__package"
|
||||
></span>
|
||||
}
|
||||
</span>
|
||||
<!-- Page title -->
|
||||
<span [innerHtml]="result.labelHtml"></span>
|
||||
@if (result.package) {
|
||||
<span
|
||||
[innerHTML]="result.package"
|
||||
class="docs-search-result__label__package"
|
||||
></span>
|
||||
}
|
||||
</p>
|
||||
|
||||
@if (result.subLabelHtml) {
|
||||
@@ -65,22 +63,12 @@
|
||||
<docs-search-history />
|
||||
} @else {
|
||||
<div class="docs-search-results docs-mini-scroll-track">
|
||||
@if (emptyState() === 'start') {
|
||||
@if (!resultsResource.hasValue()) {
|
||||
<div class="docs-search-results__start-typing">
|
||||
<img
|
||||
src="assets/images/angie/magnifying-glass.svg"
|
||||
class="docs-search-angie"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>Start typing to see results</span>
|
||||
</div>
|
||||
} @else {
|
||||
} @else if (searchResults().length === 0) {
|
||||
<div class="docs-search-results__no-results">
|
||||
<img
|
||||
src="assets/images/angie/question.svg"
|
||||
class="docs-search-angie"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ dialog {
|
||||
|
||||
.docs-search-result-icon {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
@@ -92,20 +91,7 @@ dialog {
|
||||
|
||||
&__label {
|
||||
font-weight: 600;
|
||||
|
||||
&__text {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem 0.5rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__package {
|
||||
font-size: 0.75rem;
|
||||
@@ -150,20 +136,10 @@ dialog {
|
||||
|
||||
.docs-search-results__start-typing,
|
||||
.docs-search-results__no-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
color: var(--quaternary-contrast);
|
||||
}
|
||||
|
||||
.docs-search-angie {
|
||||
width: 210px;
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
margin-block: -1rem -0.5rem;
|
||||
}
|
||||
|
||||
.docs-search-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {Router, provideRouter} from '@angular/router';
|
||||
import {SearchDialog} from './search-dialog.component';
|
||||
import {ENVIRONMENT, WINDOW} from '../../providers';
|
||||
import {ALGOLIA_CLIENT, Search} from '../../services';
|
||||
import {FakeEventTarget, timeout, useAutoTick} from '../../testing/index';
|
||||
import {FakeEventTarget} from '../../testing/index';
|
||||
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
|
||||
import {SearchResult} from '../../interfaces';
|
||||
|
||||
@@ -27,10 +27,8 @@ describe('SearchDialog', () => {
|
||||
|
||||
let search: Search;
|
||||
|
||||
useAutoTick();
|
||||
|
||||
beforeEach(async () => {
|
||||
searchResults.and.returnValue(Promise.resolve({results: [{hits: []}]}));
|
||||
searchResults.and.returnValue([]);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SearchDialog],
|
||||
@@ -57,9 +55,6 @@ describe('SearchDialog', () => {
|
||||
// Fire the request
|
||||
TestBed.inject(ApplicationRef).tick();
|
||||
|
||||
// The delay from debounced (200ms)
|
||||
await timeout(300);
|
||||
|
||||
// Wait for the resource to resolve
|
||||
await TestBed.inject(ApplicationRef).whenStable();
|
||||
|
||||
@@ -90,9 +85,6 @@ describe('SearchDialog', () => {
|
||||
// Fire the request
|
||||
TestBed.inject(ApplicationRef).tick();
|
||||
|
||||
// The delay from debounced (200ms)
|
||||
await timeout(300);
|
||||
|
||||
// Wait for the resource to resolve
|
||||
await TestBed.inject(ApplicationRef).whenStable();
|
||||
|
||||
@@ -104,7 +96,7 @@ describe('SearchDialog', () => {
|
||||
});
|
||||
|
||||
it('should display `Start typing to see results` message when there are no provided query', () => {
|
||||
searchResults.and.returnValue(Promise.resolve(undefined));
|
||||
searchResults.and.returnValue(undefined);
|
||||
|
||||
const startTypingContainer = fixture.debugElement.query(
|
||||
By.css('.docs-search-results__start-typing'),
|
||||
@@ -113,35 +105,6 @@ describe('SearchDialog', () => {
|
||||
expect(startTypingContainer).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should keep the `No results found` message while re-querying instead of flickering back to `Start typing`', async () => {
|
||||
const appRef = TestBed.inject(ApplicationRef);
|
||||
|
||||
// Settle on an empty result set so `No results found` is shown.
|
||||
search.searchQuery.set('foobarbaz');
|
||||
searchResults.and.returnValue(Promise.resolve({results: [{hits: []}]}));
|
||||
appRef.tick();
|
||||
await timeout(300);
|
||||
await appRef.whenStable();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('.docs-search-results__no-results'))).toBeTruthy();
|
||||
|
||||
// Edit the query so a new search goes in flight and stays pending.
|
||||
let resolveReload!: (value: unknown) => void;
|
||||
searchResults.and.returnValue(new Promise((resolve) => (resolveReload = resolve)));
|
||||
search.searchQuery.set('foobarba');
|
||||
appRef.tick();
|
||||
|
||||
// The delay from debounced (200ms), after which the pending request is loading.
|
||||
await timeout(300);
|
||||
|
||||
// While the re-query is loading it must not revert to the `Start typing` state.
|
||||
expect(fixture.debugElement.query(By.css('.docs-search-results__start-typing'))).toBeNull();
|
||||
expect(fixture.debugElement.query(By.css('.docs-search-results__no-results'))).toBeTruthy();
|
||||
|
||||
resolveReload({results: [{hits: []}]});
|
||||
await appRef.whenStable();
|
||||
});
|
||||
|
||||
it('should display list of the search results when results exist', async () => {
|
||||
search.searchQuery.set('fakeQuery');
|
||||
searchResults.and.returnValue(Promise.resolve({results: [{hits: fakeSearchResults}]}));
|
||||
@@ -149,9 +112,6 @@ describe('SearchDialog', () => {
|
||||
// Fire the request
|
||||
TestBed.inject(ApplicationRef).tick();
|
||||
|
||||
// The delay from debounced (200ms)
|
||||
await timeout(300);
|
||||
|
||||
// Wait for the resource to resolve
|
||||
await TestBed.inject(ApplicationRef).whenStable();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import {
|
||||
afterNextRender,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
effect,
|
||||
@@ -32,10 +33,10 @@ import {RelativeLink} from '../../pipes';
|
||||
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
|
||||
import {SearchHistoryComponent} from '../search-history/search-history.component';
|
||||
import {TextField} from '../text-field/text-field.component';
|
||||
import {getRelativeUrl} from '../../utils';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-search-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ClickOutside,
|
||||
TextField,
|
||||
@@ -56,7 +57,7 @@ export class SearchDialog {
|
||||
|
||||
readonly history = inject(SearchHistory);
|
||||
private readonly search = inject(Search);
|
||||
|
||||
private readonly relativeLink = new RelativeLink();
|
||||
private readonly router = inject(Router);
|
||||
private readonly window = inject(WINDOW);
|
||||
private readonly injector = inject(Injector);
|
||||
@@ -67,7 +68,6 @@ export class SearchDialog {
|
||||
|
||||
readonly resultsResource = this.search.resultsResource;
|
||||
readonly searchResults = this.search.searchResults;
|
||||
readonly emptyState = this.search.emptyState;
|
||||
|
||||
searchForm = form(this.search.searchQuery);
|
||||
|
||||
@@ -122,7 +122,7 @@ export class SearchDialog {
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigateByUrl(getRelativeUrl(activeItemLink));
|
||||
this.router.navigateByUrl(this.relativeLink.transform(activeItemLink));
|
||||
this.onClose.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ ng_project(
|
||||
"//adev/shared-docs/directives",
|
||||
"//adev/shared-docs/pipes",
|
||||
"//adev/shared-docs/services",
|
||||
"//adev/shared-docs/utils",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
|
||||
import {NgTemplateOutlet} from '@angular/common';
|
||||
import {
|
||||
afterNextRender,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
effect,
|
||||
@@ -17,13 +16,14 @@ import {
|
||||
Injector,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
|
||||
import {NgTemplateOutlet} from '@angular/common';
|
||||
|
||||
import {SearchItem} from '../../directives';
|
||||
import {RelativeLink} from '../../pipes';
|
||||
import {SearchHistory} from '../../services';
|
||||
import {getRelativeUrl} from '../../utils';
|
||||
import {RelativeLink} from '../../pipes';
|
||||
import {SearchItem} from '../../directives';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-search-history',
|
||||
@@ -34,6 +34,7 @@ import {getRelativeUrl} from '../../utils';
|
||||
'(document:keydown)': 'onKeydown($event)',
|
||||
'(document:mousemove)': 'onMouseMove($event)',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SearchHistoryComponent {
|
||||
protected readonly items = viewChildren(SearchItem);
|
||||
@@ -42,6 +43,7 @@ export class SearchHistoryComponent {
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
private readonly relativeLink = new RelativeLink();
|
||||
private readonly keyManager = new ActiveDescendantKeyManager(
|
||||
this.items,
|
||||
this.injector,
|
||||
@@ -99,7 +101,7 @@ export class SearchHistoryComponent {
|
||||
const activeItemLink = this.keyManager.activeItem?.item()?.url;
|
||||
|
||||
if (activeItemLink) {
|
||||
const url = getRelativeUrl(activeItemLink);
|
||||
const url = this.relativeLink.transform(activeItemLink);
|
||||
this.router.navigateByUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ ng_project(
|
||||
],
|
||||
deps = [
|
||||
"//adev:node_modules/@angular/aria",
|
||||
"//adev:node_modules/@angular/cdk",
|
||||
"//adev:node_modules/@angular/common",
|
||||
"//adev:node_modules/@angular/core",
|
||||
"//adev:node_modules/@angular/forms",
|
||||
|
||||
@@ -1,94 +1,61 @@
|
||||
<div ngCombobox #combobox="ngCombobox" class="docs-combobox-container" [(expanded)]="popupExpanded">
|
||||
<div #origin class="docs-select-input-container">
|
||||
<div ngCombobox #outerCombobox="ngCombobox" [readonly]="true" [disabled]="disabled()">
|
||||
<div class="docs-select-input-container">
|
||||
<input
|
||||
ngComboboxInput
|
||||
[attr.id]="id()"
|
||||
[attr.name]="name()"
|
||||
[value]="displayValue()"
|
||||
placeholder="Select an option"
|
||||
placeholder="Select an option..."
|
||||
readonly
|
||||
[tabindex]="-1"
|
||||
/>
|
||||
<span class="material-symbols-outlined docs-select-arrow" translate="no" aria-hidden="true"
|
||||
>arrow_drop_down</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[cdkConnectedOverlay]="{origin: combobox.element, usePopover: 'inline', matchWidth: true}"
|
||||
[cdkConnectedOverlayOpen]="popupExpanded()"
|
||||
[cdkConnectedOverlayDisableClose]="false"
|
||||
(overlayOutsideClick)="popupExpanded.set(false)"
|
||||
>
|
||||
<ng-template ngComboboxPopup [combobox]="combobox" popupType="dialog">
|
||||
<div class="docs-select-popover">
|
||||
<div class="docs-select-dialog" ngComboboxWidget>
|
||||
<div class="example-combobox-container">
|
||||
<div class="docs-select-search-container">
|
||||
<span
|
||||
class="material-symbols-outlined docs-select-search-icon"
|
||||
translate="no"
|
||||
aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
ngCombobox
|
||||
#innerCombobox="ngCombobox"
|
||||
#searchInput
|
||||
class="docs-select-search-input"
|
||||
placeholder="Search..."
|
||||
[(value)]="searchString"
|
||||
[alwaysExpanded]="true"
|
||||
(keydown.escape)="onSearchEscape($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div aria-live="polite" class="cdk-visually-hidden">
|
||||
{{ filteredOptions().length === 0 ? 'No results found for ' + searchString() : '' }}
|
||||
</div>
|
||||
|
||||
<ng-template ngComboboxPopup [combobox]="innerCombobox">
|
||||
<div class="example-popup example-popup-no-margin">
|
||||
@if (filteredOptions().length === 0) {
|
||||
<div class="docs-select-no-results">
|
||||
<img
|
||||
src="assets/images/angie/question.svg"
|
||||
class="docs-select-no-results-angie"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
#listbox="ngListbox"
|
||||
ngListbox
|
||||
[(value)]="selectedValues"
|
||||
[multi]="false"
|
||||
ngComboboxWidget
|
||||
class="docs-select-listbox"
|
||||
focusMode="activedescendant"
|
||||
tabindex="-1"
|
||||
selectionMode="explicit"
|
||||
(click)="onCommit()"
|
||||
(keydown.enter)="onCommit()"
|
||||
[activeDescendant]="listbox.activeDescendant()"
|
||||
[class.example-empty]="filteredOptions().length === 0"
|
||||
>
|
||||
@for (option of filteredOptions(); track option.value) {
|
||||
<div
|
||||
ngOption
|
||||
[value]="option.value"
|
||||
[label]="option.label"
|
||||
class="docs-select-option"
|
||||
>
|
||||
<span class="docs-select-option-label">{{ option.label }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
<dialog ngComboboxDialog class="docs-select-dialog">
|
||||
<div ngCombobox #innerCombobox="ngCombobox" filterMode="manual" [alwaysExpanded]="true">
|
||||
<div class="docs-select-search-container">
|
||||
<span
|
||||
class="material-symbols-outlined docs-select-search-icon"
|
||||
translate="no"
|
||||
aria-hidden="true"
|
||||
>search</span
|
||||
>
|
||||
<input
|
||||
ngComboboxInput
|
||||
class="docs-select-search-input"
|
||||
placeholder="Search..."
|
||||
[(value)]="searchString"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ng-template ngComboboxPopupContainer>
|
||||
@if (filteredOptions().length === 0) {
|
||||
<div class="docs-select-no-results">No results found</div>
|
||||
}
|
||||
|
||||
<div ngListbox [(values)]="selectedValues" class="docs-select-listbox">
|
||||
@for (option of filteredOptions(); track option.value) {
|
||||
<div
|
||||
ngOption
|
||||
[value]="option.value"
|
||||
[label]="option.label"
|
||||
class="docs-select-option"
|
||||
>
|
||||
<span class="docs-select-option-label">{{ option.label }}</span>
|
||||
<span
|
||||
class="material-symbols-outlined docs-select-check-icon"
|
||||
translate="no"
|
||||
aria-hidden="true"
|
||||
>check</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
</dialog>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
:host {
|
||||
--border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));
|
||||
}
|
||||
|
||||
[ngCombobox] {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.docs-combobox-container {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.docs-select-input-container {
|
||||
@@ -18,25 +16,15 @@
|
||||
position: relative;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
padding: 0.7rem 1rem 0.7rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
color: var(--primary-contrast);
|
||||
[ngComboboxInput] {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&[readonly] {
|
||||
cursor: pointer;
|
||||
padding: 0.7rem 1rem;
|
||||
}
|
||||
|
||||
&[aria-expanded='true'] + .docs-select-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
[ngComboboxInput][readonly='true'] {
|
||||
cursor: pointer;
|
||||
padding: 0.7rem 1rem;
|
||||
}
|
||||
|
||||
[ngCombobox]:focus-within [ngComboboxInput]:not(.docs-select-search-input) {
|
||||
@@ -44,10 +32,6 @@
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--vivid-pink) 25%, transparent);
|
||||
}
|
||||
|
||||
.docs-select-popover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.docs-select-arrow {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@@ -62,19 +46,22 @@
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
[ngComboboxInput] [ngCombobox] {
|
||||
[ngComboboxInput][aria-expanded='true'] + .docs-select-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
[ngComboboxInput] {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
padding: 0.7rem 1rem 0.7rem;
|
||||
padding: 0.7rem 1rem 0.7rem 2.5rem;
|
||||
background-color: var(--septenary-contrast);
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
|
||||
.docs-select-dialog {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
left: auto;
|
||||
right: auto;
|
||||
top: auto;
|
||||
@@ -82,7 +69,6 @@
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
background-color: var(--septenary-contrast);
|
||||
color: inherit;
|
||||
|
||||
@@ -149,10 +135,6 @@
|
||||
}
|
||||
|
||||
.docs-select-no-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
@@ -160,18 +142,11 @@
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.docs-select-no-results-angie {
|
||||
width: 88px;
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.docs-select-listbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 12rem;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -196,8 +171,16 @@
|
||||
color: var(--vivid-pink);
|
||||
background-color: color-mix(in srgb, var(--vivid-pink) 5%, transparent);
|
||||
}
|
||||
|
||||
&:not([aria-selected='true']) .docs-select-check-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-select-option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.docs-select-check-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
@@ -6,21 +6,28 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/combobox';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxDialog,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
} from '@angular/aria/combobox';
|
||||
import {Listbox, Option} from '@angular/aria/listbox';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
model,
|
||||
signal,
|
||||
untracked,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import {FormValueControl} from '@angular/forms/signals';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
type SelectOptionValue = string;
|
||||
type SelectOptionValue = string | number | boolean;
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
@@ -29,9 +36,18 @@ export interface SelectOption {
|
||||
|
||||
@Component({
|
||||
selector: 'docs-select',
|
||||
templateUrl: 'select.component.html',
|
||||
styleUrl: 'select.component.css',
|
||||
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
Combobox,
|
||||
ComboboxDialog,
|
||||
ComboboxInput,
|
||||
ComboboxPopupContainer,
|
||||
FormsModule,
|
||||
Listbox,
|
||||
Option,
|
||||
],
|
||||
templateUrl: './select.component.html',
|
||||
styleUrls: ['./select.component.scss'],
|
||||
})
|
||||
export class Select implements FormValueControl<string | null> {
|
||||
readonly value = model<string | null>(null);
|
||||
@@ -39,13 +55,14 @@ export class Select implements FormValueControl<string | null> {
|
||||
readonly id = input.required<string>({alias: 'selectId'});
|
||||
readonly name = input.required<string>();
|
||||
readonly options = input.required<SelectOption[]>();
|
||||
readonly disabled = input(false);
|
||||
|
||||
readonly listbox = viewChild(Listbox);
|
||||
readonly combobox = viewChild(Combobox);
|
||||
readonly dialog = viewChild(ComboboxDialog);
|
||||
readonly listbox = viewChild<Listbox<SelectOptionValue>>(Listbox);
|
||||
readonly combobox = viewChild<Combobox<SelectOptionValue>>(Combobox);
|
||||
|
||||
readonly searchString = signal('');
|
||||
|
||||
readonly popupExpanded = signal(false);
|
||||
readonly filteredOptions = computed(() => {
|
||||
const search = this.searchString().toLowerCase();
|
||||
if (!search) {
|
||||
@@ -67,36 +84,39 @@ export class Select implements FormValueControl<string | null> {
|
||||
|
||||
constructor() {
|
||||
afterRenderEffect(() => {
|
||||
this.listbox()?.scrollActiveItemIntoView();
|
||||
if (this.dialog() && this.combobox()?.expanded()) {
|
||||
untracked(() => this.listbox()?.gotoFirst());
|
||||
this.positionDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommit() {
|
||||
const values = this.selectedValues();
|
||||
if (values.length) {
|
||||
this.value.set(values[0]);
|
||||
this.popupExpanded.set(false);
|
||||
afterRenderEffect(() => {
|
||||
const selected = this.selectedValues();
|
||||
if (selected.length > 0) {
|
||||
untracked(() => this.dialog()?.close());
|
||||
this.value.set(selected[0] as string);
|
||||
this.searchString.set('');
|
||||
}
|
||||
});
|
||||
|
||||
afterRenderEffect(() => this.listbox()?.scrollActiveItemIntoView());
|
||||
}
|
||||
// TODO: Improve once CDK overlay is fixed https://github.com/angular/components/issues/32504
|
||||
private positionDialog(): void {
|
||||
const dialog = this.dialog();
|
||||
const combobox = this.combobox();
|
||||
|
||||
if (!dialog || !combobox) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** Dismisses the dialog overlay on Escape key. */
|
||||
onSearchEscape(event: Event) {
|
||||
this.popupExpanded.set(false);
|
||||
this.combobox()?.element.focus();
|
||||
}
|
||||
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
|
||||
const scrollY = window.scrollY;
|
||||
|
||||
/** Handles keydown events on the clear button. */
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
if (event.key === 'Enter') {
|
||||
this.clear();
|
||||
this.popupExpanded.set(false);
|
||||
event.stopPropagation();
|
||||
if (comboboxRect) {
|
||||
dialog.element.style.width = `${comboboxRect.width}px`;
|
||||
dialog.element.style.top = `${comboboxRect.bottom + scrollY + 4}px`;
|
||||
dialog.element.style.left = `${comboboxRect.left}px`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears the search query and all selected options. */
|
||||
clear(): void {
|
||||
this.searchString.set('');
|
||||
this.value.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Component, input, model} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, input, model} from '@angular/core';
|
||||
import {FormCheckboxControl} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-slide-toggle',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './slide-toggle.component.html',
|
||||
styleUrls: ['./slide-toggle.component.scss'],
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
ElementRef,
|
||||
@@ -32,6 +33,7 @@ let idCounter = 0;
|
||||
host: {
|
||||
class: 'docs-tab-group',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TabGroup {
|
||||
private readonly _renderer = inject(Renderer2);
|
||||
|
||||
@@ -6,14 +6,23 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
input,
|
||||
inject,
|
||||
afterNextRender,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import {Location, ViewportScroller} from '@angular/common';
|
||||
import {afterNextRender, Component, DestroyRef, inject, input, signal} from '@angular/core';
|
||||
import {TableOfContentsLevel} from '../../interfaces/index';
|
||||
import {TableOfContentsLoader} from '../../services';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-table-of-contents',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './table-of-contents.component.html',
|
||||
styleUrls: ['./table-of-contents.component.scss'],
|
||||
imports: [IconComponent],
|
||||
|
||||
@@ -6,12 +6,21 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {afterNextRender, Component, ElementRef, input, model, viewChild} from '@angular/core';
|
||||
import {
|
||||
afterNextRender,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
input,
|
||||
model,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import {FormValueControl} from '@angular/forms/signals';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-text-field',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [IconComponent],
|
||||
templateUrl: './text-field.component.html',
|
||||
styleUrls: ['./text-field.component.scss'],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Component, inject, input, linkedSignal} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, inject, input, linkedSignal} from '@angular/core';
|
||||
import {ExternalLink} from '../../directives';
|
||||
import {LOCAL_STORAGE} from '../../providers';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
@@ -18,6 +18,7 @@ export const STORAGE_KEY_PREFIX = 'docs-was-closed-top-banner-';
|
||||
imports: [ExternalLink, IconComponent],
|
||||
templateUrl: './top-level-banner.component.html',
|
||||
styleUrl: './top-level-banner.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TopLevelBannerComponent {
|
||||
private readonly localStorage = inject(LOCAL_STORAGE);
|
||||
|
||||
@@ -71,7 +71,6 @@ ts_project(
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/providers",
|
||||
"//adev/shared-docs/services",
|
||||
"//adev/shared-docs/utils",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -132,7 +132,6 @@
|
||||
.docs-page-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
|
||||
h1 {
|
||||
margin-block: 0;
|
||||
|
||||
@@ -20,7 +20,6 @@ import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-co
|
||||
import {CopyLinkButton} from '../../copy-link-anchor/copy-link-anchor.component';
|
||||
import {TableOfContents} from '../../table-of-contents/table-of-contents.component';
|
||||
import {Clipboard} from '@angular/cdk/clipboard';
|
||||
import {isFirefox} from '../../../utils';
|
||||
|
||||
describe('DocViewer', () => {
|
||||
let exampleContentSpy: jasmine.SpyObj<ExampleViewerContentLoader>;
|
||||
@@ -66,23 +65,6 @@ describe('DocViewer', () => {
|
||||
</div>
|
||||
`;
|
||||
|
||||
const exampleContentWithVideoFacade = `
|
||||
<div class="docs-video-container">
|
||||
<a
|
||||
class="docs-video-facade"
|
||||
href="https://www.youtube.com/watch?v=abc123&autoplay=1"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label="Play video: Test video"
|
||||
data-video-src="https://www.youtube.com/embed/abc123"
|
||||
data-video-title="Test video"
|
||||
>
|
||||
<img class="docs-video-thumbnail" src="https://i.ytimg.com/vi/abc123/maxresdefault.jpg" alt="" loading="lazy" />
|
||||
<span class="docs-video-play-button" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const exampleContentWithHeadings = `
|
||||
<h2>Heading h2</h2>
|
||||
<h3>Heading h3</h3>
|
||||
@@ -238,16 +220,6 @@ describe('DocViewer', () => {
|
||||
expect(copyButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not leak any icon text content (so it stays out of the search index)', () => {
|
||||
const fixture = TestBed.createComponent(CopyLinkButton);
|
||||
fixture.componentRef.setInput('href', '#test-section');
|
||||
fixture.componentRef.setInput('label', 'Test Section');
|
||||
fixture.componentRef.setInput('matTooltip', 'Copy link to Test Section');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('should copy link to clipboard when copy button is clicked', async () => {
|
||||
const clipboard = TestBed.inject(Clipboard);
|
||||
const clipboardSpy = spyOn(clipboard, 'copy').and.returnValue(true);
|
||||
@@ -265,25 +237,4 @@ describe('DocViewer', () => {
|
||||
// necessary to undo this location change by going back in the history.
|
||||
window.history.back();
|
||||
});
|
||||
|
||||
it('should upgrade a video facade to an iframe in browsers that can embed it', async () => {
|
||||
const fixture = TestBed.createComponent(DocViewer);
|
||||
fixture.componentRef.setInput('docContent', exampleContentWithVideoFacade);
|
||||
|
||||
await fixture.whenStable();
|
||||
|
||||
const iframe = fixture.nativeElement.querySelector('iframe.docs-video');
|
||||
const facade = fixture.nativeElement.querySelector('a.docs-video-facade');
|
||||
|
||||
if (isFirefox) {
|
||||
// Firefox can't load the cross-origin embed under COEP, so the facade stays a link.
|
||||
expect(facade).toBeTruthy();
|
||||
expect(iframe).toBeNull();
|
||||
} else {
|
||||
expect(iframe).toBeTruthy();
|
||||
expect(iframe.getAttribute('src')).toContain('youtube.com/embed/abc123');
|
||||
expect(iframe.hasAttribute('credentialless')).toBeTrue();
|
||||
expect(facade).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user