diff --git a/.github/actions/retry-command/action.yml b/.github/actions/retry-command/action.yml index 5b70e2a3d627..6bbf45f9797e 100644 --- a/.github/actions/retry-command/action.yml +++ b/.github/actions/retry-command/action.yml @@ -18,13 +18,17 @@ runs: steps: - name: Retry command shell: bash + env: + INPUT_MAX_ATTEMPTS: ${{ inputs.max_attempts }} + INPUT_DELAY: ${{ inputs.delay }} + INPUT_COMMAND: ${{ inputs.command }} run: | # Generic retry function: configurable attempts and delay retry_command() { - local max_attempts=${{ inputs.max_attempts }} - local delay=${{ inputs.delay }} + local max_attempts=${INPUT_MAX_ATTEMPTS} + local delay=${INPUT_DELAY} local attempt=1 - local command="${{ inputs.command }}" + local command="${INPUT_COMMAND}" while [ $attempt -le $max_attempts ]; do echo "Attempt $attempt/$max_attempts: Running command..." diff --git a/.github/actions/setup-elasticsearch/action.yml b/.github/actions/setup-elasticsearch/action.yml index 60d3fb07216e..813e46bc11d8 100644 --- a/.github/actions/setup-elasticsearch/action.yml +++ b/.github/actions/setup-elasticsearch/action.yml @@ -34,14 +34,18 @@ runs: - name: Pull Docker image shell: bash if: steps.cache-docker-layers.outputs.cache-hit != 'true' - run: docker pull elasticsearch:${{ inputs.elasticsearch_version }} + env: + ES_VERSION: ${{ inputs.elasticsearch_version }} + run: docker pull elasticsearch:${ES_VERSION} - name: Save Docker image to cache shell: bash if: steps.cache-docker-layers.outputs.cache-hit != 'true' + env: + ES_VERSION: ${{ inputs.elasticsearch_version }} run: | mkdir -p /tmp/docker-cache - docker save -o /tmp/docker-cache/elasticsearch.tar elasticsearch:${{ inputs.elasticsearch_version }} + docker save -o /tmp/docker-cache/elasticsearch.tar elasticsearch:${ES_VERSION} # Setups the Elasticsearch container # Derived from https://github.com/getong/elasticsearch-action diff --git a/.github/workflows/auto-close-dependencies.yml b/.github/workflows/auto-close-dependencies.yml index 8ed6848c3645..280ef8f07f79 100644 --- a/.github/workflows/auto-close-dependencies.yml +++ b/.github/workflows/auto-close-dependencies.yml @@ -19,7 +19,7 @@ on: - submitted permissions: - contents: read + contents: write pull-requests: write jobs: @@ -34,12 +34,12 @@ jobs: }} runs-on: ubuntu-latest steps: - - name: Close pull request + - name: Close pull request and delete branch env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_URL: ${{ github.event.pull_request.html_url }} run: | - gh pr close "$PR_URL" + gh pr close "$PR_URL" --delete-branch - name: Comment on the pull request env: diff --git a/.github/workflows/close-on-invalid-label.yaml b/.github/workflows/close-on-invalid-label.yaml index cf68f22a396b..ec54378401d8 100644 --- a/.github/workflows/close-on-invalid-label.yaml +++ b/.github/workflows/close-on-invalid-label.yaml @@ -27,13 +27,15 @@ jobs: if: ${{ github.event_name == 'issues' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh issue close ${{ github.event.issue.html_url }} + ISSUE_URL: ${{ github.event.issue.html_url }} + run: gh issue close "$ISSUE_URL" - name: Close PR if: ${{ github.event_name == 'pull_request_target' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh pr close ${{ github.event.pull_request.html_url }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: gh pr close "$PR_URL" - name: Check out repo if: ${{ failure() && github.event_name != 'pull_request_target' }} diff --git a/.github/workflows/content-pipelines.yml b/.github/workflows/content-pipelines.yml index ef723035be15..c0b82400916b 100644 --- a/.github/workflows/content-pipelines.yml +++ b/.github/workflows/content-pipelines.yml @@ -22,7 +22,6 @@ on: permissions: contents: write pull-requests: write - copilot-requests: write env: HUSKY: 0 @@ -71,11 +70,11 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" if git ls-remote --exit-code --heads origin "$UPDATE_BRANCH" > /dev/null 2>&1; then - git fetch origin "$UPDATE_BRANCH" + git fetch origin "$UPDATE_BRANCH" main git checkout "$UPDATE_BRANCH" git merge origin/main --no-edit || { echo "Merge conflict with main — resetting branch to main" - git merge --abort + git merge --abort 2>/dev/null || true git checkout main git branch -D "$UPDATE_BRANCH" git push origin --delete "$UPDATE_BRANCH" || true @@ -89,6 +88,7 @@ jobs: env: GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COPILOT_GITHUB_TOKEN: ${{ secrets.DOCS_BOT_PAT_COPILOT }} run: npx tsx src/content-pipelines/scripts/update.ts --id "${{ matrix.id }}" - name: Commit changes @@ -131,6 +131,7 @@ jobs: PR_BODY+="Runs the \`content-pipeline-update\` agent (${PIPELINE_ID}) against the latest source docs and updates official articles under \`content/\` that have fallen out of sync."$'\n\n' PR_BODY+="## Review"$'\n\n' PR_BODY+="* Review each commit for accuracy — the agent uses AI, so spot-check important changes"$'\n' + PR_BODY+="* To adjust agent behavior, see [Modifying results](${{ github.server_url }}/${{ github.repository }}/blob/main/src/content-pipelines/README.md#modifying-results)"$'\n' PR_BODY+="* Once satisfied, merge to keep docs up to date"$'\n' PR_BODY+="* A new PR will be created on the next run if there are further changes" diff --git a/.github/workflows/generate-code-scanning-query-lists.yml b/.github/workflows/generate-code-scanning-query-lists.yml index b3309211e0ae..2b965f65dc25 100644 --- a/.github/workflows/generate-code-scanning-query-lists.yml +++ b/.github/workflows/generate-code-scanning-query-lists.yml @@ -24,8 +24,7 @@ on: - .github/actions/install-cocofix/action.yml permissions: - contents: write - pull-requests: write + contents: read jobs: generate-security-query-lists: @@ -159,6 +158,9 @@ jobs: create-pull-request: if: github.repository == 'github/docs-internal' runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write needs: [generate-security-query-lists, generate-quality-query-lists] steps: - name: Checkout repository code diff --git a/.github/workflows/link-check-internal.yml b/.github/workflows/link-check-internal.yml index 369681f638ea..89bc0d207f67 100644 --- a/.github/workflows/link-check-internal.yml +++ b/.github/workflows/link-check-internal.yml @@ -17,7 +17,6 @@ on: permissions: contents: read - issues: write jobs: # Determine which version/language combos to run @@ -35,14 +34,18 @@ jobs: - name: Set matrix id: set-matrix run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then # Manual run: use the provided version and language - echo 'matrix={"include":[{"version":"${{ inputs.version }}","language":"${{ inputs.language }}"}]}' >> $GITHUB_OUTPUT + echo "matrix={\"include\":[{\"version\":\"${INPUT_VERSION}\",\"language\":\"${INPUT_LANGUAGE}\"}]}" >> $GITHUB_OUTPUT else # Scheduled run: English free-pro-team + English latest enterprise-server LATEST_GHES=$(npx tsx -e "import { latest } from './src/versions/lib/enterprise-server-releases'; console.log(latest)") echo "matrix={\"include\":[{\"version\":\"free-pro-team@latest\",\"language\":\"en\"},{\"version\":\"enterprise-server@${LATEST_GHES}\",\"language\":\"en\"}]}" >> $GITHUB_OUTPUT fi + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ inputs.version }} + INPUT_LANGUAGE: ${{ inputs.language }} - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} @@ -104,6 +107,8 @@ jobs: if: always() && github.repository == 'github/docs-internal' needs: [setup-matrix, check-internal-links] runs-on: ubuntu-latest + permissions: + issues: write steps: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 diff --git a/.github/workflows/needs-sme-workflow.yml b/.github/workflows/needs-sme-workflow.yml index 82a9d32c1ecb..284ece93107a 100644 --- a/.github/workflows/needs-sme-workflow.yml +++ b/.github/workflows/needs-sme-workflow.yml @@ -13,13 +13,13 @@ on: permissions: contents: read - issues: write - pull-requests: write jobs: add-issue-comment: if: ${{ github.repository == 'github/docs' && (github.event.label.name == 'needs SME' && github.event_name == 'issues') }} runs-on: ubuntu-latest + permissions: + issues: write steps: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -39,6 +39,8 @@ jobs: add-pr-comment: if: ${{ github.repository == 'github/docs' && (github.event.label.name == 'needs SME' && github.event_name == 'pull_request_target') }} runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 diff --git a/.github/workflows/ready-for-doc-review.yml b/.github/workflows/ready-for-doc-review.yml index 152490b8b801..534402a63740 100644 --- a/.github/workflows/ready-for-doc-review.yml +++ b/.github/workflows/ready-for-doc-review.yml @@ -37,11 +37,15 @@ jobs: - name: Set AUTHOR_LOGIN run: | - if [[ "${{ github.event.pull_request.assignee.login && github.event.pull_request.user.login == 'docs-bot' }}" ]]; then - echo "AUTHOR_LOGIN=${{ github.event.pull_request.assignee.login }}" >> $GITHUB_ENV + if [[ "${IS_DOCS_BOT_ASSIGNEE}" == "true" ]]; then + echo "AUTHOR_LOGIN=${ASSIGNEE_LOGIN}" >> $GITHUB_ENV else - echo "AUTHOR_LOGIN=${{ github.event.pull_request.user.login }}" >> $GITHUB_ENV + echo "AUTHOR_LOGIN=${USER_LOGIN}" >> $GITHUB_ENV fi + env: + IS_DOCS_BOT_ASSIGNEE: ${{ github.event.pull_request.assignee.login && github.event.pull_request.user.login == 'docs-bot' }} + ASSIGNEE_LOGIN: ${{ github.event.pull_request.assignee.login }} + USER_LOGIN: ${{ github.event.pull_request.user.login }} - name: Run script run: | diff --git a/.github/workflows/sync-graphql.yml b/.github/workflows/sync-graphql.yml index b2992859f7d6..57b8f6e54998 100644 --- a/.github/workflows/sync-graphql.yml +++ b/.github/workflows/sync-graphql.yml @@ -10,13 +10,15 @@ on: - cron: '20 16 * * 1-5' # Run Mon-Fri at 16:20 UTC / 8:20 PST permissions: - contents: write - pull-requests: write + contents: read jobs: update_graphql_files: if: github.repository == 'github/docs-internal' runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write outputs: ignored-changes: ${{ steps.sync.outputs.ignored-changes }} ignored-count: ${{ steps.sync.outputs.ignored-count }} diff --git a/.github/workflows/triage-stale-check.yml b/.github/workflows/triage-stale-check.yml index 58f71fe8e68b..e154f9132bbe 100644 --- a/.github/workflows/triage-stale-check.yml +++ b/.github/workflows/triage-stale-check.yml @@ -10,14 +10,15 @@ on: permissions: contents: read - issues: write - pull-requests: write jobs: stale_contributor: name: Identify and close stale issues and PRs if: github.repository == 'github/docs' runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write steps: - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 @@ -55,6 +56,9 @@ jobs: name: Remind staff about PRs waiting for review if: github.repository == 'github/docs' runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write steps: - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000000..63a7e6ffe6f8 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,33 @@ +name: Workflow security lint + +# **What it does**: Runs zizmor to detect security issues in GitHub Actions workflows. +# **Why we have it**: To catch injection vulnerabilities and other security misconfigurations before they ship. +# **Who does it impact**: Docs engineering. + +on: + pull_request: + paths: + - '.github/workflows/**' + - '.github/actions/**' + - '.github/zizmor.yml' + +permissions: + contents: read + +jobs: + zizmor: + if: github.repository == 'github/docs-internal' + name: zizmor + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 + with: + online-audits: 'false' + advanced-security: 'false' + annotations: 'true' + min-severity: 'high' diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000000..95639824c31c --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,22 @@ +rules: + # pull_request_target is required for workflows that need write access + # on PRs from forks (e.g. labeling, commenting). We audit these manually. + dangerous-triggers: + disable: true + + # moda-ci uses reusable workflows (uses:) which don't support job-level + # permissions. id-token:write and attestations:write are needed by docker-image + # for attestation but can't be scoped to that job alone. + excessive-permissions: + ignore: + - moda-ci.yaml + + # actions/* has immutable tags, so ref-pinning is sufficient. + # github/internal-actions is a private GitHub org repo, ref-pin is fine. + # Everything else must be hash-pinned. + unpinned-uses: + config: + policies: + 'actions/*': ref-pin + 'github/internal-actions/*': ref-pin + '*': hash-pin diff --git a/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/adding-a-collaborator-to-a-repository-security-advisory.md b/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/adding-a-collaborator-to-a-repository-security-advisory.md index b6c55c1a6f22..13c2df1e81c5 100644 --- a/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/adding-a-collaborator-to-a-repository-security-advisory.md +++ b/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/adding-a-collaborator-to-a-repository-security-advisory.md @@ -1,6 +1,6 @@ --- title: Adding a collaborator to a repository security advisory -intro: You can add other users or teams to collaborate on a security advisory with you. +intro: Add other users or teams to collaborate on a security advisory with you. permissions: '{% data reusables.permissions.security-repo-enable %}' redirect_from: - /articles/adding-a-collaborator-to-a-maintainer-security-advisory @@ -21,9 +21,7 @@ topics: shortTitle: Add collaborators --- -{% data reusables.security-advisory.repository-level-advisory-note %} - -## Adding a collaborator to a security advisory +This article applies to repository-level security advisories in a public repository. To edit a global advisory in the {% data variables.product.prodname_advisory_database %}, see [AUTOTITLE](/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/editing-security-advisories-in-the-github-advisory-database). Collaborators have write permissions to the security advisory. For more information, see [AUTOTITLE](/code-security/security-advisories/working-with-repository-security-advisories/permission-levels-for-repository-security-advisories). @@ -40,6 +38,4 @@ Collaborators have write permissions to the security advisory. For more informat ## Further reading -* [AUTOTITLE](/code-security/security-advisories/working-with-repository-security-advisories/permission-levels-for-repository-security-advisories) * [AUTOTITLE](/code-security/security-advisories/working-with-repository-security-advisories/collaborating-in-a-temporary-private-fork-to-resolve-a-repository-security-vulnerability) -* [AUTOTITLE](/code-security/security-advisories/working-with-repository-security-advisories/removing-a-collaborator-from-a-repository-security-advisory). diff --git a/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/managing-privately-reported-security-vulnerabilities.md b/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/managing-privately-reported-security-vulnerabilities.md index 031338f2123c..3b828572304c 100644 --- a/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/managing-privately-reported-security-vulnerabilities.md +++ b/content/code-security/how-tos/report-and-fix-vulnerabilities/fix-reported-vulnerabilities/managing-privately-reported-security-vulnerabilities.md @@ -15,16 +15,8 @@ redirect_from: - /code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/managing-privately-reported-security-vulnerabilities --- -{% data reusables.security-advisory.private-vulnerability-reporting-enable %} - -## About privately reporting a security vulnerability - -Private vulnerability reporting makes it easy for security researchers to report vulnerabilities directly to you using a simple form. - When a security researcher reports a vulnerability privately, you are notified and can choose to either accept it, ask more questions, or reject it. If you accept the report, you're ready to collaborate on a fix for the vulnerability in private with the security researcher. -## Managing security vulnerabilities that are privately reported - {% data reusables.security-advisory.private-vulnerability-reporting-configure-notifications %} For more information about configuring notification preferences, see [AUTOTITLE](/code-security/security-advisories/working-with-repository-security-advisories/configuring-private-vulnerability-reporting-for-a-repository#configuring-notifications-for-private-vulnerability-reporting). diff --git a/content/code-security/tutorials/manage-security-alerts/best-practices-for-participating-in-a-security-campaign.md b/content/code-security/tutorials/manage-security-alerts/best-practices-for-participating-in-a-security-campaign.md index 548fb5cfd15d..eaa314dc4d55 100644 --- a/content/code-security/tutorials/manage-security-alerts/best-practices-for-participating-in-a-security-campaign.md +++ b/content/code-security/tutorials/manage-security-alerts/best-practices-for-participating-in-a-security-campaign.md @@ -1,7 +1,7 @@ --- -title: Best practices for participating in a code security campaign -shortTitle: Best practices for campaigns -intro: Learn how you can successfully take part in a security campaign for {% data variables.product.prodname_code_scanning %} alerts and how it can benefit your career as well as your code. +title: Participating in a code security campaign +shortTitle: Participate in campaigns +intro: If you’ve been assigned alerts as part of a security campaign, this guide explains what campaigns are, what to expect, and how to resolve alerts effectively. allowTitleToDifferFromFilename: true permissions: '{% data reusables.permissions.code-scanning-all-alerts %}' product: '{% data reusables.gated-features.security-campaigns %}' @@ -17,15 +17,15 @@ redirect_from: - /code-security/code-scanning/managing-code-scanning-alerts/best-practices-for-participating-in-a-security-campaign --- -## What is a code security campaign +## What is a code security campaign? -A security campaign is a group of {% data variables.product.prodname_code_scanning %} alerts, detected in the default branches of repositories, chosen by an organization owner or security manager for remediation. +A code security campaign is a focused effort to remediate a defined group of {% data variables.product.prodname_code_scanning %} alerts across one or more repositories. -You can take part in a security campaign by fixing one or more of the alerts included in the campaign. +Campaigns are created by organization owners or security managers and typically target alerts detected in the default branches of repositories. If you’re participating in a campaign, you’ve been asked to help resolve some of these alerts. -## What are the benefits of participating in a campaign +## What are the benefits of participating in a campaign? -In addition to the benefit of removing an important security problem from your organization's codebase, alerts in a security campaign have several other benefits compared with fixing another alert in your repository. +In addition to reducing risk in your organization’s codebase, alerts in a security campaign have several other benefits compared with fixing another alert in your repository. * You have a campaign manager on the security team to collaborate with and a specific contact link for discussing campaign activities. * You know that you are fixing a security alert that is important to the company. @@ -34,9 +34,11 @@ In addition to the benefit of removing an important security problem from your o * If you have access to {% data variables.copilot.copilot_chat %}, you can ask questions about the alert and the suggested fix.{% endif %} * You are improving and demonstrating your knowledge of secure coding. -Adopting a few key best practices can help you participate successfully in a campaign. +Participating in a campaign helps reduce risk in your organization’s codebase while strengthening your secure coding skills. -## Stay informed +## 1. Learn about campaigns + +Start by reviewing campaign updates and deadlines so you can plan your work effectively. ### Notification settings @@ -50,15 +52,15 @@ When you open the **Security** tab for a repository with one or more campaign al ### Campaign-generated {% data variables.product.prodname_github_issues %} -Some campaigns automatically create {% data variables.product.prodname_github_issues %} for each repository which details the campaign managers, contact URL, and due date. +Some campaigns automatically create {% data variables.product.prodname_github_issues %} for each repository that detail the campaign managers, contact URL, and due date. -You can use this issue to plan and track campaign work as part of your usual workflows, such as: +Use this issue to coordinate work, track progress, and keep stakeholders aligned. For example, you might use the issue to: -* Adding the issue to project boards -* Adding assignees -* Creating sub-issues or tasklists +* Add the issue to project boards +* Add assignees +* Create sub-issues or tasklists -## Seek context +## 2. Build context before applying fixes Your security team may provide you with specific training ahead of participating in a campaign, so that you feel equipped to address the alerts included in the campaign. @@ -73,13 +75,25 @@ In addition, there are external resources for understanding common security issu * The **OWASP Foundation** provides many resources for learning about the most common vulnerabilities, see [About the OWASP Foundation](https://owasp.org/about/). * The **MITRE Corporation** maintains a detailed list of common weaknesses, see [About CWE](https://cwe.mitre.org/about/index.html). -## Group similar alerts +## 3. Collaborate early and often + +A security campaign will generally include a contact URL, which might link you to the campaign manager, an open forum (such as a {% data variables.product.github %} Discussion), or a website of resources. You should use this space to ask questions about the campaign or specific alerts, find useful resources, and share knowledge. + +To find the contact URL: + +1. Open the **Security** tab for your repository. +1. On the left sidebar, click the name of the campaign you are participating in. +1. On the campaign tracking page, to the right of the campaign manager's name, click **{% octicon "comment" aria-hidden="true" aria-label="comment" %}**. + +## 4. Group alerts strategically -When fixing security alerts as part of a campaign, it may be helpful to group and fix similar alerts together. By doing so, you can develop a deeper understanding of the underlying issue. As you gain confidence and efficiency in resolving a specific type of alert, it makes it easier and faster for you to resolve subsequent alerts. +Tackle similar alerts together to build momentum, reduce context switching, and develop a deeper understanding of the underlying issue. As you gain confidence and efficiency in resolving a specific type of alert, it makes it easier and faster for you to resolve subsequent alerts. {% ifversion copilot %} -## Leverage {% data variables.product.prodname_copilot_short %} +## 5. Resolve alerts with the help of {% data variables.product.prodname_copilot_short %} + +You can leverage {% data variables.product.prodname_copilot_short %} to help resolve alerts in a security campaign. Depending on the features enabled in your repository, you may have access to {% data variables.copilot.copilot_autofix_short %} suggestions and {% data variables.copilot.copilot_chat_short %}. {% ifversion code-scanning-autofix %} @@ -111,20 +125,10 @@ For example: ``` -If you don't already have access to {% data variables.copilot.copilot_chat_short %} through your organization{% ifversion ghec %} or enterprise{% endif %}, you can sign up to {% data variables.copilot.copilot_free %}. For more information, see [AUTOTITLE](/copilot/managing-copilot/managing-copilot-as-an-individual-subscriber/managing-copilot-free/accessing-github-copilot-free). +If you don't already have access to {% data variables.copilot.copilot_chat_short %} through your organization{% ifversion ghec %} or enterprise{% endif %}, you can sign up to {% data variables.copilot.copilot_free %}. See [AUTOTITLE](/copilot/managing-copilot/managing-copilot-as-an-individual-subscriber/managing-copilot-free/accessing-github-copilot-free). {% endif %} -## Ask questions - -A security campaign will generally include a contact URL, which might link you to the campaign manager, an open forum (such as a {% data variables.product.github %} Discussion), or a website of resources. You should use this space to ask questions about the campaign or specific alerts, find useful resources, and share knowledge. - -To find the contact URL: - -1. Open the **Security** tab for your repository. -1. On the left sidebar, click the name of the campaign you are participating in. -1. On the campaign tracking page, to the right of the campaign manager's name, click **{% octicon "comment" aria-hidden="true" aria-label="comment" %}**. - ## Next steps * [AUTOTITLE](/code-security/code-scanning/managing-code-scanning-alerts/fixing-alerts-in-security-campaign) diff --git a/content/copilot/concepts/agents/coding-agent/about-coding-agent.md b/content/copilot/concepts/agents/coding-agent/about-coding-agent.md index 931470f67c7d..e5b62e140c6c 100644 --- a/content/copilot/concepts/agents/coding-agent/about-coding-agent.md +++ b/content/copilot/concepts/agents/coding-agent/about-coding-agent.md @@ -69,9 +69,9 @@ You can create specialized {% data variables.copilot.custom_agents_short %} for ## Measuring pull request outcomes for {% data variables.copilot.copilot_coding_agent %} -Enterprise administrators can use {% data variables.product.prodname_copilot_short %} usage metrics to analyze pull request outcomes for pull requests created by {% data variables.copilot.copilot_coding_agent %}. +Enterprise administrators and organization owners can use {% data variables.product.prodname_copilot_short %} usage metrics to analyze pull request outcomes for pull requests created by {% data variables.copilot.copilot_coding_agent %}. -The enterprise-level {% data variables.product.prodname_copilot_short %} usage metrics API includes pull request lifecycle metrics such as: +The {% data variables.product.prodname_copilot_short %} usage metrics APIs include pull request lifecycle metrics such as: * The total number of pull requests created and merged * The number of pull requests created by {% data variables.copilot.copilot_coding_agent %} that have been merged diff --git a/content/copilot/concepts/copilot-usage-metrics/copilot-metrics.md b/content/copilot/concepts/copilot-usage-metrics/copilot-metrics.md index b624da9e6f47..0119438efb88 100644 --- a/content/copilot/concepts/copilot-usage-metrics/copilot-metrics.md +++ b/content/copilot/concepts/copilot-usage-metrics/copilot-metrics.md @@ -115,7 +115,15 @@ For example, all usage data for a Monday (which closes at midnight UTC) will be **Lines of Code (LoC) metrics** measure the number of lines {% data variables.product.prodname_copilot_short %} suggested, added, or deleted in the editor, providing a directional view of {% data variables.product.prodname_copilot_short %}’s tangible output. For example, "Lines added" shows how much code was actually accepted and inserted into the editor. -**Pull request lifecycle metrics** measure how {% data variables.product.prodname_copilot_short %} activity relates to pull request outcomes and delivery flow. These metrics include pull request creation and merge counts, median time to merge, and review suggestion activity. By comparing overall pull request activity with pull requests created by {% data variables.product.prodname_copilot_short %}, you can evaluate how AI-assisted workflows influence throughput and cycle time across your enterprise. +**Pull request lifecycle metrics** measure how {% data variables.product.prodname_copilot_short %} activity relates to pull request outcomes and delivery flow. These metrics include pull request creation and merge counts, median time to merge, and review suggestion activity. By comparing overall pull request activity with pull requests created by {% data variables.product.prodname_copilot_short %}, you can evaluate how AI-assisted workflows influence throughput and cycle time at the organization or enterprise level. + +### Interpreting pull request lifecycle metrics across scopes + +Pull request lifecycle metrics are available at both the organization and enterprise level. When comparing reports, keep the following in mind: + +* **Deduplication**: Enterprise-level reports deduplicate users across organizations. Organization-level reports do not. +* **Pull request-only data**: Pull request lifecycle metrics may appear even if IDE usage metrics are absent, since pull request data is derived from repository activity. +* **Attribution timing**: If a repository or organization is transferred between owners, pull request creation, review, and merge events may be attributed to different entities depending on when each event occurred. ## How can I use these metrics? diff --git a/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md b/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md index 8f8c0087f1e6..804842c11ab2 100644 --- a/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md +++ b/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md @@ -97,18 +97,21 @@ These fields appear in the exported NDJSON reports and in the {% data variables. ### Pull request activity fields (API only) -These fields capture daily pull request creation, review, merge, and suggestion activity across the enterprise, including activity performed by {% data variables.product.prodname_copilot_short %}. +> [!IMPORTANT] +> Organization- and enterprise-level reports may show different totals due to differences in user deduplication and attribution timing. For guidance on interpreting pull request metrics across scopes, see [AUTOTITLE](/copilot/concepts/copilot-usage-metrics/copilot-metrics#interpreting-pull-request-lifecycle-metrics-across-scopes). + +These fields capture daily pull request creation, review, merge, and suggestion activity at the enterprise or organization scope, including activity performed by {% data variables.product.prodname_copilot_short %}. | Field | Description | |:--|:--| -| `pull_requests.total_created` | Total number of pull requests created across the enterprise on this specific day.

Creation is a one-time event. Each pull request is counted only on the day it is created. | -| `pull_requests.total_reviewed` | Total number of pull requests reviewed across the enterprise on this specific day.

The same pull request may be counted on multiple days if it receives reviews on multiple days. Within a single day, each pull request is counted once, even if multiple review actions occur. | -| `pull_requests.total_merged` | Total number of pull requests merged across the enterprise on this specific day.

Merging is a one-time event. Each pull request is counted only on the day it is merged. | +| `pull_requests.total_created` | Total number of pull requests created on this specific day.

Creation is a one-time event. Each pull request is counted only on the day it is created. | +| `pull_requests.total_reviewed` | Total number of pull requests reviewed on this specific day.

The same pull request may be counted on multiple days if it receives reviews on multiple days. Within a single day, each pull request is counted once, even if multiple review actions occur. | +| `pull_requests.total_merged` | Total number of pull requests merged on this specific day.

Merging is a one-time event. Each pull request is counted only on the day it is merged. | | `pull_requests.median_minutes_to_merge` | Median time, in minutes, between pull request creation and merge for pull requests merged on this specific day.

Median is used to reduce the impact of outliers from unusually long-running pull requests. | | `pull_requests.total_suggestions` | Total number of pull request review suggestions generated on this specific day, regardless of author. | | `pull_requests.total_applied_suggestions` | Total number of pull request review suggestions that were applied on this specific day, regardless of author. | -| `pull_requests.total_created_by_copilot` | Number of pull requests created by {% data variables.product.prodname_copilot_short %} across the enterprise on this specific day. | -| `pull_requests.total_reviewed_by_copilot` | Number of pull requests reviewed by {% data variables.product.prodname_copilot_short %} across the enterprise on this specific day.

A pull request may be counted on multiple days if {% data variables.product.prodname_copilot_short %} reviews it on multiple days. | +| `pull_requests.total_created_by_copilot` | Number of pull requests created by {% data variables.product.prodname_copilot_short %} on this specific day. | +| `pull_requests.total_reviewed_by_copilot` | Number of pull requests reviewed by {% data variables.product.prodname_copilot_short %} on this specific day.

A pull request may be counted on multiple days if {% data variables.product.prodname_copilot_short %} reviews it on multiple days. | | `pull_requests.total_merged_created_by_copilot` | Number of pull requests created by {% data variables.product.prodname_copilot_short %} that were merged on this specific day. Each pull request is counted only on the day it is merged. | | `pull_requests.median_minutes_to_merge_copilot_authored` | Median time, in minutes, between pull request creation and merge for pull requests created by {% data variables.product.prodname_copilot_short %} and merged on this specific day. | | `pull_requests.total_copilot_suggestions` | Number of pull request review suggestions generated by {% data variables.product.prodname_copilot_short %} on this specific day. | diff --git a/data/reusables/security-advisory/private-vulnerability-reporting-configure-notifications.md b/data/reusables/security-advisory/private-vulnerability-reporting-configure-notifications.md index 76c114aed806..581606345512 100644 --- a/data/reusables/security-advisory/private-vulnerability-reporting-configure-notifications.md +++ b/data/reusables/security-advisory/private-vulnerability-reporting-configure-notifications.md @@ -1,4 +1,4 @@ -When a new vulnerability is privately reported on a repository where private vulnerability reporting is enabled, {% data variables.product.github %} notifies repository maintainers and security managers if: +When a new vulnerability is privately reported in a repository, {% data variables.product.github %} notifies repository maintainers and security managers if: * They're watching the repository for all activity. * They have notifications enabled for the repository. diff --git a/data/reusables/security-advisory/repository-level-advisory-note.md b/data/reusables/security-advisory/repository-level-advisory-note.md index fb7f927ad71a..b1080399a812 100644 --- a/data/reusables/security-advisory/repository-level-advisory-note.md +++ b/data/reusables/security-advisory/repository-level-advisory-note.md @@ -1,4 +1,3 @@ > [!NOTE] -> This article applies to editing repository-level advisories as an owner of a public repository. -> -> Users who are not repository owners can contribute to global security advisories in the {% data variables.product.prodname_advisory_database %} at [github.com/advisories](https://github.com/advisories). Edits to global advisories will not change or affect how the advisory appears on the repository. For more information, see [AUTOTITLE](/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/editing-security-advisories-in-the-github-advisory-database). +> This article applies to repository-level security advisories in a public repository. +> To edit a global advisory in the {% data variables.product.prodname_advisory_database %}, see [AUTOTITLE](/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/editing-security-advisories-in-the-github-advisory-database). diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml index 13d535e736b8..a5cdba4266b6 100644 --- a/data/tables/copilot/model-supported-clients.yml +++ b/data/tables/copilot/model-supported-clients.yml @@ -168,7 +168,7 @@ - name: GPT-5.3-Codex dotcom: true vscode: true - vs: false + vs: true eclipse: false xcode: false jetbrains: false diff --git a/eslint.config.ts b/eslint.config.ts index 8d367feaa749..bc4f7d6fd7a2 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -193,7 +193,6 @@ export default [ 'src/article-api/scripts/generate-api-docs.ts', 'src/article-api/transformers/audit-logs-transformer.ts', 'src/article-api/transformers/rest-transformer.ts', - 'src/assets/scripts/validate-asset-images.ts', 'src/automated-pipelines/tests/rendering.ts', 'src/codeql-cli/scripts/convert-markdown-for-docs.ts', 'src/content-linter/lib/helpers/get-lintable-yml.ts', @@ -226,27 +225,18 @@ export default [ 'src/content-render/scripts/move-content.ts', 'src/content-render/tests/data.ts', 'src/content-render/tests/link-error-line-numbers.ts', - 'src/content-render/unified/alerts.ts', 'src/content-render/unified/annotate.ts', - 'src/content-render/unified/code-header.ts', - 'src/content-render/unified/copilot-prompt.ts', 'src/content-render/unified/index.ts', 'src/content-render/unified/module-types.d.ts', - 'src/content-render/unified/rewrite-empty-table-rows.ts', - 'src/content-render/unified/rewrite-for-rowheaders.ts', 'src/content-render/unified/rewrite-local-links.ts', 'src/data-directory/lib/data-directory.ts', - 'src/data-directory/lib/data-schemas/features.ts', 'src/data-directory/lib/data-schemas/learning-tracks.ts', 'src/data-directory/lib/get-data.ts', 'src/data-directory/scripts/find-orphaned-features/find.ts', - 'src/dev-toc/generate.ts', 'src/early-access/scripts/migrate-early-access-product.ts', 'src/early-access/scripts/what-docs-early-access-branch.ts', 'src/fixtures/tests/categories-and-subcategory.ts', - 'src/fixtures/tests/glossary.ts', 'src/fixtures/tests/guides.ts', - 'src/fixtures/tests/homepage.ts', 'src/fixtures/tests/liquid.ts', 'src/fixtures/tests/markdown.ts', 'src/fixtures/tests/translations.ts', @@ -267,10 +257,7 @@ export default [ 'src/ghes-releases/scripts/deprecate/update-content.ts', 'src/github-apps/lib/index.ts', 'src/graphql/lib/index.ts', - 'src/graphql/pages/breaking-changes.tsx', - 'src/graphql/pages/changelog.tsx', 'src/graphql/pages/reference.tsx', - 'src/graphql/pages/schema-previews.tsx', 'src/graphql/scripts/build-changelog.ts', 'src/graphql/scripts/utils/process-previews.ts', 'src/graphql/scripts/utils/process-schemas.ts', @@ -285,7 +272,6 @@ export default [ 'src/landings/pages/home.tsx', 'src/landings/pages/product.tsx', 'src/languages/lib/correct-translation-content.ts', - 'src/languages/lib/get-alert-titles.ts', 'src/languages/lib/render-with-fallback.ts', 'src/languages/lib/translation-utils.ts', 'src/learning-track/lib/process-learning-tracks.ts', @@ -324,12 +310,10 @@ export default [ 'src/search/scripts/index/index-cli.ts', 'src/search/scripts/index/utils/indexing-elasticsearch-utils.ts', 'src/search/scripts/scrape/lib/parse-page-sections-into-records.ts', - 'src/search/tests/topics.ts', 'src/tests/helpers/check-url.ts', 'src/tests/helpers/e2etest.ts', 'src/tests/scripts/copy-fixture-data.ts', 'src/tests/vitest.setup.ts', - 'src/tools/components/Picker.tsx', 'src/types/github__markdownlint-github.d.ts', 'src/types/markdownlint-lib-rules.d.ts', 'src/types/markdownlint-rule-helpers.d.ts', diff --git a/package-lock.json b/package-lock.json index 410a504d6878..dc08aa8b1df8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,8 +29,7 @@ "azure-kusto-data": "^7.0.0", "bottleneck": "2.19.5", "boxen": "8.0.1", - "cheerio": "^1.0.0-rc.12", - "cheerio-to-text": "0.2.4", + "cheerio": "^1.2.0", "classnames": "^2.5.1", "clsx": "^2.1.1", "connect-timeout": "1.9.1", @@ -117,7 +116,6 @@ "@octokit/rest": "22.0.0", "@playwright/test": "^1.56", "@types/accept-language-parser": "1.5.7", - "@types/cheerio": "^0.22.35", "@types/connect-timeout": "1.9.0", "@types/cookie": "0.6.0", "@types/cookie-parser": "1.4.8", @@ -310,19 +308,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@axe-core/playwright": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.1.tgz", @@ -484,43 +469,45 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", - "peer": true, + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -535,30 +522,26 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "peer": true - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "peer": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", - "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.4", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" @@ -576,14 +559,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "peer": true, + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -595,7 +578,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "peer": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -604,70 +587,42 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "peer": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "peer": true - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "peer": true, + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -684,73 +639,53 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "peer": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", - "peer": true, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", - "peer": true, + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -785,54 +720,45 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", - "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", - "dependencies": { - "@babel/code-frame": "^7.23.4", - "@babel/generator": "^7.23.4", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.4", - "@babel/types": "^7.23.4", - "debug": "^4.1.0", - "globals": "^11.1.0" + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1401,9 +1327,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { @@ -1509,9 +1435,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { @@ -2310,30 +2236,30 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "engines": { - "node": ">=6.0.0" + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -2345,9 +2271,10 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2579,6 +2506,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -2879,6 +2807,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2911,6 +2840,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2950,6 +2880,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2970,6 +2901,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2990,6 +2922,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3010,6 +2943,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3030,6 +2964,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3050,6 +2985,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3070,6 +3006,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3090,6 +3027,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3110,6 +3048,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3130,6 +3069,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3150,6 +3090,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3170,6 +3111,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3190,6 +3132,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3207,6 +3150,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, "license": "Apache-2.0", "optional": true, "bin": { @@ -3235,6 +3179,7 @@ "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.56.1" }, @@ -3947,15 +3892,6 @@ "assertion-error": "^2.0.1" } }, - "node_modules/@types/cheerio": { - "version": "0.22.35", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.35.tgz", - "integrity": "sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/command-line-args": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", @@ -4058,6 +3994,7 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4219,6 +4156,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4230,6 +4168,7 @@ "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4400,6 +4339,7 @@ "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", @@ -5045,6 +4985,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5075,6 +5016,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5602,12 +5544,24 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -5640,6 +5594,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", @@ -5917,21 +5872,25 @@ } }, "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">=20.18.1" }, "funding": { "url": "https://github.com/cheeriojs/cheerio?sponsor=1" @@ -5952,15 +5911,37 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cheerio-to-text": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/cheerio-to-text/-/cheerio-to-text-0.2.4.tgz", - "integrity": "sha512-/Qpraiyk9FEHG0WJlXkRTkKA60hprTN1eaLk+bhd+Hhv7UtO1AR3kXXLztdVVXu1YdzgoFF7cAKN8ouDuJs6kA==", + "node_modules/cheerio/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", "engines": { - "node": ">=18" + "node": ">=0.12" }, - "peerDependencies": { - "cheerio": "1.0.0-rc.12" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" } }, "node_modules/chokidar": { @@ -6336,6 +6317,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -6897,6 +6884,31 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ensure-posix-path": { "version": "1.1.1", "license": "ISC" @@ -7188,6 +7200,7 @@ "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -7249,6 +7262,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7524,6 +7538,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7574,9 +7589,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { @@ -7637,10 +7652,11 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -8023,7 +8039,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { @@ -8666,6 +8684,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -8720,8 +8739,9 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -8848,27 +8868,6 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/glob/node_modules/minimatch": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", @@ -8943,6 +8942,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -9390,9 +9390,9 @@ } }, "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -9404,14 +9404,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -10317,6 +10317,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -10361,13 +10362,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-bignum": { @@ -10442,8 +10445,9 @@ }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", - "peer": true, "bin": { "json5": "lib/cli.js" }, @@ -11086,9 +11090,9 @@ } }, "node_modules/matcher-collection/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -12057,13 +12061,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", + "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -12655,6 +12659,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, "license": "MIT", "optional": true }, @@ -12712,7 +12717,9 @@ } }, "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { @@ -13148,10 +13155,24 @@ } }, "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.0.0", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "license": "MIT", "dependencies": { - "domhandler": "^5.0.2", "parse5": "^7.0.0" }, "funding": { @@ -13411,6 +13432,7 @@ "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -13474,6 +13496,7 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13643,6 +13666,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13663,6 +13687,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13687,9 +13712,10 @@ } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", "peer": true }, "node_modules/react-markdown": { @@ -14130,29 +14156,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/rimraf/node_modules/glob": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", @@ -14394,6 +14397,7 @@ "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -15287,6 +15291,7 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.4.5", @@ -15545,6 +15550,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15869,6 +15875,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16223,6 +16230,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.2.2" }, @@ -16425,6 +16433,7 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16533,6 +16542,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16726,27 +16736,6 @@ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "license": "MIT" }, - "node_modules/walk-sync/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/walk-sync/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/walk-sync/node_modules/minimatch": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", @@ -16790,6 +16779,41 @@ "node": ">=14.14" } }, + "node_modules/website-scraper/node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/website-scraper/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/website-scraper/node_modules/got": { "version": "12.6.1", "dev": true, @@ -16814,6 +16838,26 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/website-scraper/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/website-scraper/node_modules/normalize-url": { "version": "7.2.0", "dev": true, @@ -16825,6 +16869,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "dev": true, @@ -17066,6 +17144,12 @@ "node": ">=0.4" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, "node_modules/yaml": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", diff --git a/package.json b/package.json index be7db1ba3bca..37d6c0afed8b 100644 --- a/package.json +++ b/package.json @@ -179,8 +179,7 @@ "azure-kusto-data": "^7.0.0", "bottleneck": "2.19.5", "boxen": "8.0.1", - "cheerio": "^1.0.0-rc.12", - "cheerio-to-text": "0.2.4", + "cheerio": "^1.2.0", "classnames": "^2.5.1", "clsx": "^2.1.1", "connect-timeout": "1.9.1", @@ -267,7 +266,6 @@ "@octokit/rest": "22.0.0", "@playwright/test": "^1.56", "@types/accept-language-parser": "1.5.7", - "@types/cheerio": "^0.22.35", "@types/connect-timeout": "1.9.0", "@types/cookie": "0.6.0", "@types/cookie-parser": "1.4.8", diff --git a/src/assets/scripts/validate-asset-images.ts b/src/assets/scripts/validate-asset-images.ts index 9e91bd86c0cb..f799bee711c1 100755 --- a/src/assets/scripts/validate-asset-images.ts +++ b/src/assets/scripts/validate-asset-images.ts @@ -16,7 +16,7 @@ import path from 'path' import { program } from 'commander' import chalk from 'chalk' -import cheerio from 'cheerio' +import { load } from 'cheerio' import { fileTypeFromFile } from 'file-type' import walk from 'walk-sync' import isSVG from 'is-svg' @@ -115,8 +115,8 @@ async function checkFile(filePath: string) { } try { checkSVGContent(content) - } catch (error: any) { - return [CRITICAL, filePath, error.message] + } catch (error: unknown) { + return [CRITICAL, filePath, error instanceof Error ? error.message : String(error)] } } else if (EXPECT[ext]) { const fileType = await fileTypeFromFile(filePath) @@ -138,14 +138,16 @@ async function checkFile(filePath: string) { } function checkSVGContent(content: string) { - const $ = cheerio.load(content) + const $ = load(content) const disallowedTagNames = new Set(['script', 'object', 'iframe', 'embed']) $('*').each((i, element) => { - const { tagName } = $(element).get(0) + const el = $(element).get(0) + if (!el || !('tagName' in el)) return + const { tagName } = el if (disallowedTagNames.has(tagName)) { throw new Error(`contains a <${tagName}> tag`) } - for (const key in $(element).get(0).attribs) { + for (const key in 'attribs' in el ? el.attribs : {}) { // Looks for suspicious event handlers on tags. // For example ` { 1. item three`) const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) expect($('ol').length).toBe(1) expect($('ol > li').length).toBe(3) }) @@ -43,7 +43,7 @@ describe('renderContent', () => { - item`) const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) expect($('ul p').length).toBe(0) }) @@ -69,7 +69,7 @@ describe('renderContent', () => { |g i | Go to the **Issues** tab. For more information, see "[About issues](/articles/about-issues)." `) const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) expect( $.html().includes('"About issues."'), ).toBeTruthy() @@ -82,7 +82,7 @@ describe('renderContent', () => { | Python | \`requirements.txt\`, \`pipfile.lock\` `) const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) expect( $.html().includes('requirements.txt, pipfile.lock'), ).toBeTruthy() @@ -95,7 +95,7 @@ describe('renderContent', () => { | user:USERNAME | [**user:defunkt ubuntu**](https://github.com/search?q=user%3Adefunkt+ubuntu&type=Issues) matches issues with the word "ubuntu" from repositories owned by @defunkt. `) const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) expect($.html().includes('user:USERNAME')).toBeTruthy() }) @@ -110,7 +110,7 @@ describe('renderContent', () => { 1. This is another list item. `) const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) expect($('ol').length).toBe(1) expect($.html().includes('# { ##### This is a level 5 `) const html = await renderContent(template) - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) for (const level of [1, 2, 3, 4, 5]) { expect( @@ -147,7 +147,7 @@ const example = true \`\`\`\` `) let html = await renderContent(template) - let $ = cheerio.load(html, { xmlMode: true }) + let $ = load(html, { xmlMode: true }) expect($.html().includes('
')).toBeTruthy()
     expect($.html().includes('const')).toBeTruthy()
 
@@ -157,7 +157,7 @@ const example = true
 \`\`\`\`
     `)
     html = await renderContent(template)
-    $ = cheerio.load(html, { xmlMode: true })
+    $ = load(html, { xmlMode: true })
     expect($.html().includes('
')).toBeTruthy()
     expect($.html().includes('@articles')).toBeTruthy()
 
@@ -167,7 +167,7 @@ POST / HTTP/2
 \`\`\`\`
     `)
     html = await renderContent(template)
-    $ = cheerio.load(html, { xmlMode: true })
+    $ = load(html, { xmlMode: true })
     expect($.html().includes('
')).toBeTruthy()
     expect($.html().includes('POST')).toBeTruthy()
 
@@ -180,7 +180,7 @@ plugins {
 \`\`\`\`
     `)
     html = await renderContent(template)
-    $ = cheerio.load(html, { xmlMode: true })
+    $ = load(html, { xmlMode: true })
     expect($.html().includes('
')).toBeTruthy()
     expect(
       $.html().includes(''maven-publish''),
@@ -192,7 +192,7 @@ FROM alpine:3.10
 \`\`\`\`
     `)
     html = await renderContent(template)
-    $ = cheerio.load(html, { xmlMode: true })
+    $ = load(html, { xmlMode: true })
     expect($.html().includes('
')).toBeTruthy()
     expect($.html().includes('FROM')).toBeTruthy()
 
@@ -202,7 +202,7 @@ $resourceGroupName = "octocat-testgroup"
 \`\`\`\`
     `)
     html = await renderContent(template)
-    $ = cheerio.load(html, { xmlMode: true })
+    $ = load(html, { xmlMode: true })
     expect($.html().includes('
')).toBeTruthy()
     expect(
       $.html().includes('$resourceGroupName'),
@@ -216,7 +216,7 @@ var a = 1
 \`\`\`
     `)
     const html = await renderContent(template)
-    const $ = cheerio.load(html, { xmlMode: true })
+    const $ = load(html, { xmlMode: true })
     expect($.html().includes('var a = 1')).toBeTruthy()
   })
 
@@ -238,7 +238,7 @@ var a = 1
 \`\`\`
     `)
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
     const el = $('button.js-btn-copy')
     expect(el.data('clipboard')).toBe(2967273189)
     // Generates a murmurhash based ID that matches a 
@@ -250,7 +250,7 @@ var a = 1
 > This is a note with a [link](https://example.com)
     `)
     const html = await renderContent(template, { alertTitles: { NOTE: 'Note' } })
-    const $ = cheerio.load(html)
+    const $ = load(html)
     const alertEl = $('.ghd-alert')
     expect(alertEl.length).toBe(1)
     expect(alertEl.attr('data-container')).toBe('alert')
diff --git a/src/content-render/tests/table-accessibility-labels.ts b/src/content-render/tests/table-accessibility-labels.ts
index 84dd119f4c8f..e17e246cf096 100644
--- a/src/content-render/tests/table-accessibility-labels.ts
+++ b/src/content-render/tests/table-accessibility-labels.ts
@@ -1,4 +1,4 @@
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
 import { describe, expect, test } from 'vitest'
 
 import { renderContent } from '@/content-render/index'
@@ -20,7 +20,7 @@ describe('table accessibility labels', () => {
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const table = $('table')
     expect(table.length).toBe(1)
@@ -42,7 +42,7 @@ describe('table accessibility labels', () => {
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const table = $('table')
     expect(table.attr('aria-labelledby')).toBe('configuration-options')
@@ -59,7 +59,7 @@ describe('table accessibility labels', () => {
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const table = $('table')
     expect(table.attr('aria-label')).toBe('Pre-labeled table')
@@ -78,7 +78,7 @@ describe('table accessibility labels', () => {
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const table = $('table')
     expect(table.find('caption').text()).toBe('Existing caption')
@@ -101,7 +101,7 @@ describe('table accessibility labels', () => {
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const tables = $('table')
     expect(tables.length).toBe(2)
@@ -123,7 +123,7 @@ Some text here.
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const tables = $('table')
     expect(tables.length).toBe(2)
@@ -145,7 +145,7 @@ Some additional context here.
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const table = $('table')
     expect(table.attr('aria-labelledby')).toBe('data-table')
@@ -165,7 +165,7 @@ Some additional context here.
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const tables = $('table')
     expect(tables.length).toBe(2)
@@ -185,7 +185,7 @@ Some additional context here.
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const table = $('table')
     expect(table.attr('aria-labelledby')).toBe('supported-github-actions-features')
@@ -201,7 +201,7 @@ Some additional context here.
 `)
 
     const html = await renderContent(template)
-    const $ = cheerio.load(html)
+    const $ = load(html)
 
     const table = $('table')
     expect(table.find('thead th').length).toBe(2)
diff --git a/src/content-render/unified/alerts.ts b/src/content-render/unified/alerts.ts
index d354a774736d..61dc11d9961d 100644
--- a/src/content-render/unified/alerts.ts
+++ b/src/content-render/unified/alerts.ts
@@ -5,7 +5,7 @@ Custom "Alerts", based on similar filter/styling in the monolith code.
 import { visit } from 'unist-util-visit'
 import { h } from 'hastscript'
 import octicons from '@primer/octicons'
-import type { Element } from 'hast'
+import type { Element, Root, ElementContent } from 'hast'
 
 interface AlertType {
   icon: string
@@ -22,36 +22,33 @@ const alertTypes: Record = {
 
 // Must contain one of [!NOTE], [!IMPORTANT], ...
 const ALERT_REGEXP = new RegExp(`\\[!(${Object.keys(alertTypes).join('|')})\\]`, 'gi')
-
-// Using any due to conflicting unist/hast type definitions between dependencies
-const matcher = (node: any): boolean =>
-  node.type === 'element' &&
-  node.tagName === 'blockquote' &&
-  ALERT_REGEXP.test(JSON.stringify(node.children))
+// Non-global version for .test() and .match() to avoid stateful lastIndex issues
+const ALERT_REGEXP_DETECT = new RegExp(`\\[!(${Object.keys(alertTypes).join('|')})\\]`, 'i')
 
 export default function alerts({ alertTitles = {} }: { alertTitles?: Record }) {
-  // Using any due to conflicting unist/hast type definitions between dependencies
-  return (tree: any) => {
-    // Using any due to conflicting unist/hast type definitions between dependencies
-    visit(tree, matcher, (node: any) => {
-      const key = getAlertKey(node)
+  return (tree: Root) => {
+    visit(tree, 'element', (node) => {
+      const el = node as Element
+      if (el.tagName !== 'blockquote' || !ALERT_REGEXP_DETECT.test(JSON.stringify(el.children)))
+        return
+      const key = getAlertKey(el)
       if (!(key in alertTypes)) {
         console.warn(
           `Alert key '${key}' should be all uppercase (change it to '${key.toUpperCase()}')`,
         )
       }
-      const alertType = alertTypes[getAlertKey(node).toUpperCase()]
-      node.tagName = 'div'
-      node.properties.className = `ghd-alert ghd-alert-${alertType.color}`
-      node.properties.dataContainer = 'alert'
-      node.children = [
+      const alertType = alertTypes[getAlertKey(el).toUpperCase()]
+      el.tagName = 'div'
+      el.properties.className = `ghd-alert ghd-alert-${alertType.color}`
+      el.properties.dataContainer = 'alert'
+      el.children = [
         h(
           'p',
           { className: 'ghd-alert-title' },
           getOcticonSVG(alertType.icon),
           alertTitles[key] || '',
         ),
-        ...removeAlertSyntax(node.children),
+        ...removeAlertSyntax(el.children),
       ]
     })
   }
@@ -59,19 +56,22 @@ export default function alerts({ alertTitles = {} }: { alertTitles?: Record removeAlertSyntax(n))
   }
-  if (node.children) {
-    node.children = node.children.map(removeAlertSyntax)
+  if ('children' in node) {
+    node.children = node.children.map((n) => removeAlertSyntax(n)) as typeof node.children
   }
-  if (node.value) {
+  if ('value' in node) {
     node.value = node.value.replace(ALERT_REGEXP, '')
   }
   return node
diff --git a/src/content-render/unified/code-header.ts b/src/content-render/unified/code-header.ts
index f9e0c13743e3..a2ef46ff2f37 100644
--- a/src/content-render/unified/code-header.ts
+++ b/src/content-render/unified/code-header.ts
@@ -13,44 +13,40 @@ import { fromParse5 } from 'hast-util-from-parse5'
 import murmur from 'imurmurhash'
 import { getPrompt } from './copilot-prompt'
 import { generatePromptId } from '../lib/prompt-id'
-import type { Element } from 'hast'
+import type { Element, Root } from 'hast'
 
 interface LanguageConfig {
   name: string
-  // Using any for language properties that can vary (aliases, extensions, etc.)
-  [key: string]: any
+  [key: string]: string | string[] | boolean | undefined
 }
 
 type Languages = Record
 
 const languages = yaml.load(fs.readFileSync('./data/code-languages.yml', 'utf8')) as Languages
 
-// Using any due to conflicting unist/hast type definitions between dependencies
-const matcher = (node: any): boolean =>
-  node.type === 'element' &&
-  node.tagName === 'pre' &&
-  // For now, limit to ones with the copy or prompt meta,
-  // but we may enable for all examples later.
-  (getPreMeta(node).copy || getPreMeta(node).prompt) &&
-  // Don't add this header for annotated examples.
-  !getPreMeta(node).annotate
-
 export default function codeHeader() {
-  // Using any due to conflicting unist/hast type definitions between dependencies
-  return (tree: any) => {
-    // Using any due to conflicting unist/hast type definitions between dependencies
-    visit(tree, matcher, (node: any, index: number | undefined, parent: any) => {
-      if (index !== undefined && parent) {
-        parent.children[index] = wrapCodeExample(node, tree)
+  return (tree: Root) => {
+    visit(tree, 'element', (node, index, parent) => {
+      const el = node as Element
+      if (
+        el.tagName !== 'pre' ||
+        !(getPreMeta(el).copy || getPreMeta(el).prompt) ||
+        getPreMeta(el).annotate
+      )
+        return
+      if (index !== undefined && parent && 'children' in parent) {
+        ;(parent as Element).children[index] = wrapCodeExample(el, tree)
       }
     })
   }
 }
 
-// Using any due to conflicting unist/hast type definitions between dependencies
-function wrapCodeExample(node: any, tree: any): Element {
-  const lang: string = node.children[0].properties.className?.[0].replace('language-', '')
-  const code: string = node.children[0].children[0].value
+function wrapCodeExample(node: Element, tree: Root): Element {
+  const codeChild = node.children[0] as Element
+  const classNames = codeChild.properties.className as string[] | undefined
+  const lang: string = classNames?.[0]?.replace('language-', '') ?? ''
+  const textNode = codeChild.children[0] as { value: string }
+  const code: string = textNode.value
 
   const subnav = null // getSubnav() lives in annotate.ts, not needed for normal code blocks
   const hasPrompt: boolean = Boolean(getPreMeta(node).prompt)
@@ -120,16 +116,14 @@ export function header(
 function btnIcon(): Element {
   const btnIconHtml: string = octicons.copy.toSVG()
   const btnIconAst = parse(String(btnIconHtml), { sourceCodeLocationInfo: true })
-  // Using any because fromParse5 expects VFile but we only have a string
-  // This is safe because parse5 only needs the string content
-  const btnIconElement = fromParse5(btnIconAst, { file: btnIconHtml as any })
+  const btnIconElement = fromParse5(btnIconAst)
   return btnIconElement as Element
 }
 
-// Using any due to conflicting unist/hast type definitions between dependencies
-// node can be various mdast/hast node types, return value contains meta properties from code blocks
-export function getPreMeta(node: any): Record {
+// node can be various hast element types, return value contains meta properties from code blocks
+export function getPreMeta(node: Element): Record {
   // Here's why this monstrosity works:
   // https://github.com/syntax-tree/mdast-util-to-hast/blob/c87cd606731c88a27dbce4bfeaab913a9589bf83/lib/handlers/code.js#L40-L42
-  return node.children[0]?.data?.meta || {}
+  const firstChild = node.children[0] as Element | undefined
+  return (firstChild?.data as Record> | undefined)?.meta || {}
 }
diff --git a/src/content-render/unified/copilot-prompt.ts b/src/content-render/unified/copilot-prompt.ts
index 19d03cbf57e5..36a2554a87b8 100644
--- a/src/content-render/unified/copilot-prompt.ts
+++ b/src/content-render/unified/copilot-prompt.ts
@@ -9,14 +9,13 @@ import { parse } from 'parse5'
 import { fromParse5 } from 'hast-util-from-parse5'
 import { getPreMeta } from './code-header'
 import { generatePromptId } from '../lib/prompt-id'
+import type { Element, Root } from 'hast'
 
-// node and tree are hast/unist AST nodes without proper TypeScript definitions
-// Returns an object with the prompt button element and the full prompt content
 export function getPrompt(
-  node: any,
-  tree: any,
+  node: Element,
+  tree: Root,
   code: string,
-): { element: any; promptContent: string } | null {
+): { element: Element; promptContent: string } | null {
   const hasPrompt = Boolean(getPreMeta(node).prompt)
   if (!hasPrompt) return null
 
@@ -40,11 +39,9 @@ export function getPrompt(
   return { element, promptContent }
 }
 
-// Using any because node and tree are hast/unist AST nodes without proper TypeScript definitions
-// node is the current code block element, tree is used to find referenced code blocks
 function buildPromptData(
-  node: any,
-  tree: any,
+  node: Element,
+  tree: Root,
   code: string,
 ): { promptContent: string; ariaLabel: string } {
   // Find a ref meta in the format 'ref='
@@ -56,14 +53,18 @@ function buildPromptData(
   }
 
   // If the 'ref=' meta is found, find a matching code block to include as context in the prompt link.
-  const matchingCodeEl = findMatchingCode(ref, tree)
+  const matchingCodeEl = findMatchingCode(ref as string, tree)
   if (!matchingCodeEl) {
     console.warn(`Can't find referenced code block with id=${ref}`)
     return promptOnly(code)
   }
-  // Using any to access children property on untyped hast element node
   // AST structure: element -> code -> text node with value property
-  const matchingCode = (matchingCodeEl as any)?.children[0].children[0].value || null
+  const codeChild = matchingCodeEl.children[0] as Element | undefined
+  const textNode = codeChild?.children[0] as { value?: string } | undefined
+  const matchingCode = textNode?.value || null
+  if (!matchingCode) {
+    return promptOnly(code)
+  }
   return promptAndContext(code, matchingCode)
 }
 
@@ -84,21 +85,17 @@ function promptAndContext(
   }
 }
 
-// Using any because tree and node are hast/unist AST nodes without proper TypeScript definitions
-// Searches the AST tree for a code block with matching id in meta
-function findMatchingCode(ref: string, tree: any): any {
-  return find(tree, (node: any) => {
-    // Using any to access tagName property on untyped hast element node
-    return node.type === 'element' && (node as any).tagName === 'pre' && getPreMeta(node).id === ref
-  })
+function findMatchingCode(ref: string, tree: Root): Element | undefined {
+  return find(tree, ((node: { type: string; tagName?: string }) => {
+    return (
+      node.type === 'element' && node.tagName === 'pre' && getPreMeta(node as Element).id === ref
+    )
+  }) as Parameters[1])
 }
 
-// Returns a hast element node for the Copilot icon
-// Using any return type because fromParse5 returns untyped hast nodes
-function copilotIcon(): any {
+function copilotIcon(): Element {
   const copilotIconHtml = octicons.copilot.toSVG()
   const copilotIconAst = parse(String(copilotIconHtml), { sourceCodeLocationInfo: true })
-  // Using any because fromParse5 expects VFile but we only have a string
-  const copilotIconElement = fromParse5(copilotIconAst, { file: copilotIconHtml as any })
-  return copilotIconElement
+  const copilotIconElement = fromParse5(copilotIconAst)
+  return copilotIconElement as Element
 }
diff --git a/src/content-render/unified/rewrite-empty-table-rows.ts b/src/content-render/unified/rewrite-empty-table-rows.ts
index d10e95e875c5..997fbecb963d 100644
--- a/src/content-render/unified/rewrite-empty-table-rows.ts
+++ b/src/content-render/unified/rewrite-empty-table-rows.ts
@@ -1,4 +1,5 @@
 import { visit, SKIP } from 'unist-util-visit'
+import type { Element, Root } from 'hast'
 
 /**
  * Where it can mutate the AST to swap from:
@@ -49,31 +50,23 @@ import { visit, SKIP } from 'unist-util-visit'
  * isn't the same all the way down. But Unified will still parse it.
  * */
 
-// node is a hast element node without proper TypeScript definitions
-function matcher(node: any): boolean {
-  return node.type === 'element' && node.tagName === 'tr'
-}
-
-// node, parent, and grandChild are hast element nodes without proper TypeScript definitions
-function visitor(
-  node: any,
-  index: number | undefined,
-  parent: any,
-): [typeof SKIP, number] | undefined {
-  if (
-    node.children.every(
-      (grandChild: any) =>
-        grandChild.type === 'element' && grandChild.tagName === 'td' && !grandChild.children.length,
-    )
-  ) {
-    if (index !== undefined) {
-      parent.children.splice(index, 1)
-      return [SKIP, index]
-    }
-  }
-}
-
-// tree is a hast root node without proper TypeScript definitions
 export default function rewriteEmptyTableRows() {
-  return (tree: any) => visit(tree, matcher, visitor)
+  return (tree: Root) =>
+    visit(tree, 'element', (node, index, parent) => {
+      const el = node as Element
+      if (el.tagName !== 'tr') return
+      if (
+        el.children.every(
+          (grandChild) =>
+            grandChild.type === 'element' &&
+            grandChild.tagName === 'td' &&
+            !grandChild.children.length,
+        )
+      ) {
+        if (index !== undefined && parent) {
+          parent.children.splice(index, 1)
+          return [SKIP, index] as const
+        }
+      }
+    })
 }
diff --git a/src/content-render/unified/rewrite-for-rowheaders.ts b/src/content-render/unified/rewrite-for-rowheaders.ts
index f86e1d7162dc..c547e8c5c801 100644
--- a/src/content-render/unified/rewrite-for-rowheaders.ts
+++ b/src/content-render/unified/rewrite-for-rowheaders.ts
@@ -1,21 +1,10 @@
 import { visitParents } from 'unist-util-visit-parents'
+import type { Element, Root } from 'hast'
 
-interface ElementNode {
-  type: 'element'
-  tagName: string
-  properties: {
-    // Properties can have any value type (strings, booleans, arrays, etc.)
-    [key: string]: any
-  }
+interface ScopedElement extends Element {
   _scoped?: boolean
 }
 
-interface AncestorNode {
-  properties?: {
-    className?: string[]
-  }
-}
-
 /**
  * Where it can mutate the AST to swap from:
  *
@@ -49,32 +38,28 @@ interface AncestorNode {
  *
  * */
 
-function matcher(node: any): node is ElementNode {
-  return node.type === 'element' && node.tagName === 'td' && !('scope' in node.properties)
-}
-
-function insideRowheaders(ancestors: AncestorNode[]): boolean {
-  return ancestors.some(
-    (node: AncestorNode) =>
-      node.properties &&
-      node.properties.className &&
-      node.properties.className.includes('rowheaders'),
-  )
-}
+export default function rewriteForRowheaders() {
+  return (tree: Root) =>
+    visitParents(tree, 'element', (node, ancestors) => {
+      const el = node as Element
+      if (el.tagName !== 'td' || 'scope' in el.properties) return
 
-// ancestors is an array of hast nodes without proper TypeScript definitions
-function visitor(node: ElementNode, ancestors: any[]): void {
-  if (insideRowheaders(ancestors)) {
-    const tr = ancestors.at(-1) as ElementNode
-    if (!tr._scoped) {
-      tr._scoped = true
-      node.properties.scope = 'row'
-      node.tagName = 'th'
-    }
-  }
-}
+      const insideRowheaders = ancestors.some((ancestor) => {
+        const ancestorEl = ancestor as Partial
+        return (
+          ancestorEl.properties &&
+          Array.isArray(ancestorEl.properties.className) &&
+          ancestorEl.properties.className.includes('rowheaders')
+        )
+      })
 
-// tree is a hast root node without proper TypeScript definitions
-export default function rewriteForRowheaders() {
-  return (tree: any) => visitParents(tree, matcher, visitor)
+      if (insideRowheaders) {
+        const tr = ancestors.at(-1) as ScopedElement
+        if (!tr._scoped) {
+          tr._scoped = true
+          el.properties.scope = 'row'
+          el.tagName = 'th'
+        }
+      }
+    })
 }
diff --git a/src/content-render/unified/text-only.ts b/src/content-render/unified/text-only.ts
index 6488b1f90593..83ce950a5b0a 100644
--- a/src/content-render/unified/text-only.ts
+++ b/src/content-render/unified/text-only.ts
@@ -1,4 +1,4 @@
-import cheerio from 'cheerio'
+import { load } from 'cheerio'
 import { decode } from 'html-entities'
 
 // Given a piece of HTML return it without HTML. E.g.
@@ -13,6 +13,6 @@ export function fastTextOnly(html: string): string {
     const middle = html.slice(3, -4)
     if (!middle.includes('<')) return decode(middle.trim())
   }
-  const $ = cheerio.load(html, { xmlMode: true })
+  const $ = load(html, { xmlMode: true })
   return $.root().text().trim()
 }
diff --git a/src/data-directory/lib/data-schemas/features.ts b/src/data-directory/lib/data-schemas/features.ts
index 8cf101ff16b1..1b9aa310350b 100644
--- a/src/data-directory/lib/data-schemas/features.ts
+++ b/src/data-directory/lib/data-schemas/features.ts
@@ -1,9 +1,16 @@
 import { schema } from '@/frame/lib/frontmatter'
 
+interface FeatureVersionsProperties {
+  type?: string | string[]
+  properties?: Record
+  additionalProperties?: boolean
+  [key: string]: unknown
+}
+
 interface FeatureVersionsSchema {
   type: 'object'
   properties: {
-    versions: any
+    versions: FeatureVersionsProperties
   }
   additionalProperties: false
 }
@@ -12,13 +19,14 @@ interface FeatureVersionsSchema {
 const featureVersions: FeatureVersionsSchema = {
   type: 'object',
   properties: {
-    versions: Object.assign({}, (schema.properties as any).versions),
+    versions: Object.assign({}, schema.properties.versions) as FeatureVersionsProperties,
   },
   additionalProperties: false,
 }
 
 // Remove the feature versions properties.
 // We don't want to allow features within features! We just want pure versioning.
-delete featureVersions.properties.versions.properties.feature
+delete (featureVersions.properties.versions.properties as Record | undefined)
+  ?.feature
 
 export default featureVersions
diff --git a/src/dev-toc/generate.ts b/src/dev-toc/generate.ts
index f836d8afa282..5bc80594a1ce 100644
--- a/src/dev-toc/generate.ts
+++ b/src/dev-toc/generate.ts
@@ -15,7 +15,7 @@ import path from 'path'
 import { execSync } from 'child_process'
 import { program } from 'commander'
 import type { NextFunction, Response } from 'express'
-import type { ExtendedRequest } from '@/types'
+import type { ExtendedRequest, Context } from '@/types'
 import fpt from '@/versions/lib/non-enterprise-default-version'
 import { allVersionKeys } from '@/versions/lib/all-versions'
 import { liquid } from '@/content-render/index'
@@ -60,10 +60,16 @@ async function main(): Promise {
     get: () => '',
     header: () => '',
     accepts: () => false,
-    context: {} as any,
+    context: {} as Context,
   } as unknown as ExtendedRequest
 
-  async function recurse(tree: any): Promise {
+  interface PageTreeNode {
+    page: { rawTitle: string }
+    renderedFullTitle?: string
+    childPages?: PageTreeNode[]
+  }
+
+  async function recurse(tree: PageTreeNode): Promise {
     const { page } = tree
     tree.renderedFullTitle = page.rawTitle.includes('{')
       ? await liquid.parseAndRender(page.rawTitle, req.context)
@@ -92,7 +98,7 @@ async function main(): Promise {
     }
 
     if (req.context && req.context.currentEnglishTree) {
-      await recurse(req.context.currentEnglishTree)
+      await recurse(req.context.currentEnglishTree as PageTreeNode)
     }
 
     // Add any defaultOpenSections to the context.
diff --git a/src/fixtures/tests/annotations.ts b/src/fixtures/tests/annotations.ts
index 9a87ca2401ac..87f35190137e 100644
--- a/src/fixtures/tests/annotations.ts
+++ b/src/fixtures/tests/annotations.ts
@@ -1,11 +1,11 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { getDOM } from '@/tests/helpers/e2etest'
 
 describe('annotations', () => {
   test('code-snippet-with-hashbang', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/foo/code-snippet-with-hashbang')
+    const $: CheerioAPI = await getDOM('/get-started/foo/code-snippet-with-hashbang')
     const annotations = $('#article-contents .annotate')
 
     // Check http://localhost:4000/en/get-started/foo/code-snippet-with-hashbang
diff --git a/src/fixtures/tests/breadcrumbs.ts b/src/fixtures/tests/breadcrumbs.ts
index 66eb05784a80..9356ec762215 100644
--- a/src/fixtures/tests/breadcrumbs.ts
+++ b/src/fixtures/tests/breadcrumbs.ts
@@ -1,5 +1,7 @@
 import { describe, expect, test } from 'vitest'
 
+import type { Element } from 'domhandler'
+
 import { getDOM } from '@/tests/helpers/e2etest'
 
 describe('breadcrumbs', () => {
@@ -68,7 +70,7 @@ describe('breadcrumbs', () => {
 
     expect($breadcrumbTitles.length).toBe(0)
     expect($breadcrumbLinks.length).toBe(2)
-    expect(($breadcrumbLinks[0] as cheerio.TagElement).attribs.title).toBe('Deeper secrets')
-    expect(($breadcrumbLinks[1] as cheerio.TagElement).attribs.title).toBe('Mariana Trench')
+    expect(($breadcrumbLinks[0] as Element).attribs.title).toBe('Deeper secrets')
+    expect(($breadcrumbLinks[1] as Element).attribs.title).toBe('Mariana Trench')
   })
 })
diff --git a/src/fixtures/tests/categories-and-subcategory.ts b/src/fixtures/tests/categories-and-subcategory.ts
index 3d16eb951a6a..01fb0a629124 100644
--- a/src/fixtures/tests/categories-and-subcategory.ts
+++ b/src/fixtures/tests/categories-and-subcategory.ts
@@ -1,11 +1,11 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { getDOM, head } from '@/tests/helpers/e2etest'
 
 describe('subcategories', () => {
   test('get-started/start-your-journey subcategory', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/start-your-journey')
+    const $: CheerioAPI = await getDOM('/get-started/start-your-journey')
     const lead = $('[data-search=lead]').text()
     expect(lead).toMatch('Get started using HubGit to manage Git repositories')
 
@@ -22,7 +22,7 @@ describe('subcategories', () => {
   })
 
   test('actions/category/subcategory subcategory has its articles intro', async () => {
-    const $: cheerio.Root = await getDOM('/actions/category/subcategory')
+    const $: CheerioAPI = await getDOM('/actions/category/subcategory')
     const lead = $('[data-search=lead]').text()
     expect(lead).toMatch("Here's the intro for HubGit Actions.")
 
@@ -43,7 +43,7 @@ describe('subcategories', () => {
 
 describe('categories', () => {
   test('actions/category subcategory', async () => {
-    const $: cheerio.Root = await getDOM('/actions/category')
+    const $: CheerioAPI = await getDOM('/actions/category')
     const lead = $('[data-search=lead]').text()
     expect(lead).toMatch('Learn how to migrate your existing CI/CD')
 
diff --git a/src/fixtures/tests/footer.ts b/src/fixtures/tests/footer.ts
index 3fb6e8d48344..cfb61ee35559 100644
--- a/src/fixtures/tests/footer.ts
+++ b/src/fixtures/tests/footer.ts
@@ -1,5 +1,5 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { getDOM } from '@/tests/helpers/e2etest'
 import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version'
@@ -7,7 +7,7 @@ import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-v
 describe('footer', () => {
   describe('"contact us" link', () => {
     test('leads to support from articles', async () => {
-      const $: cheerio.Root = await getDOM(
+      const $: CheerioAPI = await getDOM(
         `/en/${nonEnterpriseDefaultVersion}/get-started/start-your-journey/hello-world`,
       )
       expect($('a#support').attr('href')).toBe('https://support.github.com')
@@ -16,21 +16,21 @@ describe('footer', () => {
     test('leads to support on 404 pages', async () => {
       // Important to use the prefix /en/ on the failing URL or else
       // it will render a very basic plain text 404 response.
-      const $: cheerio.Root = await getDOM('/en/delicious-snacks/donuts.php', { allow404: true })
+      const $: CheerioAPI = await getDOM('/en/delicious-snacks/donuts.php', { allow404: true })
       expect($('a#support').attr('href')).toBe('https://support.github.com')
     })
   })
 
   describe('"support" link with nextjs', () => {
     test('leads to support from articles', async () => {
-      const $: cheerio.Root = await getDOM(`/en/${nonEnterpriseDefaultVersion}/get-started?nextjs=`)
+      const $: CheerioAPI = await getDOM(`/en/${nonEnterpriseDefaultVersion}/get-started?nextjs=`)
       expect($('a#support').attr('href')).toBe('https://support.github.com')
     })
   })
 
   describe('test redirects for product landing community links pages', () => {
     test('codespaces product landing page leads to discussions page', async () => {
-      const $: cheerio.Root = await getDOM('/en/get-started')
+      const $: CheerioAPI = await getDOM('/en/get-started')
       expect($('a#ask-community').attr('href')).toBe(
         'https://hubgit.com/orgs/community/discussions/categories/get-started',
       )
@@ -39,7 +39,7 @@ describe('footer', () => {
 
   describe('test redirects for non-product landing community links pages', () => {
     test('leads to https://github.community/ when clicking on the community link', async () => {
-      const $: cheerio.Root = await getDOM(`/en/get-started/start-your-journey/hello-world`)
+      const $: CheerioAPI = await getDOM(`/en/get-started/start-your-journey/hello-world`)
       expect($('a#ask-community').attr('href')).toBe(
         'https://github.com/orgs/community/discussions',
       )
diff --git a/src/fixtures/tests/glossary.ts b/src/fixtures/tests/glossary.ts
index 70b314bc7dca..e214b7927aa4 100644
--- a/src/fixtures/tests/glossary.ts
+++ b/src/fixtures/tests/glossary.ts
@@ -1,20 +1,20 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { getDOMCached as getDOM } from '@/tests/helpers/e2etest'
 
 describe('glossary', () => {
   test('headings are sorted alphabetically', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/learning-about-github/github-glossary')
+    const $: CheerioAPI = await getDOM('/get-started/learning-about-github/github-glossary')
     const headings = $('#article-contents h2')
-    const headingTexts = headings.map((_: number, el: any) => $(el).text()).get()
+    const headingTexts = headings.map((_, el) => $(el).text()).get()
     const cloned = [...headingTexts].sort((a: string, b: string) => a.localeCompare(b))
     const equalStringArray = (a: string[], b: string[]) =>
       a.length === b.length && a.every((v, i) => v === b[i])
     expect(equalStringArray(headingTexts, cloned)).toBe(true)
   })
   test('Markdown links are correct', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/learning-about-github/github-glossary')
+    const $: CheerioAPI = await getDOM('/get-started/learning-about-github/github-glossary')
     const internalLink = $('#article-contents a[href="/en/get-started/foo"]')
     expect(internalLink.length).toBe(1)
     // That link used AUTOTITLE so it should be "expanded"
@@ -22,19 +22,19 @@ describe('glossary', () => {
   })
 
   test('all Liquid is evaluated', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/learning-about-github/github-glossary')
+    const $: CheerioAPI = await getDOM('/get-started/learning-about-github/github-glossary')
     const paragraphs = $('#article-contents p')
-    const paragraphTexts = paragraphs.map((_: number, el: any) => $(el).text()).get()
+    const paragraphTexts = paragraphs.map((_, el) => $(el).text()).get()
     expect(paragraphTexts.find((text: string) => text.includes('{%'))).toBe(undefined)
   })
 
   test('liquid in one of the description depends on version', async () => {
     // fpt
     {
-      const $: cheerio.Root = await getDOM('/get-started/learning-about-github/github-glossary')
+      const $: CheerioAPI = await getDOM('/get-started/learning-about-github/github-glossary')
       const paragraphs = $('#article-contents p')
       const paragraphTexts = paragraphs
-        .map((_: number, el: any) => $(el).text())
+        .map((_, el) => $(el).text())
         .get()
         .join('\n')
       expect(paragraphTexts).toContain('status check on HubGit.')
@@ -42,12 +42,12 @@ describe('glossary', () => {
 
     // ghes
     {
-      const $: cheerio.Root = await getDOM(
+      const $: CheerioAPI = await getDOM(
         '/enterprise-server@latest/get-started/learning-about-github/github-glossary',
       )
       const paragraphs = $('#article-contents p')
       const paragraphTexts = paragraphs
-        .map((_: number, el: any) => $(el).text())
+        .map((_, el) => $(el).text())
         .get()
         .join('\n')
       expect(paragraphTexts).toContain('status check on HubGit Enterprise Server.')
diff --git a/src/fixtures/tests/guides.ts b/src/fixtures/tests/guides.ts
index 1e1663303601..ff4adb7d231d 100644
--- a/src/fixtures/tests/guides.ts
+++ b/src/fixtures/tests/guides.ts
@@ -1,11 +1,11 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { get, getDOMCached as getDOM } from '@/tests/helpers/e2etest'
 
 describe('guides', () => {
   test("page's title should be document title", async () => {
-    const $: cheerio.Root = await getDOM('/code-security/guides')
+    const $: CheerioAPI = await getDOM('/code-security/guides')
     // This is what you'd find in src/fixtures/fixtures/content/code-security/guides.md
     const title = 'Guides for cool security'
     expect($('title').text()).toMatch(title)
@@ -19,7 +19,7 @@ describe('guides', () => {
 
 describe('learning tracks', () => {
   test('start the first learning track and come back via the navigation banner', async () => {
-    const $: cheerio.Root = await getDOM('/code-security/guides')
+    const $: CheerioAPI = await getDOM('/code-security/guides')
     const links = $('[data-testid=learning-track] a')
     const link = links
       .filter((_: number, el: any) => $(el).text() === 'Start learning path')
@@ -28,7 +28,7 @@ describe('learning tracks', () => {
     expect(link.attr('href')).toMatch('learnProduct=code-security')
 
     // Go the first "Start learning path" link
-    const $2: cheerio.Root = await getDOM(link.attr('href')!)
+    const $2: CheerioAPI = await getDOM(link.attr('href')!)
     const card2 = $2('[data-testid=learning-track-card]')
     // The card has 2 links. One to go back to the guide page
     // whose title is the name of the learning track
@@ -57,7 +57,7 @@ describe('learning tracks', () => {
     expect(navNextLink.text()).toBe('Securing your organization')
 
     // Go to the next (last) page
-    const $3: cheerio.Root = await getDOM(nextLink.attr('href')!)
+    const $3: CheerioAPI = await getDOM(nextLink.attr('href')!)
     const card3 = $3('[data-testid=learning-track-card]')
     const span3 = card3.find('span').filter((_: number, el: any) => $(el).text().includes('2 of 2'))
     expect(span3.text()).toBe('2 of 2 in learning path')
@@ -82,15 +82,13 @@ describe('learning tracks', () => {
   test('with and without a valid ?learn= query string', async () => {
     // Valid
     {
-      const $: cheerio.Root = await getDOM(
-        '/code-security/getting-started/quickstart?learn=foo_bar',
-      )
+      const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart?learn=foo_bar')
       expect($('[data-testid=learning-track-card]').length).toBe(1)
       expect($('[data-testid=learning-track-nav]').length).toBe(1)
     }
     // Invalid
     {
-      const $: cheerio.Root = await getDOM(
+      const $: CheerioAPI = await getDOM(
         '/code-security/getting-started/quickstart?learn=blablainvalid',
       )
       expect($('[data-testid=learning-track-card]').length).toBe(0)
@@ -98,13 +96,13 @@ describe('learning tracks', () => {
     }
     // Empty
     {
-      const $: cheerio.Root = await getDOM('/code-security/getting-started/quickstart?learn=')
+      const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart?learn=')
       expect($('[data-testid=learning-track-card]').length).toBe(0)
       expect($('[data-testid=learning-track-nav]').length).toBe(0)
     }
     // Missing
     {
-      const $: cheerio.Root = await getDOM('/code-security/getting-started/quickstart')
+      const $: CheerioAPI = await getDOM('/code-security/getting-started/quickstart')
       expect($('[data-testid=learning-track-card]').length).toBe(0)
       expect($('[data-testid=learning-track-nav]').length).toBe(0)
     }
diff --git a/src/fixtures/tests/head.ts b/src/fixtures/tests/head.ts
index 99ed5c9e804a..253f43190423 100644
--- a/src/fixtures/tests/head.ts
+++ b/src/fixtures/tests/head.ts
@@ -1,11 +1,11 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { getDOM } from '@/tests/helpers/e2etest'
 
 describe('', () => {
   test('includes page intro in `description` meta tag', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/markdown/intro')
+    const $: CheerioAPI = await getDOM('/get-started/markdown/intro')
     // The intro has Markdown syntax which becomes HTML encoded in the lead element.
     const lead = $('[data-testid="lead"] p')
     expect(lead.html()).toMatch('syntax')
diff --git a/src/fixtures/tests/homepage.ts b/src/fixtures/tests/homepage.ts
index 0b259e63de23..8ba9d7ece5e0 100644
--- a/src/fixtures/tests/homepage.ts
+++ b/src/fixtures/tests/homepage.ts
@@ -1,11 +1,11 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { get, getDOM } from '@/tests/helpers/e2etest'
 
 describe('home page', () => {
   test('landing area', async () => {
-    const $: cheerio.Root = await getDOM('/')
+    const $: CheerioAPI = await getDOM('/')
     const container = $('#landing')
     expect(container.length).toBe(1)
     expect(container.find('h1').text()).toBe('GitHub Docs')
@@ -13,10 +13,10 @@ describe('home page', () => {
   })
 
   test('product groups can use Liquid', async () => {
-    const $: cheerio.Root = await getDOM('/')
+    const $: CheerioAPI = await getDOM('/')
     const main = $('[data-testid="product"]')
     const links = main.find('a[href*="/"]')
-    const hrefs = links.map((i: number, link: any) => $(link)).get()
+    const hrefs = links.map((_, link) => $(link)).get()
     let externalLinks = 0
     for (const href of hrefs) {
       if (!href.attr('href')?.startsWith('https://')) {
diff --git a/src/fixtures/tests/html-comments.ts b/src/fixtures/tests/html-comments.ts
index 62d8bcf42088..6ed1d098eede 100644
--- a/src/fixtures/tests/html-comments.ts
+++ b/src/fixtures/tests/html-comments.ts
@@ -1,11 +1,11 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { getDOMCached as getDOM } from '@/tests/helpers/e2etest'
 
 describe('html-comments', () => {
   test('regular comments are removed', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/markdown/html-comments')
+    const $: CheerioAPI = await getDOM('/get-started/markdown/html-comments')
     const contents = $('#article-contents')
     const html = contents.html()
     expect(html).not.toContain('This comment should get deleted since it mentions gooblygook')
diff --git a/src/fixtures/tests/images.ts b/src/fixtures/tests/images.ts
index 876e6902b006..229d9c188d24 100644
--- a/src/fixtures/tests/images.ts
+++ b/src/fixtures/tests/images.ts
@@ -1,13 +1,13 @@
 import { describe, expect, test } from 'vitest'
 import sharp from 'sharp'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { get, head, getDOM } from '@/tests/helpers/e2etest'
 import { MAX_WIDTH } from '@/content-render/unified/rewrite-asset-img-tags'
 
 describe('render Markdown image tags', () => {
   test('page with a single image', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/images/single-image')
+    const $: CheerioAPI = await getDOM('/get-started/images/single-image')
 
     const pictures = $('#article-contents picture')
     expect(pictures.length).toBe(1)
@@ -46,7 +46,7 @@ describe('render Markdown image tags', () => {
   })
 
   test('images have density specified', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/images/retina-image')
+    const $: CheerioAPI = await getDOM('/get-started/images/retina-image')
 
     const pictures = $('#article-contents picture')
     expect(pictures.length).toBe(3)
@@ -60,14 +60,14 @@ describe('render Markdown image tags', () => {
   })
 
   test('image inside a list keeps its span', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/images/images-in-lists')
+    const $: CheerioAPI = await getDOM('/get-started/images/images-in-lists')
 
     const imageSpan = $('#article-contents > div > ol > li > div.procedural-image-wrapper')
     expect(imageSpan.length).toBe(1)
   })
 
   test("links directly to images aren't rewritten", async () => {
-    const $: cheerio.Root = await getDOM('/get-started/images/link-to-image')
+    const $: CheerioAPI = await getDOM('/get-started/images/link-to-image')
     // There is only 1 link inside that page
     const links = $('#article-contents a[href^="/"]') // exclude header link
     expect(links.length).toBe(1)
diff --git a/src/fixtures/tests/internal-links.ts b/src/fixtures/tests/internal-links.ts
index e0847dc2395e..353dc286168c 100644
--- a/src/fixtures/tests/internal-links.ts
+++ b/src/fixtures/tests/internal-links.ts
@@ -1,5 +1,6 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
+import type { Element } from 'domhandler'
 
 import { get, getDOM } from '@/tests/helpers/e2etest'
 import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases'
@@ -7,9 +8,9 @@ import { allVersions } from '@/versions/lib/all-versions'
 
 describe('autotitle', () => {
   test('internal links with AUTOTITLE resolves', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/foo/autotitling')
+    const $: CheerioAPI = await getDOM('/get-started/foo/autotitling')
     const links = $('#article-contents a[href]')
-    links.each((i: number, element: cheerio.Element) => {
+    links.each((i: number, element: Element) => {
       if ($(element).attr('href')?.includes('/get-started/start-your-journey/hello-world')) {
         expect($(element).text()).toBe('Hello World')
       }
@@ -44,19 +45,19 @@ describe('cross-version-links', () => {
     'links to free-pro-team should be implicit even on %p',
     async (version: string) => {
       const URL = `/${version}/get-started/foo/cross-version-linking`
-      const $: cheerio.Root = await getDOM(URL)
+      const $: CheerioAPI = await getDOM(URL)
       const links = $('#article-contents a[href]')
 
       // Tests that the hardcoded prefix is always removed
       const firstLink = links.filter(
-        (i: number, element: cheerio.Element) =>
+        (i: number, element: Element) =>
           $(element).text() === 'Hello world always in free-pro-team',
       )
       expect(firstLink.attr('href')).toBe('/en/get-started/start-your-journey/hello-world')
 
       // Tests that the second link always goes to enterprise-server@X.Y
       const secondLink = links.filter(
-        (i: number, element: cheerio.Element) =>
+        (i: number, element: Element) =>
           $(element).text() === 'Autotitling page always in enterprise-server latest',
       )
       expect(secondLink.attr('href')).toBe(
@@ -68,12 +69,12 @@ describe('cross-version-links', () => {
 
 describe('link-rewriting', () => {
   test('/en is injected', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/start-your-journey/link-rewriting')
+    const $: CheerioAPI = await getDOM('/get-started/start-your-journey/link-rewriting')
     const links = $('#article-contents a[href]')
 
     {
       const link = links.filter(
-        (i: number, element: cheerio.Element) => $(element).text() === 'Cross Version Linking',
+        (i: number, element: Element) => $(element).text() === 'Cross Version Linking',
       )
       expect(link.attr('href')).toMatch('/en/get-started/')
     }
@@ -81,25 +82,25 @@ describe('link-rewriting', () => {
     // Some links are left untouched
 
     {
-      const link = links.filter((i: number, element: cheerio.Element) =>
+      const link = links.filter((i: number, element: Element) =>
         $(element).text().includes('Enterprise 11.10'),
       )
       expect(link.attr('href')).toMatch('/en/enterprise/')
     }
     {
-      const link = links.filter((i: number, element: cheerio.Element) =>
+      const link = links.filter((i: number, element: Element) =>
         $(element).text().includes('peterbe'),
       )
       expect(link.attr('href')).toMatch(/^https:/)
     }
     {
-      const link = links.filter((i: number, element: cheerio.Element) =>
+      const link = links.filter((i: number, element: Element) =>
         $(element).text().includes('Picture'),
       )
       expect(link.attr('href')).toMatch(/^\/assets\//)
     }
     {
-      const link = links.filter((i: number, element: cheerio.Element) =>
+      const link = links.filter((i: number, element: Element) =>
         $(element).text().includes('GraphQL Schema'),
       )
       expect(link.attr('href')).toMatch(/^\/public\//)
@@ -107,26 +108,26 @@ describe('link-rewriting', () => {
   })
 
   test('/en and current version (latest) is injected', async () => {
-    const $: cheerio.Root = await getDOM(
+    const $: CheerioAPI = await getDOM(
       '/enterprise-cloud@latest/get-started/start-your-journey/link-rewriting',
     )
     const links = $('#article-contents a[href]')
 
     const link = links.filter(
-      (i: number, element: cheerio.Element) => $(element).text() === 'Cross Version Linking',
+      (i: number, element: Element) => $(element).text() === 'Cross Version Linking',
     )
     expect(link.attr('href')).toMatch('/en/enterprise-cloud@latest/get-started/')
   })
 
   test('/en and current version number is injected', async () => {
     // enterprise-server, unlike enterprise-cloud, use numbers
-    const $: cheerio.Root = await getDOM(
+    const $: CheerioAPI = await getDOM(
       '/enterprise-server@latest/get-started/start-your-journey/link-rewriting',
     )
     const links = $('#article-contents a[href]')
 
     const link = links.filter(
-      (i: number, element: cheerio.Element) => $(element).text() === 'Cross Version Linking',
+      (i: number, element: Element) => $(element).text() === 'Cross Version Linking',
     )
     expect(link.attr('href')).toMatch(
       `/en/enterprise-server@${enterpriseServerReleases.latestStable}/get-started/`,
@@ -136,16 +137,16 @@ describe('link-rewriting', () => {
 
 describe('subcategory links', () => {
   test('no free-pro-team prefix', async () => {
-    const $: cheerio.Root = await getDOM('/rest/actions')
+    const $: CheerioAPI = await getDOM('/rest/actions')
     const links = $('[data-testid="table-of-contents"] a[href]')
-    links.each((i: number, element: cheerio.Element) => {
+    links.each((i: number, element: Element) => {
       expect($(element).attr('href')).not.toContain('/free-pro-team@latest')
     })
   })
   test('enterprise-server prefix', async () => {
-    const $: cheerio.Root = await getDOM('/enterprise-server@latest/rest/actions')
+    const $: CheerioAPI = await getDOM('/enterprise-server@latest/rest/actions')
     const links = $('[data-testid="table-of-contents"] a[href]')
-    links.each((i: number, element: cheerio.Element) => {
+    links.each((i: number, element: Element) => {
       expect($(element).attr('href')).toMatch(/\/enterprise-server@\d/)
     })
   })
diff --git a/src/fixtures/tests/landing-hero.ts b/src/fixtures/tests/landing-hero.ts
index 15eb263cf6e1..743b339fa793 100644
--- a/src/fixtures/tests/landing-hero.ts
+++ b/src/fixtures/tests/landing-hero.ts
@@ -1,23 +1,23 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { getDOM } from '@/tests/helpers/e2etest'
 
 describe('product landing page', () => {
   test('product landing page displays full title', async () => {
-    const $: cheerio.Root = await getDOM('/get-started')
+    const $: CheerioAPI = await getDOM('/get-started')
     expect($('h1').first().text()).toMatch(/Getting started with HubGit/)
   })
 
   test('product landing page lists with shortTitle heading (free-pro-team)', async () => {
-    const $: cheerio.Root = await getDOM('/pages')
+    const $: CheerioAPI = await getDOM('/pages')
     // Note that this particular page (in the fixtures) has Liquid
     // in its shorTitle.
     expect($('#all-docs a').first().text()).toMatch('All Pages (HubGit) docs')
   })
 
   test('product landing page lists with shortTitle heading (enterprise-server)', async () => {
-    const $: cheerio.Root = await getDOM('/enterprise-server@latest/pages')
+    const $: CheerioAPI = await getDOM('/enterprise-server@latest/pages')
     // Note that this particular page (in the fixtures) has Liquid
     // in its shorTitle.
     expect($('#all-docs a').first().text()).toMatch('All Pages (HubGit Enterprise Server) docs')
diff --git a/src/fixtures/tests/liquid.ts b/src/fixtures/tests/liquid.ts
index c559d56399fb..80bad84ff455 100644
--- a/src/fixtures/tests/liquid.ts
+++ b/src/fixtures/tests/liquid.ts
@@ -1,5 +1,5 @@
 import { describe, expect, test } from 'vitest'
-import cheerio from 'cheerio'
+import type { CheerioAPI } from 'cheerio'
 
 import { getDataByLanguage } from '@/data-directory/lib/get-data'
 import { getDOM } from '@/tests/helpers/e2etest'
@@ -7,28 +7,28 @@ import { supported } from '@/versions/lib/enterprise-server-releases'
 
 describe('spotlight', () => {
   test('renders styled warnings', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/liquid/warnings')
+    const $: CheerioAPI = await getDOM('/get-started/liquid/warnings')
     const nodes = $('.ghd-spotlight-attention')
     expect(nodes.length).toBe(1)
     expect(nodes.text().includes('This is inside the warning.')).toBe(true)
   })
 
   test('renders styled danger', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/liquid/danger')
+    const $: CheerioAPI = await getDOM('/get-started/liquid/danger')
     const nodes = $('.ghd-spotlight-danger')
     expect(nodes.length).toBe(1)
     expect(nodes.text().includes('Danger, Will Robinson.')).toBe(true)
   })
 
   test('renders styled tips', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/liquid/tips')
+    const $: CheerioAPI = await getDOM('/get-started/liquid/tips')
     const nodes = $('.ghd-spotlight-success')
     expect(nodes.length).toBe(1)
     expect(nodes.text().includes('This is inside the tip.')).toBe(true)
   })
 
   test('renders styled notes', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/liquid/notes')
+    const $: CheerioAPI = await getDOM('/get-started/liquid/notes')
     const nodes = $('.ghd-spotlight-accent')
     expect(nodes.length).toBe(1)
     expect(nodes.text().includes('This is inside the note.')).toBe(true)
@@ -37,7 +37,7 @@ describe('spotlight', () => {
 
 describe('raw', () => {
   test('renders raw', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/liquid/raw')
+    const $: CheerioAPI = await getDOM('/get-started/liquid/raw')
     const lead = $('[data-testid="lead"]').html()
     expect(lead).toMatch('{% raw %}')
     const code = $('pre code').html()
@@ -48,7 +48,7 @@ describe('raw', () => {
 
 describe('tool', () => {
   test('renders platform-specific content', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/liquid/platform-specific')
+    const $: CheerioAPI = await getDOM('/get-started/liquid/platform-specific')
     expect($('.ghd-tool.mac p').length).toBe(1)
     expect($('.ghd-tool.mac p').text().includes('mac specific content')).toBe(true)
     expect($('.ghd-tool.windows p').length).toBe(1)
@@ -58,7 +58,7 @@ describe('tool', () => {
   })
 
   test('renders expected mini TOC headings in platform-specific content', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/liquid/platform-specific')
+    const $: CheerioAPI = await getDOM('/get-started/liquid/platform-specific')
     expect($('h2#in-this-article').length).toBe(1)
     expect($('h2#in-this-article + nav ul .ghd-tool.mac').length).toBe(1)
     expect($('h2#in-this-article + nav ul .ghd-tool.windows').length).toBe(1)
@@ -68,7 +68,7 @@ describe('tool', () => {
 
 describe('post', () => {
   test('whitespace control', async () => {
-    const $: cheerio.Root = await getDOM('/get-started/liquid/whitespace')
+    const $: CheerioAPI = await getDOM('/get-started/liquid/whitespace')
     const html = $('#article-contents').html()
     expect(html).toMatch('

HubGit

') expect(html).toMatch('

Text before. HubGit Text after.

') @@ -78,7 +78,7 @@ describe('post', () => { // Test what happens to `Cram{% ifversion fpt %}FPT{% endif %}ped.` // when it's not free-pro-team. { - const $inner: cheerio.Root = await getDOM( + const $inner: CheerioAPI = await getDOM( '/enterprise-server@latest/get-started/liquid/whitespace', ) const innerHtml = $inner('#article-contents').html() @@ -91,7 +91,7 @@ describe('post', () => { describe('rowheaders', () => { test('rowheaders', async () => { - const $: cheerio.Root = await getDOM('/get-started/liquid/table-row-headers') + const $: CheerioAPI = await getDOM('/get-started/liquid/table-row-headers') const tables = $('#article-contents table') expect(tables.length).toBe(2) @@ -188,7 +188,7 @@ describe('ifversion', () => { test.each(Object.keys(matchesPerVersion))( 'ifversion using rendered version %p', async (version: string) => { - const $: cheerio.Root = await getDOM(`/${version}/get-started/liquid/ifversion`) + const $: CheerioAPI = await getDOM(`/${version}/get-started/liquid/ifversion`) const html = $('#article-contents').html() const allConditions = Object.values(matchesPerVersion).flat() @@ -215,7 +215,7 @@ describe('ifversion', () => { describe('misc Liquid', () => { test('links with liquid from data', async () => { - const $: cheerio.Root = await getDOM('/get-started/liquid/links-with-liquid') + const $: CheerioAPI = await getDOM('/get-started/liquid/links-with-liquid') // The URL comes from variables.product.pricing_url const url = getDataByLanguage('variables.product.pricing_url', 'en') if (!url) throw new Error('variable could not be found') @@ -235,7 +235,7 @@ describe('misc Liquid', () => { // Markdown directly follows a tool tag like `{% linux %}...{% endlinux %}`. // The next line immediately after the `{% endlinux %}` should not // leave the Markdown unrendered - const $: cheerio.Root = await getDOM('/get-started/liquid/tool-platform-switcher') + const $: CheerioAPI = await getDOM('/get-started/liquid/tool-platform-switcher') const innerHTML = $('#article-contents').html() expect(innerHTML).not.toMatch('On *this* line is `Markdown` too.') expect(innerHTML).toMatch('

On this line is Markdown too.

') @@ -244,7 +244,7 @@ describe('misc Liquid', () => { describe('data tag', () => { test('injects data reusables with the right whitespace', async () => { - const $: cheerio.Root = await getDOM('/get-started/liquid/data') + const $: CheerioAPI = await getDOM('/get-started/liquid/data') // This proves that the two injected reusables tables work. // CommonMark is finicky if the indentation isn't perfect, so diff --git a/src/fixtures/tests/markdown.ts b/src/fixtures/tests/markdown.ts index 36b604106c38..a438412b5ffa 100644 --- a/src/fixtures/tests/markdown.ts +++ b/src/fixtures/tests/markdown.ts @@ -1,11 +1,11 @@ import { describe, expect, test } from 'vitest' -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' import { getDOM } from '@/tests/helpers/e2etest' describe('markdown rendering', () => { test('markdown in intro', async () => { - const $: cheerio.Root = await getDOM('/get-started/markdown/intro') + const $: CheerioAPI = await getDOM('/get-started/markdown/intro') const html = $('[data-testid="lead"]').html() expect(html).toMatch('Markdown') expect(html).toMatch('syntax') @@ -15,7 +15,7 @@ describe('markdown rendering', () => { describe('alerts', () => { test('basic rendering', async () => { - const $: cheerio.Root = await getDOM('/get-started/markdown/alerts') + const $: CheerioAPI = await getDOM('/get-started/markdown/alerts') const alerts = $('#article-contents .ghd-alert') // See src/fixtures/fixtures/content/get-started/markdown/alerts.md // to be this confident in the assertions. diff --git a/src/fixtures/tests/minitoc.ts b/src/fixtures/tests/minitoc.ts index 9d2475853acd..8ca670e7a320 100644 --- a/src/fixtures/tests/minitoc.ts +++ b/src/fixtures/tests/minitoc.ts @@ -1,37 +1,37 @@ import { describe, expect, test } from 'vitest' -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' import { getDOM } from '@/tests/helpers/e2etest' describe('minitoc', () => { test('renders mini TOC in articles with more than one heading', async () => { - const $: cheerio.Root = await getDOM('/en/get-started/minitocs/multiple-headings') + const $: CheerioAPI = await getDOM('/en/get-started/minitocs/multiple-headings') expect($('h2#in-this-article').length).toBe(1) expect($('h2#in-this-article + nav ul li').length).toBeGreaterThan(1) }) test('does not render mini TOC in articles with only one heading', async () => { - const $: cheerio.Root = await getDOM('/en/get-started/minitocs/one-heading') + const $: CheerioAPI = await getDOM('/en/get-started/minitocs/one-heading') expect($('h2#in-this-article').length).toBe(0) }) test('does not render mini TOC in articles with no headings', async () => { - const $: cheerio.Root = await getDOM('/en/get-started/minitocs/no-heading') + const $: CheerioAPI = await getDOM('/en/get-started/minitocs/no-heading') expect($('h2#in-this-article').length).toBe(0) }) test('does not render mini TOC in non-articles', async () => { - const $: cheerio.Root = await getDOM('/en/get-started') + const $: CheerioAPI = await getDOM('/en/get-started') expect($('h2#in-this-article').length).toBe(0) }) test('renders mini TOC with correct links when headings contain markup', async () => { - const $: cheerio.Root = await getDOM('/en/get-started/minitocs/markup-heading') + const $: CheerioAPI = await getDOM('/en/get-started/minitocs/markup-heading') expect($('h2#in-this-article + nav ul li a[href="#on"]').length).toBe(1) }) test("max heading doesn't exceed h2", async () => { - const $: cheerio.Root = await getDOM('/en/get-started/minitocs/multiple-headings') + const $: CheerioAPI = await getDOM('/en/get-started/minitocs/multiple-headings') expect($('h2#in-this-article').length).toBe(1) expect($('h2#in-this-article + nav ul').length).toBeGreaterThan(0) // non-indented items expect($('h2#in-this-article + nav ul div ul div').length).toBe(0) // indented items diff --git a/src/fixtures/tests/page-titles.ts b/src/fixtures/tests/page-titles.ts index 2bfe53bf8cc5..3d4492ec021f 100644 --- a/src/fixtures/tests/page-titles.ts +++ b/src/fixtures/tests/page-titles.ts @@ -1,22 +1,22 @@ import { describe, expect, test } from 'vitest' -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' import enterpriseServerReleases from '@/versions/lib/enterprise-server-releases' import { getDOM } from '@/tests/helpers/e2etest' describe('page titles', () => { test('homepage', async () => { - const $: cheerio.Root = await getDOM('/en') + const $: CheerioAPI = await getDOM('/en') expect($('title').text()).toBe('GitHub Docs') }) test('fpt article', async () => { - const $: cheerio.Root = await getDOM('/get-started/start-your-journey/hello-world') + const $: CheerioAPI = await getDOM('/get-started/start-your-journey/hello-world') expect($('title').text()).toBe('Hello World - GitHub Docs') }) test('ghes article', async () => { - const $: cheerio.Root = await getDOM( + const $: CheerioAPI = await getDOM( `/enterprise-server@latest/get-started/start-your-journey/hello-world`, ) expect($('title').text()).toBe( @@ -25,12 +25,12 @@ describe('page titles', () => { }) test('fpt subcategory page', async () => { - const $: cheerio.Root = await getDOM('/en/get-started/start-your-journey') + const $: CheerioAPI = await getDOM('/en/get-started/start-your-journey') expect($('title').text()).toBe('Start your journey - GitHub Docs') }) test('fpt category page', async () => { - const $: cheerio.Root = await getDOM('/actions/category') + const $: CheerioAPI = await getDOM('/actions/category') expect($('title').text()).toBe('Category page of GitHub Actions - GitHub Docs') }) }) diff --git a/src/fixtures/tests/permissions-callout.ts b/src/fixtures/tests/permissions-callout.ts index 6c194d9a28ae..93cc31cd9d87 100644 --- a/src/fixtures/tests/permissions-callout.ts +++ b/src/fixtures/tests/permissions-callout.ts @@ -1,11 +1,11 @@ import { describe, expect, test } from 'vitest' -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' import { getDOM } from '@/tests/helpers/e2etest' describe('permission statements', () => { test('article page product statement', async () => { - const $: cheerio.Root = await getDOM('/get-started/foo/page-with-callout') + const $: CheerioAPI = await getDOM('/get-started/foo/page-with-callout') const callout = $('[data-testid=product-statement] div') expect(callout.html()).toBe('

Callout for HubGit Pages

') }) @@ -16,7 +16,7 @@ describe('permission statements', () => { // an empty string. // This test tests that alert is not rendered if its output // "exits" but is empty. - const $: cheerio.Root = await getDOM( + const $: CheerioAPI = await getDOM( '/enterprise-server@latest/get-started/foo/page-with-callout', ) const callout = $('[data-testid=product-statement]') @@ -24,13 +24,13 @@ describe('permission statements', () => { }) test('toc landing page', async () => { - const $: cheerio.Root = await getDOM('/actions/category') + const $: CheerioAPI = await getDOM('/actions/category') const callout = $('[data-testid=product-statement] div') expect(callout.html()).toBe('

This is the callout text

') }) test('page with permission frontmatter', async () => { - const $: cheerio.Root = await getDOM('/get-started/markdown/permissions') + const $: CheerioAPI = await getDOM('/get-started/markdown/permissions') const html = $('[data-testid=permissions-statement] div').html() // Markdown expect(html).toMatch('admin') @@ -39,9 +39,7 @@ describe('permission statements', () => { }) test('page with permission frontmatter and product statement', async () => { - const $: cheerio.Root = await getDOM( - '/get-started/foo/page-with-permissions-and-product-callout', - ) + const $: CheerioAPI = await getDOM('/get-started/foo/page-with-permissions-and-product-callout') const html = $('[data-testid=permissions-callout] div').html() // part of the UI expect(html).toMatch('Who can use this feature') diff --git a/src/fixtures/tests/sidebar.ts b/src/fixtures/tests/sidebar.ts index 68f746c4f504..568b5bb6a369 100644 --- a/src/fixtures/tests/sidebar.ts +++ b/src/fixtures/tests/sidebar.ts @@ -1,11 +1,11 @@ import { describe, expect, test } from 'vitest' -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' import { getDOMCached as getDOM } from '@/tests/helpers/e2etest' describe('sidebar', () => { test('top level product mentioned at top of sidebar', async () => { - const $: cheerio.Root = await getDOM('/get-started') + const $: CheerioAPI = await getDOM('/get-started') // Desktop const sidebarProduct = $('[data-testid="sidebar-product-xl"]') expect(sidebarProduct.text()).toBe('Get started') @@ -16,12 +16,12 @@ describe('sidebar', () => { }) test('REST pages get the REST sidebar', async () => { - const $: cheerio.Root = await getDOM('/rest') + const $: CheerioAPI = await getDOM('/rest') expect($('[data-testid=rest-sidebar-reference]').length).toBe(1) }) test('leaf-node article marked as aria-current=page', async () => { - const $: cheerio.Root = await getDOM('/get-started/start-your-journey/hello-world') + const $: CheerioAPI = await getDOM('/get-started/start-your-journey/hello-world') expect( $( '[data-testid=sidebar] [data-testid=product-sidebar] a[aria-current="page"] span span', @@ -30,7 +30,7 @@ describe('sidebar', () => { }) test('sidebar should always use the shortTitle', async () => { - const $: cheerio.Root = await getDOM('/get-started/foo/bar') + const $: CheerioAPI = await getDOM('/get-started/foo/bar') // The page /get-started/foo/bar has a short title that is different // from its regular title. expect( @@ -41,7 +41,7 @@ describe('sidebar', () => { }) test('short titles with Liquid and HTML characters', async () => { - const $: cheerio.Root = await getDOM('/get-started/foo/html-short-title') + const $: CheerioAPI = await getDOM('/get-started/foo/html-short-title') const link = $( '[data-testid=sidebar] [data-testid=product-sidebar] a[href*="/get-started/foo/html-short-title"]', ) @@ -51,26 +51,26 @@ describe('sidebar', () => { test('Liquid is rendered in short title used at top of sidebar', async () => { // Free, pro, team { - const $: cheerio.Root = await getDOM('/pages') + const $: CheerioAPI = await getDOM('/pages') const link = $('#allproducts-menu a') expect(link.text()).toBe('Pages (HubGit)') } // Enterprise Server { - const $: cheerio.Root = await getDOM('/enterprise-server@latest/pages') + const $: CheerioAPI = await getDOM('/enterprise-server@latest/pages') const link = $('#allproducts-menu a') expect(link.text()).toBe('Pages (HubGit Enterprise Server)') } // Enterprise Cloud { - const $: cheerio.Root = await getDOM('/enterprise-cloud@latest/pages') + const $: CheerioAPI = await getDOM('/enterprise-cloud@latest/pages') const link = $('#allproducts-menu a') expect(link.text()).toBe('Pages (HubGit Enterprise Cloud)') } }) test('no docset link for early-access', async () => { - const $: cheerio.Root = await getDOM('/early-access/secrets/deeper/mariana-trench') + const $: CheerioAPI = await getDOM('/early-access/secrets/deeper/mariana-trench') // Deskop expect($('[data-testid="sidebar-product-xl"]').length).toBe(0) // Mobile diff --git a/src/fixtures/tests/translations.ts b/src/fixtures/tests/translations.ts index 2fc631089f59..0682eecfcd71 100644 --- a/src/fixtures/tests/translations.ts +++ b/src/fixtures/tests/translations.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' import { TRANSLATIONS_FIXTURE_ROOT } from '@/frame/lib/constants' import { getDOM, head } from '@/tests/helpers/e2etest' @@ -12,7 +12,7 @@ if (!TRANSLATIONS_FIXTURE_ROOT) { describe('translations', () => { test('home page', async () => { - const $: cheerio.Root = await getDOM('/ja') + const $: CheerioAPI = await getDOM('/ja') const h1 = $('h1').text() // You gotta know your src/fixtures/fixtures/translations/ja-jp/data/ui.yml expect(h1).toBe('日本 GitHub Docs') @@ -32,13 +32,13 @@ describe('translations', () => { }) test('hello world', async () => { - const $: cheerio.Root = await getDOM('/ja/get-started/start-your-journey/hello-world') + const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/hello-world') const h1 = $('h1').text() expect(h1).toBe('こんにちは World') }) test('internal links get prefixed with /ja', async () => { - const $: cheerio.Root = await getDOM('/ja/get-started/start-your-journey/link-rewriting') + const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/link-rewriting') const links = $('#article-contents a[href]') const jaLinks = links.filter((i: number, element: any) => { const href = $(element).attr('href') @@ -53,7 +53,7 @@ describe('translations', () => { }) test('internal links with AUTOTITLE resolves', async () => { - const $: cheerio.Root = await getDOM('/ja/get-started/foo/autotitling') + const $: CheerioAPI = await getDOM('/ja/get-started/foo/autotitling') const links = $('#article-contents a[href]') links.each((i: number, element: any) => { if ($(element).attr('href')?.includes('/ja/get-started/start-your-journey/hello-world')) { @@ -67,7 +67,7 @@ describe('translations', () => { test('correction of linebreaks in translations', async () => { // free-pro-team { - const $: cheerio.Root = await getDOM('/ja/get-started/foo/table-with-ifversions') + const $: CheerioAPI = await getDOM('/ja/get-started/foo/table-with-ifversions') const paragraph = $('#article-contents p').text() expect(paragraph).toMatch('mention of HubGit in Liquid') @@ -80,7 +80,7 @@ describe('translations', () => { } // enterprise-server { - const $: cheerio.Root = await getDOM( + const $: CheerioAPI = await getDOM( '/ja/enterprise-server@latest/get-started/foo/table-with-ifversions', ) @@ -96,7 +96,7 @@ describe('translations', () => { }) test('automatic correction of bad AUTOTITLE in reusables', async () => { - const $: cheerio.Root = await getDOM('/ja/get-started/start-your-journey/hello-world') + const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/hello-world') const links = $('#article-contents a[href]') const texts = links.map((i: number, element: any) => $(element).text()).get() // That Japanese page uses AUTOTITLE links. Both in the main `.md` file @@ -126,7 +126,7 @@ describe('translations', () => { // which needs to become: // // [Bar](バー) - const $: cheerio.Root = await getDOM('/ja/get-started/start-your-journey/hello-world') + const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/hello-world') const links = $('#article-contents a[href]') const texts = links .filter((i: number, element: any) => { diff --git a/src/fixtures/tests/versioning.ts b/src/fixtures/tests/versioning.ts index 2665fcc2e682..5fe70db14127 100644 --- a/src/fixtures/tests/versioning.ts +++ b/src/fixtures/tests/versioning.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from 'vitest' -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' import { getDOM, head } from '@/tests/helpers/e2etest' import { supported } from '@/versions/lib/enterprise-server-releases' describe('article versioning', () => { test('only links to articles for fpt', async () => { - const $: cheerio.Root = await getDOM('/get-started/versioning') + const $: CheerioAPI = await getDOM('/get-started/versioning') const links = $('[data-testid="table-of-contents"] a') // Only 1 link because there's only 1 article available in fpt expect(links.length).toBe(1) @@ -14,7 +14,7 @@ describe('article versioning', () => { }) test('only links to articles for ghec', async () => { - const $: cheerio.Root = await getDOM('/enterprise-cloud@latest/get-started/versioning') + const $: CheerioAPI = await getDOM('/enterprise-cloud@latest/get-started/versioning') const links = $('[data-testid="table-of-contents"] a') expect(links.length).toBe(2) const first = links.filter((i: number) => i === 0) diff --git a/src/fixtures/tests/video-transcripts.ts b/src/fixtures/tests/video-transcripts.ts index 812e5fbc0c3d..36a4f6adca1c 100644 --- a/src/fixtures/tests/video-transcripts.ts +++ b/src/fixtures/tests/video-transcripts.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from 'vitest' -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' import { getDOM } from '@/tests/helpers/e2etest' describe('transcripts', () => { describe('product landing page', () => { test('video link from product landing page leads to video', async () => { - const $: cheerio.Root = await getDOM('/en/get-started') + const $: CheerioAPI = await getDOM('/en/get-started') expect($('a#product-video').attr('href')).toBe( '/en/video-transcripts/transcript--my-awesome-video', ) @@ -15,7 +15,7 @@ describe('transcripts', () => { describe('transcript page', () => { test('video link from transcript leads to video', async () => { - const $: cheerio.Root = await getDOM( + const $: CheerioAPI = await getDOM( '/en/get-started/video-transcripts/transcript--my-awesome-video', ) expect($('a#product-video').attr('href')).toBe('https://www.yourube.com/abc123') diff --git a/src/frame/lib/get-mini-toc-items.ts b/src/frame/lib/get-mini-toc-items.ts index a701e6bb9e98..9a370b71d98e 100644 --- a/src/frame/lib/get-mini-toc-items.ts +++ b/src/frame/lib/get-mini-toc-items.ts @@ -1,4 +1,5 @@ -import cheerio from 'cheerio' +import { load } from 'cheerio' +import type { Element } from 'domhandler' import { range } from 'lodash-es' import { renderContent } from '@/content-render/index' @@ -29,7 +30,7 @@ export default function getMiniTocItems( maxHeadingLevel = 2, headingScope = '', ): MiniTocItem[] { - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) // eg `h2, h3` or `h2, h3, h4` depending on maxHeadingLevel const selector = range(2, maxHeadingLevel + 1) @@ -48,9 +49,9 @@ export default function getMiniTocItems( const flatToc = headings .get() .filter((item) => { - if (!item.parent || !item.parent.attribs) return true - // Hide any items that belong to a hidden div - const { attribs } = item.parent + const parent = item.parent as Element | null + if (!parent || !parent.attribs) return true + const { attribs } = parent return !('hidden' in attribs) }) .map((item) => { @@ -73,7 +74,7 @@ export default function getMiniTocItems( $('strong', item).map((i, el) => $(el).replaceWith($(el).contents())) const contents: MiniTocContents = { href, title: $(item).text().trim() } - const element = $(item)[0] as cheerio.TagElement + const element = $(item)[0] as Element const headingLevel = parseInt(element.name.match(/\d+/)![0], 10) || 0 // the `2` from `h2` const platform = $(item).parent('.ghd-tool').attr('class') || '' diff --git a/src/frame/lib/page.ts b/src/frame/lib/page.ts index 5f3c619cbd8c..bd0079211673 100644 --- a/src/frame/lib/page.ts +++ b/src/frame/lib/page.ts @@ -1,7 +1,7 @@ import assert from 'assert' import path from 'path' import fs from 'fs/promises' -import cheerio from 'cheerio' +import { load } from 'cheerio' import getApplicableVersions from '@/versions/lib/get-applicable-versions' import generateRedirectsForPermalinks from '@/redirects/lib/permalinks' import getEnglishHeadings from '@/languages/lib/get-english-headings' @@ -440,7 +440,7 @@ class Page { if (!opts.unwrap) return html // The unwrap option removes surrounding tags from a string, preserving any inner HTML - const $ = cheerio.load(html, { xmlMode: true }) + const $ = load(html, { xmlMode: true }) return $.root().contents().html() || '' } diff --git a/src/frame/tests/page.ts b/src/frame/tests/page.ts index 266babf20100..591c0d8fe247 100644 --- a/src/frame/tests/page.ts +++ b/src/frame/tests/page.ts @@ -1,7 +1,7 @@ import { fileURLToPath } from 'url' import path from 'path' -import cheerio from 'cheerio' +import { load } from 'cheerio' import { beforeAll, beforeEach, describe, expect, test } from 'vitest' import Page, { FrontmatterErrorsError } from '@/frame/lib/page' @@ -97,7 +97,7 @@ describe('Page class', () => { } context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}` let rendered = await page!.render(context) - let $ = cheerio.load(rendered) + let $ = load(rendered) expect(($ as any).text()).toBe( 'This text should render on any actively supported version of Enterprise Server', ) @@ -108,7 +108,7 @@ describe('Page class', () => { context.currentVersion = `enterprise-server@${enterpriseServerReleases.oldestSupported}` context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}` rendered = await page!.render(context) - $ = cheerio.load(rendered) + $ = load(rendered) expect(($ as any).text()).toBe( 'This text should render on any actively supported version of Enterprise Server', ) @@ -119,7 +119,7 @@ describe('Page class', () => { context.currentVersion = nonEnterpriseDefaultVersion context.currentPath = `/${context.currentLanguage}/${context.currentVersion}/${page!.relativePath}` rendered = await page!.render(context) - $ = cheerio.load(rendered) + $ = load(rendered) expect(($ as any).text()).not.toBe( 'This text should render on any actively supported version of Enterprise Server', ) diff --git a/src/graphql/pages/breaking-changes.tsx b/src/graphql/pages/breaking-changes.tsx index 7e8734616df2..1224d0e54e75 100644 --- a/src/graphql/pages/breaking-changes.tsx +++ b/src/graphql/pages/breaking-changes.tsx @@ -1,5 +1,7 @@ import { GetServerSideProps } from 'next' import GithubSlugger from 'github-slugger' +import type { ExtendedRequest } from '@/types' +import type { ServerResponse } from 'http' import { MainContextT, MainContext, getMainContext } from '@/frame/components/context/MainContext' import { AutomatedPage } from '@/automated-pipelines/components/AutomatedPage' @@ -40,8 +42,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => const { getGraphqlBreakingChanges } = await import('@/graphql/lib/index') const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items') - const req = context.req as any - const res = context.res as any + const req = context.req as unknown as ExtendedRequest + const res = context.res as unknown as ServerResponse const currentVersion = context.query.versionId as string const schema = getGraphqlBreakingChanges(currentVersion) if (!schema) throw new Error(`No graphql breaking changes schema found for ${currentVersion}`) @@ -65,7 +67,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => }), ) const titles = Object.values(headings).map((heading) => heading.title) - const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context.context, 2) + const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context!, 2) // Update the existing context to include the miniTocItems from GraphQL automatedPageContext.miniTocItems.push(...changelogMiniTocItems) diff --git a/src/graphql/pages/changelog.tsx b/src/graphql/pages/changelog.tsx index b39eb0bd1d36..8dd1ce67fa7d 100644 --- a/src/graphql/pages/changelog.tsx +++ b/src/graphql/pages/changelog.tsx @@ -1,4 +1,6 @@ import { GetServerSideProps } from 'next' +import type { ExtendedRequest } from '@/types' +import type { ServerResponse } from 'http' import { MainContextT, MainContext, getMainContext } from '@/frame/components/context/MainContext' import { AutomatedPage } from '@/automated-pipelines/components/AutomatedPage' @@ -31,8 +33,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => const { getGraphqlChangelog } = await import('@/graphql/lib/index') const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items') - const req = context.req as any - const res = context.res as any + const req = context.req as unknown as ExtendedRequest + const res = context.res as unknown as ServerResponse const currentVersion = context.query.versionId as string const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[] if (!schema) throw new Error('No graphql free-pro-team changelog schema found.') @@ -41,7 +43,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => // content/graphql/reference/* const automatedPageContext = getAutomatedPageContextFromRequest(req) const titles = schema.map((item) => `Schema changes for ${item.date}`) - const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context.context, 2) + const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context!, 2) // Update the existing context to include the miniTocItems from GraphQL automatedPageContext.miniTocItems.push(...changelogMiniTocItems) diff --git a/src/graphql/pages/schema-previews.tsx b/src/graphql/pages/schema-previews.tsx index e9799f58908d..5a1ac1beb260 100644 --- a/src/graphql/pages/schema-previews.tsx +++ b/src/graphql/pages/schema-previews.tsx @@ -1,4 +1,6 @@ import { GetServerSideProps } from 'next' +import type { ExtendedRequest } from '@/types' +import type { ServerResponse } from 'http' import { MainContextT, @@ -36,8 +38,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => const { getPreviews } = await import('@/graphql/lib/index') const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items') - const req = context.req as any - const res = context.res as any + const req = context.req as unknown as ExtendedRequest + const res = context.res as unknown as ServerResponse const currentVersion = context.query.versionId as string const schema = getPreviews(currentVersion) as PreviewT[] if (!schema) throw new Error(`No graphql preview schema found for ${currentVersion}`) @@ -47,7 +49,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => // content/graphql/reference/* const automatedPageContext = getAutomatedPageContextFromRequest(req) const titles = schema.map((item) => item.title) - const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context.context, 2) + const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context!, 2) // Update the existing context to include the miniTocItems from GraphQL automatedPageContext.miniTocItems.push(...changelogMiniTocItems) diff --git a/src/landings/tests/curated-homepage-links.ts b/src/landings/tests/curated-homepage-links.ts index fa94464a1792..c469743bcd3f 100644 --- a/src/landings/tests/curated-homepage-links.ts +++ b/src/landings/tests/curated-homepage-links.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from 'vitest' -import cheerio from 'cheerio' +import type { Element } from 'domhandler' import { getDOM } from '@/tests/helpers/e2etest' @@ -14,7 +14,7 @@ describe('curated homepage links', () => { expect($links.length).toBeGreaterThanOrEqual(6) // Check that each link is localized and includes a title and intro - $links.each((i: number, el: cheerio.Element) => { + $links.each((i: number, el: Element) => { const linkUrl = $(el).attr('href') as string expect(linkUrl.startsWith('/en/')).toBe(true) diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index db740df79bf1..1e8fb91a806c 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -27,6 +27,7 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% datos variables', '{% data variables') content = content.replaceAll('{% de datos variables', '{% data variables') content = content.replaceAll('{% datos reusables', '{% data reusables') + content = content.replaceAll('{% data reutilizables.', '{% data reusables.') content = content.replaceAll('{%- ifversion fpt o ghec %}', '{%- ifversion fpt or ghec %}') content = content.replaceAll('{% ifversion fpt o ghec %}', '{% ifversion fpt or ghec %}') } @@ -69,9 +70,16 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% данные variables.', '{% data variables.') content = content.replaceAll('{% данных reusables', '{% data reusables') content = content.replaceAll('{% данные reusables', '{% data reusables') + content = content.replaceAll('{% данных переменных.', '{% data variables.') + content = content.replaceAll('{% данных.product.', '{% data variables.product.') + content = content.replaceAll('{% data переменных.product.', '{% data variables.product.') + content = content.replaceAll('{% переменным данных.product.', '{% data variables.product.') content = content.replaceAll('{% необработанного %}', '{% raw %}') content = content.replaceAll('{%- ifversion fpt или ghec %}', '{%- ifversion fpt or ghec %}') content = content.replaceAll('{% ifversion fpt или ghec %}', '{% ifversion fpt or ghec %}') + content = content.replaceAll('{% ifversion ghec или fpt %}', '{% ifversion ghec or fpt %}') + content = content.replaceAll('{% ghes или ghec %}', '{% ifversion ghes or ghec %}') + content = content.replaceAll('{% elsif ghec или ghes %}', '{% elsif ghec or ghes %}') content = content.replaceAll('{% endif _%}', '{% endif %}') content = content.replaceAll('{% конечным %}', '{% endif %}') content = content.replaceAll('{% конец %}', '{% endif %}') @@ -81,6 +89,7 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% конечных головщиков %}', '{% endrowheaders %}') content = content.replaceAll('{% данных для повторного использования.', '{% data reusables.') content = content.replaceAll('{% еще %}', '{% else %}') + content = content.replaceAll('{% ещё %}', '{% else %}') content = content.replaceAll('{% необработанные %}', '{% raw %}') // Fix double quotes in Russian YAML files that cause parsing errors @@ -105,6 +114,7 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% données variables', '{% data variables') content = content.replaceAll('{% données réutilisables.', '{% data reusables.') content = content.replaceAll('{% variables de données.', '{% data variables.') + content = content.replaceAll('{% autre %}', '{% else %}') content = content.replaceAll('{%- ifversion fpt ou ghec %}', '{%- ifversion fpt or ghec %}') content = content.replaceAll('{% ifversion fpt ou ghec %}', '{% ifversion fpt or ghec %}') } @@ -125,6 +135,7 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% Daten variables', '{% data variables') content = content.replaceAll('{% daten variables', '{% data variables') content = content.replaceAll('{%-Daten variables', '{%- data variables') + content = content.replaceAll('{%-Daten-variables', '{%- data variables') content = content.replaceAll('{%- ifversion fpt oder ghec %}', '{%- ifversion fpt or ghec %}') content = content.replaceAll('{% ifversion fpt oder ghec %}', '{% ifversion fpt or ghec %}') } diff --git a/src/languages/lib/get-alert-titles.ts b/src/languages/lib/get-alert-titles.ts index d73e33281c78..117c646adfc0 100644 --- a/src/languages/lib/get-alert-titles.ts +++ b/src/languages/lib/get-alert-titles.ts @@ -3,19 +3,28 @@ import path from 'path' import yaml from 'js-yaml' import languages from './languages-server' -const cache: Record = {} +interface AlertTitles { + [key: string]: string +} + +interface UiYaml { + alerts?: AlertTitles + [key: string]: unknown +} + +const cache: Record = {} -export async function getAlertTitles(page: Record) { +export async function getAlertTitles(page: { languageCode: string }) { const { languageCode } = page if (cache[languageCode]) return cache[languageCode] let file = '' - let yamlFile: Record = {} + let yamlFile: UiYaml = {} if (languageCode !== 'en') { try { const { dir } = languages[languageCode] file = await fs.readFile(path.join(dir, `data/ui.yml`), 'utf-8') - yamlFile = yaml.load(file) as Record + yamlFile = yaml.load(file) as UiYaml } catch (e) { console.warn(`Failed to load translated alert titles`, e) } @@ -23,9 +32,9 @@ export async function getAlertTitles(page: Record) { if (!file || !yamlFile.alerts) { const { dir } = languages.en file = await fs.readFile(path.join(dir, `data/ui.yml`), 'utf-8') - yamlFile = yaml.load(file) as Record + yamlFile = yaml.load(file) as UiYaml } - cache[languageCode] = yamlFile.alerts + cache[languageCode] = yamlFile.alerts ?? {} return cache[languageCode] } diff --git a/src/languages/tests/frame.ts b/src/languages/tests/frame.ts index fca75c4a18e7..669a12f19a49 100644 --- a/src/languages/tests/frame.ts +++ b/src/languages/tests/frame.ts @@ -2,6 +2,8 @@ import { describe, expect, test, vi } from 'vitest' import { languageKeys } from '@/languages/lib/languages-server' import { blockIndex } from '@/frame/middleware/block-robots' +import type { Element } from 'domhandler' + import { get, getDOMCached as getDOM } from '@/tests/helpers/e2etest' import Page from '@/frame/lib/page' @@ -17,13 +19,13 @@ describe('frame', () => { test.each(langs)('breadcrumbs link to %s pages', async (lang) => { const $ = await getDOM(`/${lang}/get-started/learning-about-github`) const $breadcrumbs = $('[data-testid=breadcrumbs-in-article] a') - expect(($breadcrumbs[0] as cheerio.TagElement).attribs.href).toBe(`/${lang}/get-started`) + expect(($breadcrumbs[0] as Element).attribs.href).toBe(`/${lang}/get-started`) }) test.each(langs)('homepage links go to %s pages', async (lang) => { const $ = await getDOM(`/${lang}`) const $links = $('[data-testid=bump-link]') - $links.each((i: number, el: cheerio.Element) => { + $links.each((i: number, el: Element) => { const linkUrl = $(el).attr('href') expect((linkUrl || '').startsWith(`/${lang}/`)).toBe(true) }) diff --git a/src/links/lib/excluded-links.yml b/src/links/lib/excluded-links.yml index 42e1267592fb..f606031cd003 100644 --- a/src/links/lib/excluded-links.yml +++ b/src/links/lib/excluded-links.yml @@ -70,8 +70,8 @@ - is: https://moodle.org - is: https://azure.microsoft.com - is: https://api.octocorp.ghe.com -- is: https://platform.openai.com/docs/guides/safety-best-practices -- is: https://platform.openai.com/docs/guides/function-calling +- startsWith: https://platform.openai.com/docs +- startsWith: https://openai.com - is: https://global.rel.tunnels.api.visualstudio.com/api/version - is: https://www.wireguard.com/quickstart/ - is: https://docs.openstack.org/horizon/latest/ @@ -85,8 +85,6 @@ - is: https://jsonformatter.org/ - is: https://mvnrepository.com/artifact/org.xwiki.platform/xwiki-platform-oldcore - is: https://mvnrepository.com/artifact/com.google.guava/guava -- startsWith: https://platform.openai.com/docs/models -- startsWith: https://openai.com/index - is: https://github.com/github-linguist/linguist/compare/master...octocat:master - is: https://www.servicenow.com/docs/bundle/utah-devops/page/product/enterprise-dev-ops/concept/github-integration-dev-ops.html - startsWith: https://www.ilo.org @@ -95,8 +93,7 @@ - is: https://www.nongnu.org/oath-toolkit/man-oathtool.html - is: https://www.gnu.org/software/emacs/ - is: https://www.transparency.org/what-is-corruption -- startsWith: https://platform.openai.com/docs/api-reference/ -- is: https://azuredownloads-g3ahgwb5b8bkbxhd.b01.azurefd.net/github-copilot/ +- startsWith: https://azuredownloads-g3ahgwb5b8bkbxhd.b01.azurefd.net/github-copilot/ - is: https://www.anthropic.com/claude/sonnet - is: https://www.psiexams.com/become-psi-test-center/computer-specifications/ - is: https://www.buymeacoffee.com/ @@ -115,3 +112,21 @@ - is: https://collectd.org/documentation/manpages/collectd.conf.html#plugin-fhcount - is: https://mywiki.wooledge.org/BashPitfalls - startsWith: https://code.visualstudio.com/docs/configure/telemetry + +# npmjs.com blocks automated link checkers with 403. +- startsWith: https://www.npmjs.com + +# Azure Marketplace blocks automated link checkers with 403. +- startsWith: https://azuremarketplace.microsoft.com + +# Splunk docs blocks automated link checkers with 403. +- startsWith: https://docs.splunk.com + +# HashiCorp rate-limits automated requests (429). +- startsWith: https://www.hashicorp.com + +# ISO website blocks automated link checkers with 403. +- startsWith: https://www.iso.org + +# Example domain used in style guide documentation. +- startsWith: https://some-docs.com diff --git a/src/links/lib/extract-links.ts b/src/links/lib/extract-links.ts index c2e973736cea..d6a5c79f32a1 100644 --- a/src/links/lib/extract-links.ts +++ b/src/links/lib/extract-links.ts @@ -16,7 +16,8 @@ import type { Context, Page } from '@/types' // Link patterns for Markdown const INTERNAL_LINK_PATTERN = /\]\(\/[^)]+\)/g const AUTOTITLE_LINK_PATTERN = /\[AUTOTITLE\]\(([^)]+)\)/g -const EXTERNAL_LINK_PATTERN = /\]\((https?:\/\/[^)]+)\)/g +// Handles one level of balanced parentheses in URLs (e.g., Wikipedia links) +const EXTERNAL_LINK_PATTERN = /\]\((https?:\/\/(?:[^()\s]+|\([^()]*\))*)\)/g const IMAGE_LINK_PATTERN = /!\[[^\]]*\]\(([^)]+)\)/g // Anchor link patterns (for same-page links) @@ -82,10 +83,19 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult const anchorLinks: ExtractedLink[] = [] const imageLinks: ExtractedLink[] = [] + // Strip fenced code blocks to avoid checking example/placeholder URLs + // Replaces non-newline characters with spaces to preserve line numbers and positions + const strippedContent = content.replace( + /^ {0,3}(`{3,})[^\n]*\n[\s\S]*?^ {0,3}\1\s*$/gm, + (match) => { + return match.replace(/[^\n]/g, ' ') + }, + ) + // Extract AUTOTITLE links first (they're a special case of internal links) let match - while ((match = AUTOTITLE_LINK_PATTERN.exec(content)) !== null) { - const { line, column } = getLineAndColumn(content, match.index) + while ((match = AUTOTITLE_LINK_PATTERN.exec(strippedContent)) !== null) { + const { line, column } = getLineAndColumn(strippedContent, match.index) const href = match[1].split('#')[0] // Remove anchor if present if (href.startsWith('/')) { internalLinks.push({ @@ -102,17 +112,17 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult AUTOTITLE_LINK_PATTERN.lastIndex = 0 // Extract regular internal links - while ((match = INTERNAL_LINK_PATTERN.exec(content)) !== null) { + while ((match = INTERNAL_LINK_PATTERN.exec(strippedContent)) !== null) { // Skip if this is an AUTOTITLE link (already captured) const fullMatch = match[0] - if (content.substring(match.index - 10, match.index).includes('AUTOTITLE')) { + if (strippedContent.substring(match.index - 10, match.index).includes('AUTOTITLE')) { continue } - const { line, column } = getLineAndColumn(content, match.index) + const { line, column } = getLineAndColumn(strippedContent, match.index) // Extract href from ](/path) format const href = fullMatch.substring(2, fullMatch.length - 1).split('#')[0] - const text = extractLinkText(content, match.index) + const text = extractLinkText(strippedContent, match.index) internalLinks.push({ href, @@ -127,10 +137,10 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult INTERNAL_LINK_PATTERN.lastIndex = 0 // Extract external links - while ((match = EXTERNAL_LINK_PATTERN.exec(content)) !== null) { - const { line, column } = getLineAndColumn(content, match.index) + while ((match = EXTERNAL_LINK_PATTERN.exec(strippedContent)) !== null) { + const { line, column } = getLineAndColumn(strippedContent, match.index) const href = match[1] - const text = extractLinkText(content, match.index) + const text = extractLinkText(strippedContent, match.index) externalLinks.push({ href, @@ -144,8 +154,8 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult EXTERNAL_LINK_PATTERN.lastIndex = 0 // Extract anchor links - while ((match = ANCHOR_LINK_PATTERN.exec(content)) !== null) { - const { line, column } = getLineAndColumn(content, match.index) + while ((match = ANCHOR_LINK_PATTERN.exec(strippedContent)) !== null) { + const { line, column } = getLineAndColumn(strippedContent, match.index) const href = match[0].substring(2, match[0].length - 1) anchorLinks.push({ @@ -160,8 +170,8 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult ANCHOR_LINK_PATTERN.lastIndex = 0 // Extract image links - while ((match = IMAGE_LINK_PATTERN.exec(content)) !== null) { - const { line, column } = getLineAndColumn(content, match.index) + while ((match = IMAGE_LINK_PATTERN.exec(strippedContent)) !== null) { + const { line, column } = getLineAndColumn(strippedContent, match.index) const href = match[1] // Only include internal images (starting with /) @@ -345,6 +355,19 @@ export function checkInternalLink( } } + // Strip language prefix and check redirects (which are stored without it) + const langPrefixMatch = resolved.match(/^\/[a-z]{2}\//) + if (langPrefixMatch) { + const withoutLang = resolved.slice(langPrefixMatch[0].length - 1) + if (redirects[withoutLang]) { + return { + exists: true, + isRedirect: true, + redirectTarget: redirects[withoutLang], + } + } + } + return { exists: false, isRedirect: false } } diff --git a/src/links/lib/validate-docs-urls.ts b/src/links/lib/validate-docs-urls.ts index 886dd43a5b15..fbcc7e265525 100644 --- a/src/links/lib/validate-docs-urls.ts +++ b/src/links/lib/validate-docs-urls.ts @@ -1,5 +1,5 @@ import type { Response } from 'express' -import cheerio from 'cheerio' +import { load } from 'cheerio' import warmServer from '@/frame/lib/warm-server' import { liquid } from '@/content-render/index' @@ -89,7 +89,7 @@ export async function validateDocsUrl(docsUrls: DocsUrls, { checkFragments = fal if (checkFragments && fragment) { const permalink = (redirectedPage || page).permalinks[0] const html = await renderInnerHTML(redirectedPage || page, permalink) - const $ = cheerio.load(html) + const $ = load(html) check.fragmentFound = $(`#${fragment}`).length > 0 || $(`a[name="${fragment}"]`).length > 0 if (!check.fragmentFound) { const fragmentCandidates: string[] = [] diff --git a/src/links/scripts/check-links-internal.ts b/src/links/scripts/check-links-internal.ts index 12a9c7ad98b9..a7a1b9c0da28 100644 --- a/src/links/scripts/check-links-internal.ts +++ b/src/links/scripts/check-links-internal.ts @@ -20,7 +20,7 @@ import { program } from 'commander' import chalk from 'chalk' -import cheerio from 'cheerio' +import { load } from 'cheerio' import warmServer from '@/frame/lib/warm-server' import { renderContent } from '@/content-render/index' @@ -73,7 +73,7 @@ async function getLinksFromRenderedPage( try { // Render the page content const html = await renderContent(page.markdown, context) - const $ = cheerio.load(html) + const $ = load(html) // Extract all anchor links $('a[href]').each((_, el) => { @@ -103,7 +103,7 @@ async function checkAnchorsOnPage( try { const html = await renderContent(page.markdown, context) - const $ = cheerio.load(html) + const $ = load(html) // Find all anchor links (same-page links) $('a[href^="#"]').each((_, el) => { diff --git a/src/links/tests/extract-links.ts b/src/links/tests/extract-links.ts index eb75e6bde958..6465ae4c7b01 100644 --- a/src/links/tests/extract-links.ts +++ b/src/links/tests/extract-links.ts @@ -140,6 +140,75 @@ Also [versioned](/enterprise-server@{{ currentVersion }}/admin). expect(result.internalLinks.length).toBeGreaterThanOrEqual(0) }) + test('extracts external links with parentheses in URLs', () => { + const content = ` +See the [shebang article](https://en.wikipedia.org/wiki/Shebang_(Unix)) for more. +Also [Continuum](https://en.wikipedia.org/wiki/Continuum_(measurement)) is relevant. +` + const result = extractLinksFromMarkdown(content) + + expect(result.externalLinks).toHaveLength(2) + expect(result.externalLinks[0].href).toBe('https://en.wikipedia.org/wiki/Shebang_(Unix)') + expect(result.externalLinks[1].href).toBe( + 'https://en.wikipedia.org/wiki/Continuum_(measurement)', + ) + }) + + test('skips links inside fenced code blocks', () => { + const content = ` +Here is [a real link](https://example.com). + +\`\`\`yaml +![badge](https://github.com/octocat/repo/actions/workflows/ci.yml/badge.svg) +[example](https://fake-example.com/not-real) +\`\`\` + +And [another real link](https://real.example.com/page). +` + const result = extractLinksFromMarkdown(content) + + expect(result.externalLinks).toHaveLength(2) + expect(result.externalLinks[0].href).toBe('https://example.com') + expect(result.externalLinks[1].href).toBe('https://real.example.com/page') + }) + + test('preserves correct line numbers when code blocks are stripped', () => { + const content = `Line 1 +[Link on line 2](/path/one) +\`\`\` +code block on line 3 +code block on line 4 +\`\`\` +Line 6 +[Link on line 8](/path/two) +` + const result = extractLinksFromMarkdown(content) + + expect(result.internalLinks).toHaveLength(2) + expect(result.internalLinks[0].line).toBe(2) + // Line numbers are preserved because code block content is replaced with spaces + expect(result.internalLinks[1].line).toBe(8) + }) + + test('skips links inside indented fenced code blocks', () => { + const content = ` +Here is [a real link](https://example.com). + +1. Step one: + + \`\`\`yaml + [example](https://fake-example.com/not-real) + \`\`\` + +And [another real link](https://real.example.com/page). +` + const result = extractLinksFromMarkdown(content) + + expect(result.externalLinks).toHaveLength(2) + expect(result.externalLinks[0].href).toBe('https://example.com') + expect(result.externalLinks[1].href).toBe('https://real.example.com/page') + }) + test('handles complex nested brackets', () => { const content = ` Use the [\`git clone\`](/repositories/cloning) command. @@ -186,6 +255,8 @@ describe('checkInternalLink', () => { const redirects = { '/en/old-path': '/en/new-path', '/en/deprecated': '/en/current', + '/enterprise-server@3.19/actions/old-path': '/enterprise-server@3.19/actions/new-path', + '/actions/legacy-path': '/actions/current-path', } test('finds direct page match', () => { @@ -233,6 +304,25 @@ describe('checkInternalLink', () => { const result = checkInternalLink('/enterprise-server@latest/does/not/exist', pageMap, redirects) expect(result.exists).toBe(false) }) + + test('finds redirect after stripping language prefix', () => { + // Links from rendered HTML have /en/ prefix but redirects are stored without it + const result = checkInternalLink( + '/en/enterprise-server@3.19/actions/old-path', + pageMap, + redirects, + ) + expect(result.exists).toBe(true) + expect(result.isRedirect).toBe(true) + expect(result.redirectTarget).toBe('/enterprise-server@3.19/actions/new-path') + }) + + test('finds versionless redirect after stripping language prefix', () => { + const result = checkInternalLink('/en/actions/legacy-path', pageMap, redirects) + expect(result.exists).toBe(true) + expect(result.isRedirect).toBe(true) + expect(result.redirectTarget).toBe('/actions/current-path') + }) }) describe('isAssetLink', () => { diff --git a/src/search/tests/topics.ts b/src/search/tests/topics.ts index f7a62ce02ffd..023bedff4294 100644 --- a/src/search/tests/topics.ts +++ b/src/search/tests/topics.ts @@ -21,7 +21,8 @@ const topics: string[] = walk(contentDir, { includeBasePath: true }) throw new Error(`More than 0 front-matter errors in file: ${filename}`) } - return (data as any).topics || [] + const pageTopics = (data as Record).topics + return Array.isArray(pageTopics) ? (pageTopics as string[]) : [] }) .flat() diff --git a/src/tests/helpers/e2etest.ts b/src/tests/helpers/e2etest.ts index 69224a2ecd41..85f66ad70412 100644 --- a/src/tests/helpers/e2etest.ts +++ b/src/tests/helpers/e2etest.ts @@ -1,4 +1,4 @@ -import cheerio from 'cheerio' +import { load, type CheerioAPI } from 'cheerio' import { fetchWithRetry } from '@/frame/lib/fetch-utils' import { omitBy, isUndefined } from 'lodash-es' @@ -35,7 +35,7 @@ interface ResponseWithHeaders { } // Type alias for cached DOM results to improve maintainability -type CachedDOMResult = cheerio.Root & { res: ResponseWithHeaders; $: cheerio.Root } +type CachedDOMResult = CheerioAPI & { res: ResponseWithHeaders; $: CheerioAPI } // Cache to store DOM objects const getDOMCache = new Map() @@ -174,7 +174,7 @@ export async function getDOM(route: string, options: GetDOMOptions = {}): Promis throw new Error(`Page not found on ${route} (${res.statusCode})`) } - const $ = cheerio.load(res.body || '', { xmlMode: true }) + const $ = load(res.body || '', { xmlMode: true }) const result = $ as CachedDOMResult // Attach res to the cheerio object for backward compatibility result.res = res diff --git a/src/tests/helpers/script-data.ts b/src/tests/helpers/script-data.ts index f28f6b7c7ad1..f3bef288faa3 100644 --- a/src/tests/helpers/script-data.ts +++ b/src/tests/helpers/script-data.ts @@ -1,9 +1,9 @@ -import cheerio from 'cheerio' +import type { CheerioAPI } from 'cheerio' const NEXT_DATA_QUERY = 'script#__NEXT_DATA__' const PRIMER_DATA_QUERY = 'script#__PRIMER_DATA__' -function getScriptData($: ReturnType, key: string): unknown { +function getScriptData($: CheerioAPI, key: string): unknown { const data = $(key) if (data.length !== 1) { throw new Error(`Not exactly 1 element match for '${key}'. Found ${data.length}`) @@ -18,7 +18,5 @@ function getScriptData($: ReturnType, key: string): unknown throw new Error(`Could not extract data from '${key}'`) } -export const getNextData = ($: ReturnType): unknown => - getScriptData($, NEXT_DATA_QUERY) -export const getPrimerData = ($: ReturnType): unknown => - getScriptData($, PRIMER_DATA_QUERY) +export const getNextData = ($: CheerioAPI): unknown => getScriptData($, NEXT_DATA_QUERY) +export const getPrimerData = ($: CheerioAPI): unknown => getScriptData($, PRIMER_DATA_QUERY) diff --git a/src/tools/components/Picker.tsx b/src/tools/components/Picker.tsx index e125576795f6..1d4fcf7bafdf 100644 --- a/src/tools/components/Picker.tsx +++ b/src/tools/components/Picker.tsx @@ -24,7 +24,10 @@ export interface PickerItem { text: string selected: boolean extra?: { - [key: string]: any + arrow?: boolean + info?: boolean + version?: string + currentDate?: string } divider?: boolean } diff --git a/src/workflows/experimental/readability-report.ts b/src/workflows/experimental/readability-report.ts index 14f51d2b1078..26dc8155fbe6 100644 --- a/src/workflows/experimental/readability-report.ts +++ b/src/workflows/experimental/readability-report.ts @@ -38,7 +38,7 @@ import fs from 'fs' import path from 'path' -import cheerio from 'cheerio' +import { load } from 'cheerio' import { fetchWithRetry } from '@/frame/lib/fetch-utils' interface ReadabilityMetrics { @@ -219,7 +219,7 @@ async function analyzeFile(filePath: string): Promise { // Parse HTML and extract content const body = await response.text() - const $ = cheerio.load(body) + const $ = load(body) // Get page title const title = $('h1').first().text().trim() || $('title').text().trim() || 'Untitled'