diff --git a/.github/actions/auth/package-lock.json b/.github/actions/auth/package-lock.json index 48a1790..d311f11 100644 --- a/.github/actions/auth/package-lock.json +++ b/.github/actions/auth/package-lock.json @@ -10,10 +10,10 @@ "license": "MIT", "dependencies": { "@actions/core": "^2.0.1", - "playwright": "^1.57.0" + "playwright": "^1.58.1" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } }, @@ -62,9 +62,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "dependencies": { @@ -86,12 +86,12 @@ } }, "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", + "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0" + "playwright-core": "1.58.1" }, "bin": { "playwright": "cli.js" @@ -104,9 +104,9 @@ } }, "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", + "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/.github/actions/auth/package.json b/.github/actions/auth/package.json index 4e79bfe..bcf6f10 100644 --- a/.github/actions/auth/package.json +++ b/.github/actions/auth/package.json @@ -14,10 +14,10 @@ "type": "module", "dependencies": { "@actions/core": "^2.0.1", - "playwright": "^1.57.0" + "playwright": "^1.58.1" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/.github/actions/auth/src/index.ts b/.github/actions/auth/src/index.ts index 4e0e4b1..2543caa 100644 --- a/.github/actions/auth/src/index.ts +++ b/.github/actions/auth/src/index.ts @@ -1,102 +1,96 @@ -import type { AuthContextOutput } from "./types.d.js"; -import crypto from "node:crypto"; -import process from "node:process"; -import * as url from "node:url"; -import core from "@actions/core"; -import playwright from "playwright"; +import type {AuthContextOutput} from './types.d.js' +import process from 'node:process' +import core from '@actions/core' +import playwright from 'playwright' export default async function () { - core.info("Starting 'auth' action"); + core.info("Starting 'auth' action") - let browser: playwright.Browser | undefined; - let context: playwright.BrowserContext | undefined; - let page: playwright.Page | undefined; + let browser: playwright.Browser | undefined + let context: playwright.BrowserContext | undefined + let page: playwright.Page | undefined try { // Get inputs - const loginUrl = core.getInput("login_url", { required: true }); - const username = core.getInput("username", { required: true }); - const password = core.getInput("password", { required: true }); - core.setSecret(password); - - // Determine storage path for authenticated session state - // Playwright will create missing directories, if needed - const actionDirectory = `${url.fileURLToPath(new URL(import.meta.url))}/..`; - const sessionStatePath = `${ - process.env.RUNNER_TEMP ?? actionDirectory - }/.auth/${crypto.randomUUID()}/sessionState.json`; + const loginUrl = core.getInput('login_url', {required: true}) + const username = core.getInput('username', {required: true}) + const password = core.getInput('password', {required: true}) + core.setSecret(password) // Launch a headless browser browser = await playwright.chromium.launch({ headless: true, - executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined, - }); + executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined, + }) context = await browser.newContext({ // Try HTTP Basic authentication httpCredentials: { username, password, }, - }); - page = await context.newPage(); + }) + page = await context.newPage() // Navigate to login page - core.info("Navigating to login page"); - await page.goto(loginUrl); + core.info('Navigating to login page') + await page.goto(loginUrl) // Check for a login form. // If no login form is found, then either HTTP Basic auth succeeded, or the page does not require authentication. - core.info("Checking for login form"); + core.info('Checking for login form') const [usernameField, passwordField] = await Promise.all([ page.getByLabel(/user ?name/i).first(), page.getByLabel(/password/i).first(), - ]); - const [usernameFieldExists, passwordFieldExists] = await Promise.all([ - usernameField.count(), - passwordField.count(), - ]); + ]) + const [usernameFieldExists, passwordFieldExists] = await Promise.all([usernameField.count(), passwordField.count()]) if (usernameFieldExists && passwordFieldExists) { // Try form authentication - core.info("Filling username"); - await usernameField.fill(username); - core.info("Filling password"); - await passwordField.fill(password); - core.info("Logging in"); + core.info('Filling username') + await usernameField.fill(username) + core.info('Filling password') + await passwordField.fill(password) + core.info('Logging in') await page .getByLabel(/password/i) - .locator("xpath=ancestor::form") - .evaluate((form) => (form as HTMLFormElement).submit()); + .locator('xpath=ancestor::form') + .evaluate(form => (form as HTMLFormElement).submit()) } else { - core.info("No login form detected"); + core.info('No login form detected') // This occurs if HTTP Basic auth succeeded, or if the page does not require authentication. } // Output authenticated session state - const { cookies, origins } = await context.storageState(); + const {cookies, origins} = await context.storageState() const authContextOutput: AuthContextOutput = { username, password, cookies, - localStorage: origins.reduce((acc, { origin, localStorage }) => { - acc[origin] = localStorage.reduce((acc, { name, value }) => { - acc[name] = value; - return acc; - }, {} as Record); - return acc; - }, {} as Record>), - }; - core.setOutput("auth_context", JSON.stringify(authContextOutput)); - core.debug("Output: 'auth_context'"); + localStorage: origins.reduce( + (acc, {origin, localStorage}) => { + acc[origin] = localStorage.reduce( + (acc, {name, value}) => { + acc[name] = value + return acc + }, + {} as Record, + ) + return acc + }, + {} as Record>, + ), + } + core.setOutput('auth_context', JSON.stringify(authContextOutput)) + core.debug("Output: 'auth_context'") } catch (error) { if (page) { - core.info(`Errored at page URL: ${page.url()}`); + core.info(`Errored at page URL: ${page.url()}`) } - core.setFailed(`${error}`); - process.exit(1); + core.setFailed(`${error}`) + process.exit(1) } finally { // Clean up - await context?.close(); - await browser?.close(); + await context?.close() + await browser?.close() } - core.info("Finished 'auth' action"); + core.info("Finished 'auth' action") } diff --git a/.github/actions/auth/src/types.d.ts b/.github/actions/auth/src/types.d.ts index 1c365ac..79db31e 100644 --- a/.github/actions/auth/src/types.d.ts +++ b/.github/actions/auth/src/types.d.ts @@ -1,23 +1,23 @@ export type Cookie = { - name: string; - value: string; - domain: string; - path: string; - expires?: number; - httpOnly?: boolean; - secure?: boolean; - sameSite?: "Strict" | "Lax" | "None"; -}; + name: string + value: string + domain: string + path: string + expires?: number + httpOnly?: boolean + secure?: boolean + sameSite?: 'Strict' | 'Lax' | 'None' +} export type LocalStorage = { [origin: string]: { - [key: string]: string; - }; -}; + [key: string]: string + } +} export type AuthContextOutput = { - username?: string; - password?: string; - cookies?: Cookie[]; - localStorage?: LocalStorage; -}; + username?: string + password?: string + cookies?: Cookie[] + localStorage?: LocalStorage +} diff --git a/.github/actions/file/action.yml b/.github/actions/file/action.yml index 005d3b0..c5db9d5 100644 --- a/.github/actions/file/action.yml +++ b/.github/actions/file/action.yml @@ -14,6 +14,9 @@ inputs: cached_filings: description: "Cached filings from previous runs, as stringified JSON. Without this, duplicate issues may be filed." required: false + screenshot_repository: + description: "Repository (with owner) where screenshots are stored on the gh-cache branch. Defaults to the 'repository' input if not set. Required if issues are open in a different repo to construct proper screenshot URLs." + required: false outputs: filings: diff --git a/.github/actions/file/package-lock.json b/.github/actions/file/package-lock.json index 511b3cd..0aed60d 100644 --- a/.github/actions/file/package-lock.json +++ b/.github/actions/file/package-lock.json @@ -14,7 +14,7 @@ "@octokit/plugin-throttling": "^11.0.3" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } }, @@ -76,7 +76,6 @@ "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", @@ -177,9 +176,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/.github/actions/file/package.json b/.github/actions/file/package.json index 754b3ea..d18a810 100644 --- a/.github/actions/file/package.json +++ b/.github/actions/file/package.json @@ -18,7 +18,7 @@ "@octokit/plugin-throttling": "^11.0.3" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } } diff --git a/.github/actions/file/src/Issue.ts b/.github/actions/file/src/Issue.ts index c494e1b..0e5e2af 100644 --- a/.github/actions/file/src/Issue.ts +++ b/.github/actions/file/src/Issue.ts @@ -1,44 +1,44 @@ -import type { Issue as IssueInput } from "./types.d.js"; +import type {Issue as IssueInput} from './types.d.js' export class Issue implements IssueInput { - #url!: string; + #url!: string #parsedUrl!: { - owner: string; - repository: string; - issueNumber: number; - }; - nodeId: string; - id: number; - title: string; - state?: "open" | "reopened" | "closed"; - - constructor({ url, nodeId, id, title, state }: IssueInput) { - this.url = url; - this.nodeId = nodeId; - this.id = id; - this.title = title; - this.state = state; + owner: string + repository: string + issueNumber: number + } + nodeId: string + id: number + title: string + state?: 'open' | 'reopened' | 'closed' + + constructor({url, nodeId, id, title, state}: IssueInput) { + this.url = url + this.nodeId = nodeId + this.id = id + this.title = title + this.state = state } set url(newUrl: string) { - this.#url = newUrl; - this.#parsedUrl = this.#parseUrl(); + this.#url = newUrl + this.#parsedUrl = this.#parseUrl() } get url(): string { - return this.#url; + return this.#url } get owner(): string { - return this.#parsedUrl.owner; + return this.#parsedUrl.owner } get repository(): string { - return this.#parsedUrl.repository; + return this.#parsedUrl.repository } get issueNumber(): number { - return this.#parsedUrl.issueNumber; + return this.#parsedUrl.issueNumber } /** @@ -47,17 +47,15 @@ export class Issue implements IssueInput { * @throws The provided URL is unparseable due to its unexpected format. */ #parseUrl(): { - owner: string; - repository: string; - issueNumber: number; + owner: string + repository: string + issueNumber: number } { - const { owner, repository, issueNumber } = - /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec( - this.#url - )?.groups || {}; + const {owner, repository, issueNumber} = + /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec(this.#url)?.groups || {} if (!owner || !repository || !issueNumber) { - throw new Error(`Could not parse issue URL: ${this.#url}`); + throw new Error(`Could not parse issue URL: ${this.#url}`) } - return { owner, repository, issueNumber: Number(issueNumber) }; + return {owner, repository, issueNumber: Number(issueNumber)} } } diff --git a/.github/actions/file/src/closeIssue.ts b/.github/actions/file/src/closeIssue.ts index a26f203..e589a6d 100644 --- a/.github/actions/file/src/closeIssue.ts +++ b/.github/actions/file/src/closeIssue.ts @@ -1,11 +1,11 @@ -import type { Octokit } from '@octokit/core'; -import { Issue } from './Issue.js'; +import type {Octokit} from '@octokit/core' +import {Issue} from './Issue.js' -export async function closeIssue(octokit: Octokit, { owner, repository, issueNumber }: Issue) { +export async function closeIssue(octokit: Octokit, {owner, repository, issueNumber}: Issue) { return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { owner, repository, issue_number: issueNumber, - state: 'closed' - }); -} \ No newline at end of file + state: 'closed', + }) +} diff --git a/.github/actions/file/src/generateIssueBody.ts b/.github/actions/file/src/generateIssueBody.ts new file mode 100644 index 0000000..a216cdd --- /dev/null +++ b/.github/actions/file/src/generateIssueBody.ts @@ -0,0 +1,39 @@ +import type {Finding} from './types.d.js' + +export function generateIssueBody(finding: Finding, screenshotRepo: string): string { + const solutionLong = finding.solutionLong + ?.split('\n') + .map((line: string) => + !line.trim().startsWith('Fix any') && !line.trim().startsWith('Fix all') && line.trim() !== '' + ? `- ${line}` + : line, + ) + .join('\n') + + let screenshotSection + if (finding.screenshotId) { + const screenshotUrl = `https://github.com/${screenshotRepo}/blob/gh-cache/.screenshots/${finding.screenshotId}.png` + screenshotSection = ` +[View screenshot](${screenshotUrl}) +` + } + + const acceptanceCriteria = `## Acceptance Criteria + - [ ] The specific axe violation reported in this issue is no longer reproducible. + - [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. + - [ ] A test SHOULD be added to ensure this specific axe violation does not regress. + - [ ] This PR MUST NOT introduce any new accessibility issues or regressions. + ` + + const body = `## What + An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. + + ${screenshotSection ?? ''} + To fix this, ${finding.solutionShort}. + ${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} + + ${acceptanceCriteria} + ` + + return body +} diff --git a/.github/actions/file/src/index.ts b/.github/actions/file/src/index.ts index 0c1304b..e95f6d4 100644 --- a/.github/actions/file/src/index.ts +++ b/.github/actions/file/src/index.ts @@ -1,90 +1,93 @@ -import type { Finding, ResolvedFiling, RepeatedFiling } from "./types.d.js"; -import process from "node:process"; -import core from "@actions/core"; -import { Octokit } from "@octokit/core"; -import { throttling } from "@octokit/plugin-throttling"; -import { Issue } from "./Issue.js"; -import { closeIssue } from "./closeIssue.js"; -import { isNewFiling } from "./isNewFiling.js"; -import { isRepeatedFiling } from "./isRepeatedFiling.js"; -import { isResolvedFiling } from "./isResolvedFiling.js"; -import { openIssue } from "./openIssue.js"; -import { reopenIssue } from "./reopenIssue.js"; -import { updateFilingsWithNewFindings } from "./updateFilingsWithNewFindings.js"; -const OctokitWithThrottling = Octokit.plugin(throttling); +import type {Finding, ResolvedFiling, RepeatedFiling} from './types.d.js' +import process from 'node:process' +import core from '@actions/core' +import {Octokit} from '@octokit/core' +import {throttling} from '@octokit/plugin-throttling' +import {Issue} from './Issue.js' +import {closeIssue} from './closeIssue.js' +import {isNewFiling} from './isNewFiling.js' +import {isRepeatedFiling} from './isRepeatedFiling.js' +import {isResolvedFiling} from './isResolvedFiling.js' +import {openIssue} from './openIssue.js' +import {reopenIssue} from './reopenIssue.js' +import {updateFilingsWithNewFindings} from './updateFilingsWithNewFindings.js' +const OctokitWithThrottling = Octokit.plugin(throttling) export default async function () { - core.info("Started 'file' action"); - const findings: Finding[] = JSON.parse( - core.getInput("findings", { required: true }) - ); - const repoWithOwner = core.getInput("repository", { required: true }); - const token = core.getInput("token", { required: true }); + core.info("Started 'file' action") + const findings: Finding[] = JSON.parse(core.getInput('findings', {required: true})) + const repoWithOwner = core.getInput('repository', {required: true}) + const token = core.getInput('token', {required: true}) + const screenshotRepo = core.getInput('screenshot_repository', {required: false}) || repoWithOwner const cachedFilings: (ResolvedFiling | RepeatedFiling)[] = JSON.parse( - core.getInput("cached_filings", { required: false }) || "[]" - ); - core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`); - core.debug(`Input: 'repository: ${repoWithOwner}'`); - core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`); + core.getInput('cached_filings', {required: false}) || '[]', + ) + core.debug(`Input: 'findings: ${JSON.stringify(findings)}'`) + core.debug(`Input: 'repository: ${repoWithOwner}'`) + core.debug(`Input: 'screenshot_repository: ${screenshotRepo}'`) + core.debug(`Input: 'cached_filings: ${JSON.stringify(cachedFilings)}'`) const octokit = new OctokitWithThrottling({ auth: token, throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}` - ); + octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}` - ); + octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, }, - }); - const filings = updateFilingsWithNewFindings(cachedFilings, findings); + }) + const filings = updateFilingsWithNewFindings(cachedFilings, findings) for (const filing of filings) { - let response; + let response try { if (isResolvedFiling(filing)) { // Close the filing’s issue (if necessary) - response = await closeIssue(octokit, new Issue(filing.issue)); - filing.issue.state = "closed"; + response = await closeIssue(octokit, new Issue(filing.issue)) + filing.issue.state = 'closed' } else if (isNewFiling(filing)) { // Open a new issue for the filing - response = await openIssue(octokit, repoWithOwner, filing.findings[0]); - (filing as any).issue = { state: "open" } as Issue; + response = await openIssue(octokit, repoWithOwner, filing.findings[0], screenshotRepo) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(filing as any).issue = {state: 'open'} as Issue } else if (isRepeatedFiling(filing)) { - // Reopen the filing’s issue (if necessary) - response = await reopenIssue(octokit, new Issue(filing.issue)); - filing.issue.state = "reopened"; + // Reopen the filing's issue (if necessary) and update the body with the latest finding + response = await reopenIssue( + octokit, + new Issue(filing.issue), + filing.findings[0], + repoWithOwner, + screenshotRepo, + ) + filing.issue.state = 'reopened' } if (response?.data && filing.issue) { // Update the filing with the latest issue data - filing.issue.id = response.data.id; - filing.issue.nodeId = response.data.node_id; - filing.issue.url = response.data.html_url; - filing.issue.title = response.data.title; + filing.issue.id = response.data.id + filing.issue.nodeId = response.data.node_id + filing.issue.url = response.data.html_url + filing.issue.title = response.data.title core.info( - `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}` - ); + `Set issue ${response.data.title} (${repoWithOwner}#${response.data.number}) state to ${filing.issue.state}`, + ) } } catch (error) { - core.setFailed(`Failed on filing: ${filing}\n${error}`); - process.exit(1); + core.setFailed(`Failed on filing: ${filing}\n${error}`) + process.exit(1) } } - core.setOutput("filings", JSON.stringify(filings)); - core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`); - core.info("Finished 'file' action"); + core.setOutput('filings', JSON.stringify(filings)) + core.debug(`Output: 'filings: ${JSON.stringify(filings)}'`) + core.info("Finished 'file' action") } diff --git a/.github/actions/file/src/isNewFiling.ts b/.github/actions/file/src/isNewFiling.ts index 5a0ae20..0d7ebd2 100644 --- a/.github/actions/file/src/isNewFiling.ts +++ b/.github/actions/file/src/isNewFiling.ts @@ -1,10 +1,6 @@ -import type { Filing, NewFiling } from "./types.d.js"; +import type {Filing, NewFiling} from './types.d.js' export function isNewFiling(filing: Filing): filing is NewFiling { // A Filing without an issue is new - return ( - (!("issue" in filing) || !filing.issue?.url) && - "findings" in filing && - filing.findings.length > 0 - ); + return (!('issue' in filing) || !filing.issue?.url) && 'findings' in filing && filing.findings.length > 0 } diff --git a/.github/actions/file/src/isRepeatedFiling.ts b/.github/actions/file/src/isRepeatedFiling.ts index b9dd5f2..e18cee8 100644 --- a/.github/actions/file/src/isRepeatedFiling.ts +++ b/.github/actions/file/src/isRepeatedFiling.ts @@ -1,11 +1,6 @@ -import type { Filing, RepeatedFiling } from "./types.d.js"; +import type {Filing, RepeatedFiling} from './types.d.js' export function isRepeatedFiling(filing: Filing): filing is RepeatedFiling { // A Filing with an issue and findings is a repeated filing - return ( - "findings" in filing && - filing.findings.length > 0 && - "issue" in filing && - !!filing.issue?.url - ); + return 'findings' in filing && filing.findings.length > 0 && 'issue' in filing && !!filing.issue?.url } diff --git a/.github/actions/file/src/isResolvedFiling.ts b/.github/actions/file/src/isResolvedFiling.ts index e3d7aea..544def1 100644 --- a/.github/actions/file/src/isResolvedFiling.ts +++ b/.github/actions/file/src/isResolvedFiling.ts @@ -1,10 +1,6 @@ -import type { Filing, ResolvedFiling } from "./types.d.js"; +import type {Filing, ResolvedFiling} from './types.d.js' export function isResolvedFiling(filing: Filing): filing is ResolvedFiling { // A Filing without findings is resolved - return ( - (!("findings" in filing) || filing.findings.length === 0) && - "issue" in filing && - !!filing.issue?.url - ); + return (!('findings' in filing) || filing.findings.length === 0) && 'issue' in filing && !!filing.issue?.url } diff --git a/.github/actions/file/src/openIssue.ts b/.github/actions/file/src/openIssue.ts index 9751772..2297daa 100644 --- a/.github/actions/file/src/openIssue.ts +++ b/.github/actions/file/src/openIssue.ts @@ -1,10 +1,11 @@ -import type { Octokit } from '@octokit/core'; -import type { Finding } from './types.d.js'; +import type {Octokit} from '@octokit/core' +import type {Finding} from './types.d.js' +import {generateIssueBody} from './generateIssueBody.js' import * as url from 'node:url' -const URL = url.URL; +const URL = url.URL /** Max length for GitHub issue titles */ -const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256; +const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256 /** * Truncates text to a maximum length, adding an ellipsis if truncated. @@ -13,48 +14,26 @@ const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256; * @returns Either the original text or a truncated version with an ellipsis */ function truncateWithEllipsis(text: string, maxLength: number): string { - return text.length > maxLength ? text.slice(0, maxLength - 1) + '…' : text; + return text.length > maxLength ? text.slice(0, maxLength - 1) + '…' : text } -export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding) { - const owner = repoWithOwner.split('/')[0]; - const repo = repoWithOwner.split('/')[1]; +export async function openIssue(octokit: Octokit, repoWithOwner: string, finding: Finding, screenshotRepo?: string) { + const owner = repoWithOwner.split('/')[0] + const repo = repoWithOwner.split('/')[1] - const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`]; + const labels = [`${finding.scannerType} rule: ${finding.ruleId}`, `${finding.scannerType}-scanning-issue`] const title = truncateWithEllipsis( `Accessibility issue: ${finding.problemShort[0].toUpperCase() + finding.problemShort.slice(1)} on ${new URL(finding.url).pathname}`, - GITHUB_ISSUE_TITLE_MAX_LENGTH - ); - const solutionLong = finding.solutionLong - ?.split("\n") - .map((line) => - !line.trim().startsWith("Fix any") && - !line.trim().startsWith("Fix all") && - line.trim() !== "" - ? `- ${line}` - : line - ) - .join("\n"); - const acceptanceCriteria = `## Acceptance Criteria -- [ ] The specific axe violation reported in this issue is no longer reproducible. -- [ ] The fix MUST meet WCAG 2.1 guidelines OR the accessibility standards specified by the repository or organization. -- [ ] A test SHOULD be added to ensure this specific axe violation does not regress. -- [ ] This PR MUST NOT introduce any new accessibility issues or regressions. -`; - const body = `## What -An accessibility scan flagged the element \`${finding.html}\` on ${finding.url} because ${finding.problemShort}. Learn more about why this was flagged by visiting ${finding.problemUrl}. + GITHUB_ISSUE_TITLE_MAX_LENGTH, + ) -To fix this, ${finding.solutionShort}. -${solutionLong ? `\nSpecifically:\n\n${solutionLong}` : ''} - -${acceptanceCriteria} -`; + const body = generateIssueBody(finding, screenshotRepo ?? repoWithOwner) return octokit.request(`POST /repos/${owner}/${repo}/issues`, { owner, repo, title, body, - labels - }); + labels, + }) } diff --git a/.github/actions/file/src/reopenIssue.ts b/.github/actions/file/src/reopenIssue.ts index e777bfd..329c695 100644 --- a/.github/actions/file/src/reopenIssue.ts +++ b/.github/actions/file/src/reopenIssue.ts @@ -1,11 +1,25 @@ -import type { Octokit } from '@octokit/core'; -import type { Issue } from './Issue.js'; +import type {Octokit} from '@octokit/core' +import type {Issue} from './Issue.js' +import type {Finding} from './types.d.js' +import {generateIssueBody} from './generateIssueBody.js' + +export async function reopenIssue( + octokit: Octokit, + {owner, repository, issueNumber}: Issue, + finding?: Finding, + repoWithOwner?: string, + screenshotRepo?: string, +) { + let body: string | undefined + if (finding && repoWithOwner) { + body = generateIssueBody(finding, screenshotRepo ?? repoWithOwner) + } -export async function reopenIssue(octokit: Octokit, { owner, repository, issueNumber}: Issue) { return octokit.request(`PATCH /repos/${owner}/${repository}/issues/${issueNumber}`, { owner, repository, issue_number: issueNumber, - state: 'open' - }); + state: 'open', + body, + }) } diff --git a/.github/actions/file/src/types.d.ts b/.github/actions/file/src/types.d.ts index bcc52ea..36069a5 100644 --- a/.github/actions/file/src/types.d.ts +++ b/.github/actions/file/src/types.d.ts @@ -1,35 +1,36 @@ export type Finding = { - scannerType: string; - ruleId: string; - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; -}; + scannerType: string + ruleId: string + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string + screenshotId?: string +} export type Issue = { - id: number; - nodeId: string; - url: string; - title: string; - state?: "open" | "reopened" | "closed"; -}; + id: number + nodeId: string + url: string + title: string + state?: 'open' | 'reopened' | 'closed' +} export type ResolvedFiling = { - findings: never[]; - issue: Issue; -}; + findings: never[] + issue: Issue +} export type NewFiling = { - findings: Finding[]; - issue?: never; -}; + findings: Finding[] + issue?: never +} export type RepeatedFiling = { - findings: Finding[]; - issue: Issue; -}; + findings: Finding[] + issue: Issue +} -export type Filing = ResolvedFiling | NewFiling | RepeatedFiling; +export type Filing = ResolvedFiling | NewFiling | RepeatedFiling diff --git a/.github/actions/file/src/updateFilingsWithNewFindings.ts b/.github/actions/file/src/updateFilingsWithNewFindings.ts index 3a72557..a674e68 100644 --- a/.github/actions/file/src/updateFilingsWithNewFindings.ts +++ b/.github/actions/file/src/updateFilingsWithNewFindings.ts @@ -1,28 +1,22 @@ -import type { - Finding, - ResolvedFiling, - NewFiling, - RepeatedFiling, - Filing, -} from "./types.d.js"; +import type {Finding, ResolvedFiling, NewFiling, RepeatedFiling, Filing} from './types.d.js' function getFilingKey(filing: ResolvedFiling | RepeatedFiling): string { - return filing.issue.url; + return filing.issue.url } function getFindingKey(finding: Finding): string { - return `${finding.url};${finding.ruleId};${finding.html}`; + return `${finding.url};${finding.ruleId};${finding.html}` } export function updateFilingsWithNewFindings( filings: (ResolvedFiling | RepeatedFiling)[], - findings: Finding[] + findings: Finding[], ): Filing[] { const filingKeys: { - [key: string]: ResolvedFiling | RepeatedFiling; - } = {}; - const findingKeys: { [key: string]: string } = {}; - const newFilings: NewFiling[] = []; + [key: string]: ResolvedFiling | RepeatedFiling + } = {} + const findingKeys: {[key: string]: string} = {} + const newFilings: NewFiling[] = [] // Create maps for filing and finding data from previous runs, for quick lookups for (const filing of filings) { @@ -30,23 +24,23 @@ export function updateFilingsWithNewFindings( filingKeys[getFilingKey(filing)] = { issue: filing.issue, findings: [], - }; + } for (const finding of filing.findings) { - findingKeys[getFindingKey(finding)] = getFilingKey(filing); + findingKeys[getFindingKey(finding)] = getFilingKey(filing) } } for (const finding of findings) { - const filingKey = findingKeys[getFindingKey(finding)]; + const filingKey = findingKeys[getFindingKey(finding)] if (filingKey) { // This finding already has an associated filing; add it to that filing's findings - (filingKeys[filingKey] as RepeatedFiling).findings.push(finding); + ;(filingKeys[filingKey] as RepeatedFiling).findings.push(finding) } else { // This finding is new; create a new entry with no associated issue yet - newFilings.push({ findings: [finding] }); + newFilings.push({findings: [finding]}) } } - const updatedFilings = Object.values(filingKeys); - return [...updatedFilings, ...newFilings]; + const updatedFilings = Object.values(filingKeys) + return [...updatedFilings, ...newFilings] } diff --git a/.github/actions/file/tests/generateIssueBody.test.ts b/.github/actions/file/tests/generateIssueBody.test.ts new file mode 100644 index 0000000..96023b6 --- /dev/null +++ b/.github/actions/file/tests/generateIssueBody.test.ts @@ -0,0 +1,64 @@ +import {describe, it, expect} from 'vitest' +import {generateIssueBody} from '../src/generateIssueBody.ts' + +const baseFinding = { + scannerType: 'axe', + ruleId: 'color-contrast', + url: 'https://example.com/page', + html: 'Low contrast', + problemShort: 'elements must meet minimum color contrast ratio thresholds', + problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=playwright', + solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', +} + +describe('generateIssueBody', () => { + it('includes acceptance criteria and omits the Specifically section when solutionLong is missing', () => { + const body = generateIssueBody(baseFinding, 'github/accessibility-scanner') + + expect(body).toContain('## What') + expect(body).toContain('## Acceptance Criteria') + expect(body).toContain('The specific axe violation reported in this issue is no longer reproducible.') + expect(body).not.toContain('Specifically:') + }) + + it('formats solutionLong lines into bullets while preserving Fix any/Fix all lines', () => { + const body = generateIssueBody( + { + ...baseFinding, + solutionLong: [ + 'Use a darker foreground color.', + 'Fix any of the following:', + 'Increase font weight.', + 'Fix all of the following:', + 'Add a non-color visual indicator.', + '', + ].join('\n'), + }, + 'github/accessibility-scanner', + ) + + expect(body).toContain('Specifically:') + expect(body).toContain('- Use a darker foreground color.') + expect(body).toContain('Fix any of the following:') + expect(body).toContain('- Increase font weight.') + expect(body).toContain('Fix all of the following:') + expect(body).toContain('- Add a non-color visual indicator.') + + expect(body).not.toContain('- Fix any of the following:') + expect(body).not.toContain('- Fix all of the following:') + }) + + it('uses the screenshotRepo for the screenshot URL, not the filing repo', () => { + const body = generateIssueBody({...baseFinding, screenshotId: 'abc123'}, 'github/my-workflow-repo') + + expect(body).toContain('github/my-workflow-repo/blob/gh-cache/.screenshots/abc123.png') + expect(body).not.toContain('github/accessibility-scanner') + }) + + it('omits screenshot section when screenshotId is not present', () => { + const body = generateIssueBody(baseFinding, 'github/accessibility-scanner') + + expect(body).not.toContain('View screenshot') + expect(body).not.toContain('.screenshots') + }) +}) diff --git a/.github/actions/file/tests/openIssue.test.ts b/.github/actions/file/tests/openIssue.test.ts new file mode 100644 index 0000000..1757381 --- /dev/null +++ b/.github/actions/file/tests/openIssue.test.ts @@ -0,0 +1,80 @@ +import {describe, it, expect, vi} from 'vitest' + +// Mock generateIssueBody so we can inspect what screenshotRepo is passed +vi.mock('../src/generateIssueBody.js', () => ({ + generateIssueBody: vi.fn((_finding, screenshotRepo: string) => `body with screenshotRepo=${screenshotRepo}`), +})) + +import {openIssue} from '../src/openIssue.ts' +import {generateIssueBody} from '../src/generateIssueBody.ts' + +const baseFinding = { + scannerType: 'axe', + ruleId: 'color-contrast', + url: 'https://example.com/page', + html: 'Low contrast', + problemShort: 'elements must meet minimum color contrast ratio thresholds', + problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=playwright', + solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', +} + +function mockOctokit() { + return { + request: vi.fn().mockResolvedValue({data: {id: 1, html_url: 'https://github.com/org/repo/issues/1'}}), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any +} + +describe('openIssue', () => { + it('passes screenshotRepo to generateIssueBody when provided', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/filing-repo', baseFinding, 'org/workflow-repo') + + expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, 'org/workflow-repo') + }) + + it('falls back to repoWithOwner when screenshotRepo is not provided', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/filing-repo', baseFinding) + + expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, 'org/filing-repo') + }) + + it('posts to the correct filing repo, not the screenshot repo', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/filing-repo', baseFinding, 'org/workflow-repo') + + expect(octokit.request).toHaveBeenCalledWith( + 'POST /repos/org/filing-repo/issues', + expect.objectContaining({ + owner: 'org', + repo: 'filing-repo', + }), + ) + }) + + it('includes the correct labels based on the finding', async () => { + const octokit = mockOctokit() + await openIssue(octokit, 'org/repo', baseFinding) + + expect(octokit.request).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + labels: ['axe rule: color-contrast', 'axe-scanning-issue'], + }), + ) + }) + + it('truncates long titles with ellipsis', async () => { + const octokit = mockOctokit() + const longFinding = { + ...baseFinding, + problemShort: 'a'.repeat(300), + } + await openIssue(octokit, 'org/repo', longFinding) + + const callArgs = octokit.request.mock.calls[0][1] + expect(callArgs.title.length).toBeLessThanOrEqual(256) + expect(callArgs.title).toMatch(/…$/) + }) +}) diff --git a/.github/actions/file/tests/reopenIssue.test.ts b/.github/actions/file/tests/reopenIssue.test.ts new file mode 100644 index 0000000..f5b34ef --- /dev/null +++ b/.github/actions/file/tests/reopenIssue.test.ts @@ -0,0 +1,98 @@ +import {describe, it, expect, vi, beforeEach} from 'vitest' + +// Mock generateIssueBody so we can inspect what screenshotRepo is passed +vi.mock('../src/generateIssueBody.js', () => ({ + generateIssueBody: vi.fn((_finding, screenshotRepo: string) => `body with screenshotRepo=${screenshotRepo}`), +})) + +import {reopenIssue} from '../src/reopenIssue.ts' +import {generateIssueBody} from '../src/generateIssueBody.ts' +import {Issue} from '../src/Issue.ts' + +const baseFinding = { + scannerType: 'axe', + ruleId: 'color-contrast', + url: 'https://example.com/page', + html: 'Low contrast', + problemShort: 'elements must meet minimum color contrast ratio thresholds', + problemUrl: 'https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=playwright', + solutionShort: 'ensure the contrast between foreground and background colors meets WCAG thresholds', +} + +const testIssue = new Issue({ + id: 42, + nodeId: 'MDU6SXNzdWU0Mg==', + url: 'https://github.com/org/filing-repo/issues/7', + title: 'Accessibility issue: test', + state: 'closed', +}) + +function mockOctokit() { + return { + request: vi.fn().mockResolvedValue({data: {id: 42, html_url: 'https://github.com/org/filing-repo/issues/7'}}), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any +} + +describe('reopenIssue', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('passes screenshotRepo to generateIssueBody when provided', async () => { + const octokit = mockOctokit() + await reopenIssue(octokit, testIssue, baseFinding, 'org/filing-repo', 'org/workflow-repo') + + expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, 'org/workflow-repo') + }) + + it('falls back to repoWithOwner when screenshotRepo is not provided', async () => { + const octokit = mockOctokit() + await reopenIssue(octokit, testIssue, baseFinding, 'org/filing-repo') + + expect(generateIssueBody).toHaveBeenCalledWith(baseFinding, 'org/filing-repo') + }) + + it('does not generate a body when finding is not provided', async () => { + const octokit = mockOctokit() + await reopenIssue(octokit, testIssue) + + expect(generateIssueBody).not.toHaveBeenCalled() + expect(octokit.request).toHaveBeenCalledWith( + expect.any(String), + expect.not.objectContaining({body: expect.anything()}), + ) + }) + + it('does not generate a body when repoWithOwner is not provided', async () => { + const octokit = mockOctokit() + await reopenIssue(octokit, testIssue, baseFinding) + + expect(generateIssueBody).not.toHaveBeenCalled() + }) + + it('sends PATCH to the correct issue URL with state open', async () => { + const octokit = mockOctokit() + await reopenIssue(octokit, testIssue, baseFinding, 'org/filing-repo', 'org/workflow-repo') + + expect(octokit.request).toHaveBeenCalledWith( + 'PATCH /repos/org/filing-repo/issues/7', + expect.objectContaining({ + state: 'open', + issue_number: 7, + }), + ) + }) + + it('includes generated body when finding and repoWithOwner are provided', async () => { + const octokit = mockOctokit() + await reopenIssue(octokit, testIssue, baseFinding, 'org/filing-repo', 'org/workflow-repo') + + expect(octokit.request).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: 'body with screenshotRepo=org/workflow-repo', + }), + ) + }) +}) diff --git a/.github/actions/find/action.yml b/.github/actions/find/action.yml index 696d11d..2ab8dcb 100644 --- a/.github/actions/find/action.yml +++ b/.github/actions/find/action.yml @@ -9,6 +9,10 @@ inputs: auth_context: description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session" required: false + include_screenshots: + description: "Whether to capture screenshots of scanned pages and include links to them in the issue" + required: false + default: "false" outputs: findings: diff --git a/.github/actions/find/package-lock.json b/.github/actions/find/package-lock.json index af1a912..0da5db9 100644 --- a/.github/actions/find/package-lock.json +++ b/.github/actions/find/package-lock.json @@ -11,10 +11,10 @@ "dependencies": { "@actions/core": "^2.0.1", "@axe-core/playwright": "^4.11.0", - "playwright": "^1.57.0" + "playwright": "^1.58.1" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } }, @@ -75,9 +75,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "dependencies": { @@ -108,12 +108,12 @@ } }, "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", + "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.57.0" + "playwright-core": "1.58.1" }, "bin": { "playwright": "cli.js" @@ -126,11 +126,10 @@ } }, "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", + "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, diff --git a/.github/actions/find/package.json b/.github/actions/find/package.json index 05c85fa..878981a 100644 --- a/.github/actions/find/package.json +++ b/.github/actions/find/package.json @@ -15,10 +15,10 @@ "dependencies": { "@actions/core": "^2.0.1", "@axe-core/playwright": "^4.11.0", - "playwright": "^1.57.0" + "playwright": "^1.58.1" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/.github/actions/find/src/AuthContext.ts b/.github/actions/find/src/AuthContext.ts index 820425f..5b815e0 100644 --- a/.github/actions/find/src/AuthContext.ts +++ b/.github/actions/find/src/AuthContext.ts @@ -1,37 +1,36 @@ -import type playwright from "playwright"; -import type { Cookie, LocalStorage, AuthContextInput } from "./types.js"; +import type playwright from 'playwright' +import type {Cookie, LocalStorage, AuthContextInput} from './types.js' export class AuthContext implements AuthContextInput { - readonly username?: string; - readonly password?: string; - readonly cookies?: Cookie[]; - readonly localStorage?: LocalStorage; + readonly username?: string + readonly password?: string + readonly cookies?: Cookie[] + readonly localStorage?: LocalStorage - constructor({ username, password, cookies, localStorage }: AuthContextInput) { - this.username = username; - this.password = password; - this.cookies = cookies; - this.localStorage = localStorage; + constructor({username, password, cookies, localStorage}: AuthContextInput) { + this.username = username + this.password = password + this.cookies = cookies + this.localStorage = localStorage } toPlaywrightBrowserContextOptions(): playwright.BrowserContextOptions { - const playwrightBrowserContextOptions: playwright.BrowserContextOptions = - {}; + const playwrightBrowserContextOptions: playwright.BrowserContextOptions = {} if (this.username && this.password) { playwrightBrowserContextOptions.httpCredentials = { username: this.username, password: this.password, - }; + } } if (this.cookies || this.localStorage) { playwrightBrowserContextOptions.storageState = { // Add default values for fields Playwright requires which aren’t actually required by the Cookie API. cookies: - this.cookies?.map((cookie) => ({ + this.cookies?.map(cookie => ({ expires: -1, httpOnly: false, secure: false, - sameSite: "Lax", + sameSite: 'Lax', ...cookie, })) ?? [], // Transform the localStorage object into the shape Playwright expects. @@ -43,8 +42,8 @@ export class AuthContext implements AuthContextInput { value, })), })) ?? [], - }; + } } - return playwrightBrowserContextOptions; + return playwrightBrowserContextOptions } } diff --git a/.github/actions/find/src/findForUrl.ts b/.github/actions/find/src/findForUrl.ts index 3bcd3fa..185f58d 100644 --- a/.github/actions/find/src/findForUrl.ts +++ b/.github/actions/find/src/findForUrl.ts @@ -1,33 +1,48 @@ -import type { Finding } from './types.d.js'; +import type {Finding} from './types.d.js' import AxeBuilder from '@axe-core/playwright' -import playwright from 'playwright'; -import { AuthContext } from './AuthContext.js'; +import playwright from 'playwright' +import {AuthContext} from './AuthContext.js' +import {generateScreenshots} from './generateScreenshots.js' -export async function findForUrl(url: string, authContext?: AuthContext): Promise { - const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined }); - const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {}; - const context = await browser.newContext(contextOptions); - const page = await context.newPage(); - await page.goto(url); - console.log(`Scanning ${page.url()}`); +export async function findForUrl( + url: string, + authContext?: AuthContext, + includeScreenshots: boolean = false, +): Promise { + const browser = await playwright.chromium.launch({ + headless: true, + executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined, + }) + const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {} + const context = await browser.newContext(contextOptions) + const page = await context.newPage() + await page.goto(url) + console.log(`Scanning ${page.url()}`) - let findings: Finding[] = []; + let findings: Finding[] = [] try { - const rawFindings = await new AxeBuilder({ page }).analyze(); + const rawFindings = await new AxeBuilder({page}).analyze() + + let screenshotId: string | undefined + if (includeScreenshots) { + screenshotId = await generateScreenshots(page) + } + findings = rawFindings.violations.map(violation => ({ scannerType: 'axe', url, - html: violation.nodes[0].html.replace(/'/g, "'"), - problemShort: violation.help.toLowerCase().replace(/'/g, "'"), - problemUrl: violation.helpUrl.replace(/'/g, "'"), + html: violation.nodes[0].html.replace(/'/g, '''), + problemShort: violation.help.toLowerCase().replace(/'/g, '''), + problemUrl: violation.helpUrl.replace(/'/g, '''), ruleId: violation.id, - solutionShort: violation.description.toLowerCase().replace(/'/g, "'"), - solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, "'") - })); + solutionShort: violation.description.toLowerCase().replace(/'/g, '''), + solutionLong: violation.nodes[0].failureSummary?.replace(/'/g, '''), + screenshotId, + })) } catch (e) { - // do something with the error + console.error('Error during accessibility scan:', e) } - await context.close(); - await browser.close(); - return findings; + await context.close() + await browser.close() + return findings } diff --git a/.github/actions/find/src/generateScreenshots.ts b/.github/actions/find/src/generateScreenshots.ts new file mode 100644 index 0000000..d1dd353 --- /dev/null +++ b/.github/actions/find/src/generateScreenshots.ts @@ -0,0 +1,38 @@ +import fs from 'node:fs' +import path from 'node:path' +import crypto from 'node:crypto' +import type {Page} from 'playwright' + +// Use GITHUB_WORKSPACE to ensure screenshots are saved in the workflow workspace root +// where the artifact upload step can find them +const SCREENSHOT_DIR = path.join(process.env.GITHUB_WORKSPACE || process.cwd(), '.screenshots') + +export const generateScreenshots = async function (page: Page) { + let screenshotId: string | undefined + // Ensure screenshot directory exists + if (!fs.existsSync(SCREENSHOT_DIR)) { + fs.mkdirSync(SCREENSHOT_DIR, {recursive: true}) + console.log(`Created screenshot directory: ${SCREENSHOT_DIR}`) + } else { + console.log(`Using existing screenshot directory ${SCREENSHOT_DIR}`) + } + + try { + const screenshotBuffer = await page.screenshot({ + fullPage: true, + type: 'png', + }) + + screenshotId = crypto.randomUUID() + const filename = `${screenshotId}.png` + const filepath = path.join(SCREENSHOT_DIR, filename) + + fs.writeFileSync(filepath, screenshotBuffer) + console.log(`Screenshot saved: ${filename}`) + } catch (error) { + console.error('Failed to capture/save screenshot:', error) + screenshotId = undefined + } + + return screenshotId +} diff --git a/.github/actions/find/src/index.ts b/.github/actions/find/src/index.ts index e596647..d5f53da 100644 --- a/.github/actions/find/src/index.ts +++ b/.github/actions/find/src/index.ts @@ -1,31 +1,31 @@ -import type { AuthContextInput } from "./types.js"; -import core from "@actions/core"; -import { AuthContext } from "./AuthContext.js"; -import { findForUrl } from "./findForUrl.js"; +import type {AuthContextInput} from './types.js' +import core from '@actions/core' +import {AuthContext} from './AuthContext.js' +import {findForUrl} from './findForUrl.js' export default async function () { - core.info("Starting 'find' action"); - const urls = core.getMultilineInput("urls", { required: true }); - core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`); - const authContextInput: AuthContextInput = JSON.parse( - core.getInput("auth_context", { required: false }) || "{}" - ); - const authContext = new AuthContext(authContextInput); + core.info("Starting 'find' action") + const urls = core.getMultilineInput('urls', {required: true}) + core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`) + const authContextInput: AuthContextInput = JSON.parse(core.getInput('auth_context', {required: false}) || '{}') + const authContext = new AuthContext(authContextInput) - let findings = []; + const includeScreenshots = core.getInput('include_screenshots', {required: false}) !== 'false' + + const findings = [] for (const url of urls) { - core.info(`Preparing to scan ${url}`); - const findingsForUrl = await findForUrl(url, authContext); + core.info(`Preparing to scan ${url}`) + const findingsForUrl = await findForUrl(url, authContext, includeScreenshots) if (findingsForUrl.length === 0) { - core.info(`No accessibility gaps were found on ${url}`); - continue; + core.info(`No accessibility gaps were found on ${url}`) + continue } - findings.push(...findingsForUrl); - core.info(`Found ${findingsForUrl.length} findings for ${url}`); + findings.push(...findingsForUrl) + core.info(`Found ${findingsForUrl.length} findings for ${url}`) } - core.setOutput("findings", JSON.stringify(findings)); - core.debug(`Output: 'findings: ${JSON.stringify(findings)}'`); - core.info(`Found ${findings.length} findings in total`); - core.info("Finished 'find' action"); + core.setOutput('findings', JSON.stringify(findings)) + core.debug(`Output: 'findings: ${JSON.stringify(findings)}'`) + core.info(`Found ${findings.length} findings in total`) + core.info("Finished 'find' action") } diff --git a/.github/actions/find/src/types.d.ts b/.github/actions/find/src/types.d.ts index ee0ea27..72582c4 100644 --- a/.github/actions/find/src/types.d.ts +++ b/.github/actions/find/src/types.d.ts @@ -1,32 +1,33 @@ export type Finding = { - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; -}; + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string + screenshotId?: string +} export type Cookie = { - name: string; - value: string; - domain: string; - path: string; - expires?: number; - httpOnly?: boolean; - secure?: boolean; - sameSite?: "Strict" | "Lax" | "None"; -}; + name: string + value: string + domain: string + path: string + expires?: number + httpOnly?: boolean + secure?: boolean + sameSite?: 'Strict' | 'Lax' | 'None' +} export type LocalStorage = { [origin: string]: { - [key: string]: string; - }; -}; + [key: string]: string + } +} export type AuthContextInput = { - username?: string; - password?: string; - cookies?: Cookie[]; - localStorage?: LocalStorage; -}; + username?: string + password?: string + cookies?: Cookie[] + localStorage?: LocalStorage +} diff --git a/.github/actions/fix/package-lock.json b/.github/actions/fix/package-lock.json index 5775460..eb77729 100644 --- a/.github/actions/fix/package-lock.json +++ b/.github/actions/fix/package-lock.json @@ -14,7 +14,7 @@ "@octokit/plugin-throttling": "^11.0.3" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } }, @@ -76,7 +76,6 @@ "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", @@ -177,9 +176,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/.github/actions/fix/package.json b/.github/actions/fix/package.json index 9c11f05..9d1acba 100644 --- a/.github/actions/fix/package.json +++ b/.github/actions/fix/package.json @@ -18,7 +18,7 @@ "@octokit/plugin-throttling": "^11.0.3" }, "devDependencies": { - "@types/node": "^25.0.3", + "@types/node": "^25.2.0", "typescript": "^5.9.3" } } diff --git a/.github/actions/fix/src/Issue.ts b/.github/actions/fix/src/Issue.ts index eaecf01..0f7c35a 100644 --- a/.github/actions/fix/src/Issue.ts +++ b/.github/actions/fix/src/Issue.ts @@ -1,4 +1,4 @@ -import { Issue as IssueInput } from "./types.d.js"; +import {Issue as IssueInput} from './types.d.js' export class Issue implements IssueInput { /** @@ -7,31 +7,36 @@ export class Issue implements IssueInput { * @returns An object with `owner`, `repository`, and `issueNumber` keys. * @throws The provided URL is unparseable due to its unexpected format. */ - static parseIssueUrl(issueUrl: string): { owner: string; repository: string; issueNumber: number } { - const { owner, repository, issueNumber } = /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec(issueUrl)?.groups || {}; + static parseIssueUrl(issueUrl: string): { + owner: string + repository: string + issueNumber: number + } { + const {owner, repository, issueNumber} = + /\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)(?:[/?#]|$)/.exec(issueUrl)?.groups || {} if (!owner || !repository || !issueNumber) { - throw new Error(`Could not parse issue URL: ${issueUrl}`); + throw new Error(`Could not parse issue URL: ${issueUrl}`) } - return { owner, repository, issueNumber: Number(issueNumber) } + return {owner, repository, issueNumber: Number(issueNumber)} } - url: string; - nodeId?: string; + url: string + nodeId?: string get owner(): string { - return Issue.parseIssueUrl(this.url).owner; + return Issue.parseIssueUrl(this.url).owner } get repository(): string { - return Issue.parseIssueUrl(this.url).repository; + return Issue.parseIssueUrl(this.url).repository } get issueNumber(): number { - return Issue.parseIssueUrl(this.url).issueNumber; + return Issue.parseIssueUrl(this.url).issueNumber } constructor({url, nodeId}: IssueInput) { - this.url = url; - this.nodeId = nodeId; + this.url = url + this.nodeId = nodeId } } diff --git a/.github/actions/fix/src/assignIssue.ts b/.github/actions/fix/src/assignIssue.ts index ddd28e2..100fe0f 100644 --- a/.github/actions/fix/src/assignIssue.ts +++ b/.github/actions/fix/src/assignIssue.ts @@ -1,18 +1,15 @@ -import type { Octokit } from "@octokit/core"; -import { Issue } from "./Issue.js"; +import type {Octokit} from '@octokit/core' +import {Issue} from './Issue.js' // https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/use-copilot-agents/coding-agent/assign-copilot-to-an-issue#assigning-an-existing-issue -export async function assignIssue( - octokit: Octokit, - { owner, repository, issueNumber, nodeId }: Issue -) { +export async function assignIssue(octokit: Octokit, {owner, repository, issueNumber, nodeId}: Issue) { // Check whether issues can be assigned to Copilot const suggestedActorsResponse = await octokit.graphql<{ repository: { suggestedActors: { - nodes: { login: string; id: string }[]; - }; - }; + nodes: {login: string; id: string}[] + } + } }>( `query ($owner: String!, $repository: String!) { repository(owner: $owner, name: $repository) { @@ -26,58 +23,49 @@ export async function assignIssue( } } }`, - { owner, repository } - ); - if ( - suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.login !== - "copilot-swe-agent" - ) { - return; + {owner, repository}, + ) + if (suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.login !== 'copilot-swe-agent') { + return } // Get GraphQL identifier for issue (unless already provided) - let issueId = nodeId; + let issueId = nodeId if (!issueId) { - console.debug( - `Fetching identifier for issue ${owner}/${repository}#${issueNumber}` - ); + console.debug(`Fetching identifier for issue ${owner}/${repository}#${issueNumber}`) const issueResponse = await octokit.graphql<{ repository: { - issue: { id: string }; - }; + issue: {id: string} + } }>( `query($owner: String!, $repository: String!, $issueNumber: Int!) { repository(owner: $owner, name: $repository) { issue(number: $issueNumber) { id } } }`, - { owner, repository, issueNumber } - ); - issueId = issueResponse?.repository?.issue?.id; - console.debug( - `Fetched identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}` - ); + {owner, repository, issueNumber}, + ) + issueId = issueResponse?.repository?.issue?.id + console.debug(`Fetched identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`) } else { - console.debug( - `Using provided identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}` - ); + console.debug(`Using provided identifier for issue ${owner}/${repository}#${issueNumber}: ${issueId}`) } if (!issueId) { console.warn( - `Couldn’t get identifier for issue ${owner}/${repository}#${issueNumber}. Skipping assignment to Copilot.` - ); - return; + `Couldn’t get identifier for issue ${owner}/${repository}#${issueNumber}. Skipping assignment to Copilot.`, + ) + return } // Assign issue to Copilot await octokit.graphql<{ replaceActorsForAssignable: { assignable: { - id: string; - title: string; + id: string + title: string assignees: { - nodes: { login: string }[]; - }; - }; - }; + nodes: {login: string}[] + } + } + } }>( `mutation($issueId: ID!, $assigneeId: ID!) { replaceActorsForAssignable(input: {assignableId: $issueId, actorIds: [$assigneeId]}) { @@ -96,8 +84,7 @@ export async function assignIssue( }`, { issueId, - assigneeId: - suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.id, - } - ); + assigneeId: suggestedActorsResponse?.repository?.suggestedActors?.nodes[0]?.id, + }, + ) } diff --git a/.github/actions/fix/src/getLinkedPR.ts b/.github/actions/fix/src/getLinkedPR.ts index 5a5f6c3..f1b0796 100644 --- a/.github/actions/fix/src/getLinkedPR.ts +++ b/.github/actions/fix/src/getLinkedPR.ts @@ -1,22 +1,19 @@ -import type { Octokit } from "@octokit/core"; -import { Issue } from "./Issue.js"; +import type {Octokit} from '@octokit/core' +import {Issue} from './Issue.js' -export async function getLinkedPR( - octokit: Octokit, - { owner, repository, issueNumber }: Issue -) { +export async function getLinkedPR(octokit: Octokit, {owner, repository, issueNumber}: Issue) { // Check whether issues can be assigned to Copilot const response = await octokit.graphql<{ repository?: { issue?: { timelineItems?: { nodes: ( - | { source: { id: string; url: string; title: string } } - | { subject: { id: string; url: string; title: string } } - )[]; - }; - }; - }; + | {source: {id: string; url: string; title: string}} + | {subject: {id: string; url: string; title: string}} + )[] + } + } + } }>( `query($owner: String!, $repository: String!, $issueNumber: Int!) { repository(owner: $owner, name: $repository) { @@ -30,16 +27,15 @@ export async function getLinkedPR( } } }`, - { owner, repository, issueNumber } - ); - const timelineNodes = response?.repository?.issue?.timelineItems?.nodes || []; - const pullRequest: { id: string; url: string; title: string } | undefined = - timelineNodes - .map((node) => { - if ("source" in node && node.source?.url) return node.source; - if ("subject" in node && node.subject?.url) return node.subject; - return undefined; - }) - .find((pr) => !!pr); - return pullRequest; + {owner, repository, issueNumber}, + ) + const timelineNodes = response?.repository?.issue?.timelineItems?.nodes || [] + const pullRequest: {id: string; url: string; title: string} | undefined = timelineNodes + .map(node => { + if ('source' in node && node.source?.url) return node.source + if ('subject' in node && node.subject?.url) return node.subject + return undefined + }) + .find(pr => !!pr) + return pullRequest } diff --git a/.github/actions/fix/src/index.ts b/.github/actions/fix/src/index.ts index 6899f06..2d4a815 100644 --- a/.github/actions/fix/src/index.ts +++ b/.github/actions/fix/src/index.ts @@ -1,76 +1,62 @@ -import type { Issue as IssueInput, Fixing } from "./types.d.js"; -import process from "node:process"; -import core from "@actions/core"; -import { Octokit } from "@octokit/core"; -import { throttling } from "@octokit/plugin-throttling"; -import { assignIssue } from "./assignIssue.js"; -import { getLinkedPR } from "./getLinkedPR.js"; -import { retry } from "./retry.js"; -import { Issue } from "./Issue.js"; -const OctokitWithThrottling = Octokit.plugin(throttling); +import type {Issue as IssueInput, Fixing} from './types.d.js' +import process from 'node:process' +import core from '@actions/core' +import {Octokit} from '@octokit/core' +import {throttling} from '@octokit/plugin-throttling' +import {assignIssue} from './assignIssue.js' +import {getLinkedPR} from './getLinkedPR.js' +import {retry} from './retry.js' +import {Issue} from './Issue.js' +const OctokitWithThrottling = Octokit.plugin(throttling) export default async function () { - core.info("Started 'fix' action"); - const issues: IssueInput[] = JSON.parse( - core.getInput("issues", { required: true }) || "[]" - ); - const repoWithOwner = core.getInput("repository", { required: true }); - const token = core.getInput("token", { required: true }); - core.debug(`Input: 'issues: ${JSON.stringify(issues)}'`); - core.debug(`Input: 'repository: ${repoWithOwner}'`); + core.info("Started 'fix' action") + const issues: IssueInput[] = JSON.parse(core.getInput('issues', {required: true}) || '[]') + const repoWithOwner = core.getInput('repository', {required: true}) + const token = core.getInput('token', {required: true}) + core.debug(`Input: 'issues: ${JSON.stringify(issues)}'`) + core.debug(`Input: 'repository: ${repoWithOwner}'`) const octokit = new OctokitWithThrottling({ auth: token, throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Request quota exhausted for request ${options.method} ${options.url}` - ); + octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn( - `Secondary rate limit hit for request ${options.method} ${options.url}` - ); + octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, }, - }); - const fixings: Fixing[] = issues.map((issue) => ({ issue })) as Fixing[]; + }) + const fixings: Fixing[] = issues.map(issue => ({issue})) as Fixing[] for (const fixing of fixings) { try { - const issue = new Issue(fixing.issue); - await assignIssue(octokit, issue); - core.info( - `Assigned ${issue.owner}/${issue.repository}#${issue.issueNumber} to Copilot!` - ); - const pullRequest = await retry(() => getLinkedPR(octokit, issue)); + const issue = new Issue(fixing.issue) + await assignIssue(octokit, issue) + core.info(`Assigned ${issue.owner}/${issue.repository}#${issue.issueNumber} to Copilot!`) + const pullRequest = await retry(() => getLinkedPR(octokit, issue)) if (pullRequest) { - fixing.pullRequest = pullRequest; - core.info( - `Found linked PR for ${issue.owner}/${issue.repository}#${issue.issueNumber}: ${pullRequest.url}` - ); + fixing.pullRequest = pullRequest + core.info(`Found linked PR for ${issue.owner}/${issue.repository}#${issue.issueNumber}: ${pullRequest.url}`) } else { - core.info( - `No linked PR was found for ${issue.owner}/${issue.repository}#${issue.issueNumber}` - ); + core.info(`No linked PR was found for ${issue.owner}/${issue.repository}#${issue.issueNumber}`) } } catch (error) { - core.setFailed( - `Failed to assign ${fixing.issue.url} to Copilot: ${error}` - ); - process.exit(1); + core.setFailed(`Failed to assign ${fixing.issue.url} to Copilot: ${error}`) + process.exit(1) } } - core.setOutput("fixings", JSON.stringify(fixings)); - core.debug(`Output: 'fixings: ${JSON.stringify(fixings)}'`); - core.info("Finished 'fix' action"); + core.setOutput('fixings', JSON.stringify(fixings)) + core.debug(`Output: 'fixings: ${JSON.stringify(fixings)}'`) + core.info("Finished 'fix' action") } diff --git a/.github/actions/fix/src/retry.ts b/.github/actions/fix/src/retry.ts index c50ce01..5630e4d 100644 --- a/.github/actions/fix/src/retry.ts +++ b/.github/actions/fix/src/retry.ts @@ -3,7 +3,7 @@ * @param ms Time to sleep, in milliseconds. */ function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(() => resolve(), ms)); + return new Promise(resolve => setTimeout(() => resolve(), ms)) } /** @@ -18,15 +18,15 @@ export async function retry( fn: () => Promise | T | null | undefined, maxAttempts = 6, baseDelay = 2000, - attempt = 1 + attempt = 1, ): Promise { - const value = await fn(); - if (value != null) return value; - if (attempt >= maxAttempts) return undefined; + const value = await fn() + if (value != null) return value + if (attempt >= maxAttempts) return undefined /** Exponential backoff, capped at 30s */ - const delay = Math.min(30000, baseDelay * 2 ** (attempt - 1)); + const delay = Math.min(30000, baseDelay * 2 ** (attempt - 1)) /** ±10% jitter */ - const jitter = 1 + (Math.random() - 0.5) * 0.2; - await sleep(Math.round(delay * jitter)); - return retry(fn, maxAttempts, baseDelay, attempt + 1); + const jitter = 1 + (Math.random() - 0.5) * 0.2 + await sleep(Math.round(delay * jitter)) + return retry(fn, maxAttempts, baseDelay, attempt + 1) } diff --git a/.github/actions/fix/src/types.d.ts b/.github/actions/fix/src/types.d.ts index 4886425..bd94d2c 100644 --- a/.github/actions/fix/src/types.d.ts +++ b/.github/actions/fix/src/types.d.ts @@ -1,14 +1,14 @@ export type Issue = { - url: string; - nodeId?: string; -}; + url: string + nodeId?: string +} export type PullRequest = { - url: string; - nodeId?: string; -}; + url: string + nodeId?: string +} export type Fixing = { - issue: Issue; - pullRequest: PullRequest; -}; + issue: Issue + pullRequest: PullRequest +} diff --git a/.github/actions/gh-cache/delete/action.yml b/.github/actions/gh-cache/delete/action.yml index 4bea795..1f0242a 100644 --- a/.github/actions/gh-cache/delete/action.yml +++ b/.github/actions/gh-cache/delete/action.yml @@ -60,20 +60,23 @@ runs: echo "Created new orphaned 'gh-cache' branch" fi - - name: Copy file to repo + - name: Delete file from repo shell: bash run: | - if [ -f "${{ inputs.path }}" ]; then + if [ -e "${{ inputs.path }}" ]; then rm -Rf "${{ inputs.path }}" rm -Rf ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" echo "Deleted '${{ inputs.path }}' from 'gh-cache' branch" + else + echo "'${{ inputs.path }}' does not exist in 'gh-cache' branch" + echo "Skipping delete" fi - name: Commit and push shell: bash working-directory: .gh-cache-${{ github.run_id }} run: | - git add "${{ inputs.path }}" || true + git add --all -- "${{ inputs.path }}" || true git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "$(printf 'Delete artifact: %s' "${{ inputs.path }}")" \ diff --git a/.github/actions/gh-cache/save/action.yml b/.github/actions/gh-cache/save/action.yml index 9ecc2c9..ee5df6f 100644 --- a/.github/actions/gh-cache/save/action.yml +++ b/.github/actions/gh-cache/save/action.yml @@ -60,21 +60,36 @@ runs: echo "Created new orphaned 'gh-cache' branch" fi - - name: Copy file to repo + - name: Copy file or directory to repo shell: bash run: | - if [ -f "${{ inputs.path }}" ]; then - mkdir -p ".gh-cache-${{ github.run_id }}/$(dirname "${{ inputs.path }}")" - cp -Rf "${{ inputs.path }}" ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" - echo "Copied '${{ inputs.path }}' to 'gh-cache' branch" + if [ -e "${{ inputs.path }}" ]; then + if [ -d "${{ inputs.path }}" ]; then + # For directories, copy contents to avoid nesting + mkdir -p ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" + # Check if directory has files before copying + if [ "$(ls -A "${{ inputs.path }}" 2>/dev/null)" ]; then + cp -Rf "${{ inputs.path }}"/* ".gh-cache-${{ github.run_id }}/${{ inputs.path }}"/ + echo "Copied directory contents of '${{ inputs.path }}' to 'gh-cache' branch" + else + echo "Directory '${{ inputs.path }}' is empty, nothing to copy" + fi + else + # For files, copy file to path + mkdir -p ".gh-cache-${{ github.run_id }}/$(dirname "${{ inputs.path }}")" + cp -Rf "${{ inputs.path }}" ".gh-cache-${{ github.run_id }}/${{ inputs.path }}" + echo "Copied file '${{ inputs.path }}' to 'gh-cache' branch" + fi + else + echo "'${{ inputs.path }}' does not exist" fi - name: Commit and push shell: bash working-directory: .gh-cache-${{ github.run_id }} run: | - if [ -f "${{ inputs.path }}" ]; then - git add "${{ inputs.path }}" + if [ -e "${{ inputs.path }}" ]; then + git add -- "${{ inputs.path }}" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git commit -m "$(printf 'Save artifact: %s' "${{ inputs.path }}")" \ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..48ce563 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,41 @@ +name: Lint & Format +on: + pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Format check + run: npm run format:check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9fa8a72..60874c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Ruby - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b + uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd with: ruby-version: "3.4" bundler-cache: true diff --git a/README.md b/README.md index 62c3cf8..3c9cebc 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ jobs: # password: ${{ secrets.PASSWORD }} # Optional: Password for authentication (use secrets!) # auth_context: # Optional: Stringified JSON object for complex authentication # skip_copilot_assignment: false # Optional: Set to true to skip assigning issues to GitHub Copilot (or if you don't have GitHub Copilot) + # include_screenshots: false # Optional: Set to true to capture screenshots and include links to them in filed issues ``` > 👉 Update all `REPLACE_THIS` placeholders with your actual values. See [Action Inputs](#action-inputs) for details. @@ -113,6 +114,7 @@ Trigger the workflow manually or automatically based on your configuration. The | `password` | No | If scanned pages require authentication, the password to use for login | `${{ secrets.PASSWORD }}` | | `auth_context` | No | If scanned pages require authentication, a stringified JSON object containing username, password, cookies, and/or localStorage from an authenticated session | `{"username":"some-user","password":"***","cookies":[...]}` | | `skip_copilot_assignment` | No | Whether to skip assigning filed issues to GitHub Copilot. Set to `true` if you don't have GitHub Copilot or prefer to handle issues manually | `true` | +| `include_screenshots` | No | Whether to capture screenshots of scanned pages and include links to them in filed issues. Screenshots are stored on the `gh-cache` branch of the repository running the workflow. Default: `false` | `true` | --- diff --git a/action.yml b/action.yml index 3e3ee60..932bc27 100644 --- a/action.yml +++ b/action.yml @@ -31,6 +31,10 @@ inputs: description: "Whether to skip assigning filed issues to Copilot" required: false default: "false" + include_screenshots: + description: "Whether to capture screenshots and include links to them in the issue" + required: false + default: "false" outputs: results: @@ -80,6 +84,7 @@ runs: with: urls: ${{ inputs.urls }} auth_context: ${{ inputs.auth_context || steps.auth.outputs.auth_context }} + include_screenshots: ${{ inputs.include_screenshots }} - name: File id: file uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/file @@ -88,6 +93,7 @@ runs: repository: ${{ inputs.repository }} token: ${{ inputs.token }} cached_filings: ${{ steps.normalize_cache.outputs.value }} + screenshot_repository: ${{ github.repository }} - if: ${{ steps.file.outputs.filings }} name: Get issues from filings id: get_issues_from_filings @@ -125,6 +131,12 @@ runs: } core.setOutput('results', JSON.stringify(results)); core.debug(`Results: ${JSON.stringify(results)}`); + - if: ${{ inputs.include_screenshots == 'true' }} + name: Save screenshots + uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/save + with: + path: .screenshots + token: ${{ inputs.token }} - name: Save cached results uses: ./../../_actions/github/accessibility-scanner/current/.github/actions/gh-cache/cache with: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6a0c2ec --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +import tseslint from "typescript-eslint"; +import prettierConfig from "eslint-config-prettier"; + +export default tseslint.config(...tseslint.configs.recommended, prettierConfig, { + files: ["**/*.ts"], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + }, +}); diff --git a/package-lock.json b/package-lock.json index 423256e..7aab2de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,50 +9,55 @@ "version": "0.0.0-development", "license": "MIT", "devDependencies": { - "@actions/core": "^2.0.1", + "@actions/core": "^3.0.0", + "@github/prettier-config": "^0.0.6", "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", - "@types/node": "^25.0.3", - "vitest": "^4.0.16" + "@types/node": "^25.2.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.1", + "typescript-eslint": "^8.56.0", + "vitest": "^4.0.18" } }, "node_modules/@actions/core": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz", - "integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", "dev": true, "license": "MIT", "dependencies": { - "@actions/exec": "^2.0.0", - "@actions/http-client": "^3.0.0" + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" } }, "node_modules/@actions/exec": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz", - "integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", "dev": true, "license": "MIT", "dependencies": { - "@actions/io": "^2.0.0" + "@actions/io": "^3.0.2" } }, "node_modules/@actions/http-client": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz", - "integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", "dev": true, "license": "MIT", "dependencies": { "tunnel": "^0.0.6", - "undici": "^5.28.5" + "undici": "^6.23.0" } }, "node_modules/@actions/io": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz", - "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", "dev": true, "license": "MIT" }, @@ -498,14 +503,170 @@ "node": ">=18" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=14" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz", + "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.1", + "debug": "^4.3.1", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz", + "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@github/prettier-config": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@github/prettier-config/-/prettier-config-0.0.6.tgz", + "integrity": "sha512-Sdb089z+QbGnFF2NivbDeaJ62ooPlD31wE6Fkb/ESjAOXSjNJo+gjqzYYhlM7G3ERJmKFZRUJYMlsqB7Tym8lQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -639,9 +800,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -653,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -667,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -681,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -695,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -709,9 +870,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -723,9 +884,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -737,9 +898,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -751,9 +912,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -765,9 +926,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -779,9 +940,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "cpu": [ "loong64" ], @@ -793,9 +968,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -807,9 +996,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -821,9 +1010,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -835,9 +1024,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -849,9 +1038,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -863,9 +1052,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -876,10 +1065,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "cpu": [ "arm64" ], @@ -891,9 +1094,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -905,9 +1108,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -919,9 +1122,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -933,9 +1136,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -971,6 +1174,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -978,10 +1188,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", + "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", "dev": true, "license": "MIT", "peer": true, @@ -989,17 +1206,281 @@ "undici-types": "~7.16.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitest/expect": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", - "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, @@ -1008,13 +1489,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", - "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.16", + "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1035,9 +1516,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", - "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { @@ -1048,13 +1529,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", - "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.16", + "@vitest/utils": "4.0.18", "pathe": "^2.0.3" }, "funding": { @@ -1062,13 +1543,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", - "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.16", + "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1077,9 +1558,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", - "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, "license": "MIT", "funding": { @@ -1087,29 +1568,80 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", - "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.16", + "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "engines": { + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { "node": ">=12" } }, + "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/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -1124,16 +1656,69 @@ "dev": true, "license": "MIT" }, + "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/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1183,6 +1768,178 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz", + "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.0", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.0", + "eslint-visitor-keys": "^5.0.0", + "espree": "^11.1.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.1.1", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", + "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", + "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1193,6 +1950,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -1220,6 +1987,27 @@ ], "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1238,6 +2026,57 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1253,6 +2092,130 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1263,6 +2226,29 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1282,6 +2268,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -1293,6 +2286,76 @@ ], "license": "MIT" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1350,10 +2413,46 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -1367,31 +2466,70 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1467,6 +2605,19 @@ "node": ">=14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -1477,17 +2628,66 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, - "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { - "@fastify/busboy": "^2.0.0" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">=14.0" + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" } }, "node_modules/undici-types": { @@ -1504,10 +2704,20 @@ "dev": true, "license": "ISC" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "peer": true, @@ -1581,19 +2791,19 @@ } }, "node_modules/vitest": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", - "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.16", - "@vitest/mocker": "4.0.16", - "@vitest/pretty-format": "4.0.16", - "@vitest/runner": "4.0.16", - "@vitest/snapshot": "4.0.16", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", @@ -1621,10 +2831,10 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.16", - "@vitest/browser-preview": "4.0.16", - "@vitest/browser-webdriverio": "4.0.16", - "@vitest/ui": "4.0.16", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, @@ -1658,6 +2868,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1674,6 +2900,29 @@ "engines": { "node": ">=8" } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index ce59fd6..3d9a1e5 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "version": "0.0.0-development", "description": "Finds potential accessibility gaps, files GitHub issues to track them, and attempts to fix them with Copilot", "scripts": { - "test": "vitest run tests/*.test.ts" + "test": "vitest run tests/*.test.ts .github/actions/**/tests/*.test.ts", + "lint": "eslint '.github/actions/**/src/**/*.ts' 'tests/**/*.ts' '.github/actions/**/tests/**/*.ts'", + "format": "prettier --write '.github/actions/**/src/**/*.ts' 'tests/**/*.ts' '.github/actions/**/tests/**/*.ts'", + "format:check": "prettier --check '.github/actions/**/src/**/*.ts' 'tests/**/*.ts' '.github/actions/**/tests/**/*.ts'" }, "repository": { "type": "git", @@ -15,12 +18,19 @@ "url": "https://github.com/github/accessibility-scanner/issues" }, "homepage": "https://github.com/github/accessibility-scanner#readme", + "type": "module", + "prettier": "@github/prettier-config", "devDependencies": { - "@actions/core": "^2.0.1", + "@actions/core": "^3.0.0", + "@github/prettier-config": "^0.0.6", "@octokit/core": "^7.0.6", "@octokit/plugin-throttling": "^11.0.3", "@octokit/types": "^16.0.0", - "@types/node": "^25.0.3", - "vitest": "^4.0.16" + "@types/node": "^25.2.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.8.1", + "typescript-eslint": "^8.56.0", + "vitest": "^4.0.18" } } diff --git a/sites/site-with-errors/Gemfile b/sites/site-with-errors/Gemfile index 289d1ab..40ff7b5 100644 --- a/sites/site-with-errors/Gemfile +++ b/sites/site-with-errors/Gemfile @@ -34,4 +34,4 @@ gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] # Web server gem "rack", "~> 3.2" -gem "puma", "~> 7.1" \ No newline at end of file +gem "puma", "~> 7.2" \ No newline at end of file diff --git a/sites/site-with-errors/Gemfile.lock b/sites/site-with-errors/Gemfile.lock index 0a89ba5..a391568 100644 --- a/sites/site-with-errors/Gemfile.lock +++ b/sites/site-with-errors/Gemfile.lock @@ -95,13 +95,13 @@ GEM jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - nio4r (2.7.4) + nio4r (2.7.5) pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (6.0.2) - puma (7.1.0) + puma (7.2.0) nio4r (~> 2.0) - rack (3.2.4) + rack (3.2.5) rake (13.3.0) rb-fsevent (0.11.2) rb-inotify (0.11.1) @@ -171,7 +171,7 @@ DEPENDENCIES jekyll (~> 4.4.1) jekyll-feed (~> 0.12) minima (~> 2.5) - puma (~> 7.1) + puma (~> 7.2) rack (~> 3.2) tzinfo (>= 1, < 3) tzinfo-data diff --git a/tests/site-with-errors.test.ts b/tests/site-with-errors.test.ts index f670b4e..5ce4696 100644 --- a/tests/site-with-errors.test.ts +++ b/tests/site-with-errors.test.ts @@ -1,171 +1,192 @@ -import type { Endpoints } from "@octokit/types" -import type { Result } from "./types.d.js"; -import fs from "node:fs"; -import { describe, it, expect, beforeAll } from "vitest"; -import { Octokit } from "@octokit/core"; -import { throttling } from "@octokit/plugin-throttling"; -const OctokitWithThrottling = Octokit.plugin(throttling); +import type {Endpoints} from '@octokit/types' +import type {Result} from './types.d.js' +import fs from 'node:fs' +import {describe, it, expect, beforeAll} from 'vitest' +import {Octokit} from '@octokit/core' +import {throttling} from '@octokit/plugin-throttling' +const OctokitWithThrottling = Octokit.plugin(throttling) -describe("site-with-errors", () => { - let results: Result[]; +describe('site-with-errors', () => { + let results: Result[] beforeAll(() => { - expect(process.env.CACHE_PATH).toBeDefined(); - expect(fs.existsSync(process.env.CACHE_PATH!)).toBe(true); - results = JSON.parse(fs.readFileSync(process.env.CACHE_PATH!, "utf-8")); - }); + expect(process.env.CACHE_PATH).toBeDefined() + expect(fs.existsSync(process.env.CACHE_PATH!)).toBe(true) + results = JSON.parse(fs.readFileSync(process.env.CACHE_PATH!, 'utf-8')) + }) - it("cache has expected results", () => { - const actual = results.map(({ issue: { url: issueUrl }, pullRequest: { url: pullRequestUrl }, findings }) => { - const { problemUrl, solutionLong, ...finding } = findings[0]; + it('cache has expected results', () => { + const actual = results.map(({issue: {url: issueUrl}, pullRequest: {url: pullRequestUrl}, findings}) => { + const {problemUrl, solutionLong, screenshotId, ...finding} = findings[0] // Check volatile fields for existence only - expect(issueUrl).toBeDefined(); - expect(pullRequestUrl).toBeDefined(); - expect(problemUrl).toBeDefined(); - expect(solutionLong).toBeDefined(); + expect(issueUrl).toBeDefined() + expect(pullRequestUrl).toBeDefined() + expect(problemUrl).toBeDefined() + expect(solutionLong).toBeDefined() // Check `problemUrl`, ignoring axe version - expect(problemUrl.startsWith("https://dequeuniversity.com/rules/axe/")).toBe(true); - expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true); - return finding; - }); + expect(problemUrl.startsWith('https://dequeuniversity.com/rules/axe/')).toBe(true) + expect(problemUrl.endsWith(`/${finding.ruleId}?application=playwright`)).toBe(true) + // screenshotId is only present when include_screenshots is enabled + if (screenshotId !== undefined) { + expect(screenshotId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) + } + return finding + }) const expected = [ { - scannerType: "axe", - url: "http://127.0.0.1:4000/", + scannerType: 'axe', + url: 'http://127.0.0.1:4000/', html: '', - problemShort: "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds" - }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', + solutionShort: + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', + }, + { + scannerType: 'axe', + url: 'http://127.0.0.1:4000/', html: '', - problemShort: "page should contain a level-one heading", - ruleId: "page-has-heading-one", - solutionShort: "ensure that the page, or at least one of its frames contains a level-one heading" - }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html", + problemShort: 'page should contain a level-one heading', + ruleId: 'page-has-heading-one', + solutionShort: 'ensure that the page, or at least one of its frames contains a level-one heading', + }, + { + scannerType: 'axe', + url: 'http://127.0.0.1:4000/jekyll/update/2025/07/30/welcome-to-jekyll.html', html: ``, - problemShort: "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/about/", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', + solutionShort: + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', + }, + { + scannerType: 'axe', + url: 'http://127.0.0.1:4000/about/', html: 'jekyllrb.com', - problemShort: "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds", - }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/404.html", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', + solutionShort: + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', + }, + { + scannerType: 'axe', + url: 'http://127.0.0.1:4000/404.html', html: '
  • Accessibility Scanner Demo
  • ', - problemShort: "elements must meet minimum color contrast ratio thresholds", - ruleId: "color-contrast", - solutionShort: "ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds" - }, { - scannerType: "axe", - url: "http://127.0.0.1:4000/404.html", + problemShort: 'elements must meet minimum color contrast ratio thresholds', + ruleId: 'color-contrast', + solutionShort: + 'ensure the contrast between foreground and background colors meets wcag 2 aa minimum contrast ratio thresholds', + }, + { + scannerType: 'axe', + url: 'http://127.0.0.1:4000/404.html', html: '

    ', - problemShort: "headings should not be empty", - ruleId: "empty-heading", - solutionShort: "ensure headings have discernible text", + problemShort: 'headings should not be empty', + ruleId: 'empty-heading', + solutionShort: 'ensure headings have discernible text', }, - ]; + ] // Check that: // - every expected object exists (no more and no fewer), and // - each object has all fields, and // - field values match expectations exactly // A specific order is _not_ enforced. - expect(actual).toHaveLength(expected.length); - expect(actual).toEqual(expect.arrayContaining(expected)); - }); + expect(actual).toHaveLength(expected.length) + expect(actual).toEqual(expect.arrayContaining(expected)) + }) - it("GITHUB_TOKEN environment variable is set", () => { - expect(process.env.GITHUB_TOKEN).toBeDefined(); - }); + it('GITHUB_TOKEN environment variable is set', () => { + expect(process.env.GITHUB_TOKEN).toBeDefined() + }) - describe.runIf(!!process.env.GITHUB_TOKEN)("—", () => { - let octokit: Octokit; - let issues: Endpoints["GET /repos/{owner}/{repo}/issues/{issue_number}"]["response"]["data"][]; - let pullRequests: Endpoints["GET /repos/{owner}/{repo}/pulls/{pull_number}"]["response"]["data"][]; + describe.runIf(!!process.env.GITHUB_TOKEN)('—', () => { + let octokit: Octokit + let issues: Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}']['response']['data'][] + let pullRequests: Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}']['response']['data'][] beforeAll(async () => { octokit = new OctokitWithThrottling({ auth: process.env.GITHUB_TOKEN, throttle: { onRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`); + octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { - octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`); + octokit.log.warn(`Secondary rate limit hit for request ${options.method} ${options.url}`) if (retryCount < 3) { - octokit.log.info(`Retrying after ${retryAfter} seconds!`); - return true; + octokit.log.info(`Retrying after ${retryAfter} seconds!`) + return true } }, - } - }); + }, + }) // Fetch issues referenced in the cache file - issues = await Promise.all(results.map(async ({ issue: { url: issueUrl } }) => { - expect(issueUrl).toBeDefined(); - const { owner, repo, issueNumber } = - /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/.exec(issueUrl!)!.groups!; - const {data: issue} = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", { - owner, - repo, - issue_number: parseInt(issueNumber, 10) - }); - expect(issue).toBeDefined(); - return issue; - })); + issues = await Promise.all( + results.map(async ({issue: {url: issueUrl}}) => { + expect(issueUrl).toBeDefined() + const {owner, repo, issueNumber} = + /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/.exec( + issueUrl!, + )!.groups! + const {data: issue} = await octokit.request('GET /repos/{owner}/{repo}/issues/{issue_number}', { + owner, + repo, + issue_number: parseInt(issueNumber, 10), + }) + expect(issue).toBeDefined() + return issue + }), + ) // Fetch pull requests referenced in the findings file - pullRequests = await Promise.all(results.map(async ({ pullRequest: { url: pullRequestUrl } }) => { - expect(pullRequestUrl).toBeDefined(); - const { owner, repo, pullNumber } = - /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/pull\/(?\d+)/.exec(pullRequestUrl!)!.groups!; - const {data: pullRequest} = await octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", { - owner, - repo, - pull_number: parseInt(pullNumber, 10) - }); - expect(pullRequest).toBeDefined(); - return pullRequest; - })); - }); + pullRequests = await Promise.all( + results.map(async ({pullRequest: {url: pullRequestUrl}}) => { + expect(pullRequestUrl).toBeDefined() + const {owner, repo, pullNumber} = + /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/pull\/(?\d+)/.exec( + pullRequestUrl!, + )!.groups! + const {data: pullRequest} = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', { + owner, + repo, + pull_number: parseInt(pullNumber, 10), + }) + expect(pullRequest).toBeDefined() + return pullRequest + }), + ) + }) - it("issues exist and have expected title, state, and assignee", async () => { - const actualTitles = issues.map(({ title }) => (title)); + it('issues exist and have expected title, state, and assignee', async () => { + const actualTitles = issues.map(({title}) => title) const expectedTitles = [ - "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /", - "Accessibility issue: Page should contain a level-one heading on /", - "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /404.html", - "Accessibility issue: Headings should not be empty on /404.html", - "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /about/", - "Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /jekyll/update/2025/07/30/welcome-to-jekyll.html", - ]; - expect(actualTitles).toHaveLength(expectedTitles.length); - expect(actualTitles).toEqual(expect.arrayContaining(expectedTitles)); + 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /', + 'Accessibility issue: Page should contain a level-one heading on /', + 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /404.html', + 'Accessibility issue: Headings should not be empty on /404.html', + 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /about/', + 'Accessibility issue: Elements must meet minimum color contrast ratio thresholds on /jekyll/update/2025/07/30/welcome-to-jekyll.html', + ] + expect(actualTitles).toHaveLength(expectedTitles.length) + expect(actualTitles).toEqual(expect.arrayContaining(expectedTitles)) for (const issue of issues) { - expect(issue.state).toBe("open"); - expect(issue.assignees).toBeDefined(); - expect(issue.assignees!.some(a => a.login === "Copilot")).toBe(true); + expect(issue.state).toBe('open') + expect(issue.assignees).toBeDefined() + expect(issue.assignees!.some(a => a.login === 'Copilot')).toBe(true) } - }); + }) - it("pull requests exist and have expected author, state, and assignee", async () => { + it('pull requests exist and have expected author, state, and assignee', async () => { for (const pullRequest of pullRequests) { - expect(pullRequest.user.login).toBe("Copilot"); - expect(pullRequest.state).toBe("open"); - expect(pullRequest.assignees).toBeDefined(); - expect(pullRequest.assignees!.some(a => a.login === "Copilot")).toBe(true); + expect(pullRequest.user.login).toBe('Copilot') + expect(pullRequest.state).toBe('open') + expect(pullRequest.assignees).toBeDefined() + expect(pullRequest.assignees!.some(a => a.login === 'Copilot')).toBe(true) } - }); - }); -}); + }) + }) +}) diff --git a/tests/types.d.ts b/tests/types.d.ts index 684ab0e..cc2c15e 100644 --- a/tests/types.d.ts +++ b/tests/types.d.ts @@ -1,29 +1,30 @@ export type Finding = { - scannerType: string; - ruleId: string; - url: string; - html: string; - problemShort: string; - problemUrl: string; - solutionShort: string; - solutionLong?: string; -}; + scannerType: string + ruleId: string + url: string + html: string + problemShort: string + problemUrl: string + solutionShort: string + solutionLong?: string + screenshotId?: string +} export type Issue = { - id: number; - nodeId: string; - url: string; - title: string; - state?: "open" | "reopened" | "closed"; -}; + id: number + nodeId: string + url: string + title: string + state?: 'open' | 'reopened' | 'closed' +} export type PullRequest = { - url: string; - nodeId: string; -}; + url: string + nodeId: string +} export type Result = { - findings: Finding[]; - issue: Issue; - pullRequest: PullRequest; -}; + findings: Finding[] + issue: Issue + pullRequest: PullRequest +}