Skip to content

Vendor axe-core, drop the Skypack CDN dependency#14677

Open
cwickham wants to merge 4 commits into
mainfrom
axe/vendor-axe-core
Open

Vendor axe-core, drop the Skypack CDN dependency#14677
cwickham wants to merge 4 commits into
mainfrom
axe/vendor-axe-core

Conversation

@cwickham

@cwickham cwickham commented Jul 13, 2026

Copy link
Copy Markdown
Member

Description

Vendors axe-core as a bundled HTML resource, so the axe: accessibility option stops loading it from the Skypack CDN in the reader's browser.

What changes

  • configuration pins AXE_JS=4.10.3 — exactly the version the CDN import already pinned, so this PR changes where axe comes from, not which axe runs. Scan results are identical. A follow-up PR will bump to current axe-core (4.12.1), keeping "different source" and "different results" in separate diffs.
  • update-html-dependencies.ts downloads axe.min.js plus axe-core's LICENSE (MPL-2.0) and LICENSE-3RD-PARTY.txt from the npm package via unpkg — https://unpkg.com/axe-core@4.10.3/<file>, the same updateUnpkgDependency helper used for anchor-js, popper, and tippy — into src/resources/formats/html/axe/. To verify the committed bytes, curl those three URLs and compare (or rerun quarto-bld update-html-dependencies). The files are vendored byte-for-byte: the /*! banner in axe.min.js carries the Deque copyright/MPL notice that must ship with every copy. Per-directory LICENSE files follow the existing convention (src/resources/formats/pdf/pdfjs/LICENSE).
  • format-html-axe.ts injects axe.min.js as a classic script ahead of the axe-check.js module, so window.axe is set before the checker runs.
  • axe-check.js replaces the pinned Skypack dynamic import with window.axe.

Why

  • Accessibility checking now works offline and no longer depends on Skypack's uptime (previously, no network meant the checker silently never ran).
  • Viewing a rendered document no longer triggers a request to cdn.skypack.dev from every reader's browser.
  • The upcoming quarto axe scanner and the overlay will provably run the same axe version from the same file.

A race this exposed (also fixed here): quarto-dashboard.js keeps <html> hidden until its DOMContentLoaded handler runs, but deferred modules execute before that event — so with the Skypack network latency gone, the first scan ran against a fully hidden document and dashboards reported no violations. axe-check.js now waits for the window load event before the first scan. The race predates this PR (a sufficiently fast CDN response could have triggered it); vendoring just made it deterministic.

Size: ~573 KB committed across three files (541 KB of it axe.min.js; ~145 KB gzipped on the wire). Precedent: pdf.js, MathJax, and reveal.js are vendored the same way.

Out of scope: any behavior change to scanning, report rendering, or options (#14607 standard, sort order, overlay polish, #14668 rescan) — those are follow-up PRs. This PR produces identical scan results, just sourced locally.

Verification

  • Full axe Playwright suite (axe-accessibility.spec.ts, axe-code-line-numbers.spec.ts): 27/27 pass on chromium.
  • Offline check: loaded the html, dashboard, and revealjs reports with all non-local requests aborted — reports render with violations listed; zero axe-related external requests.
  • quarto-bld update-html-dependencies rerun reproduces the committed axe files byte-for-byte.
  • axe-conformance.test.ts unit tests: 9/9 pass.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR
AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

cwickham added 3 commits July 13, 2026 08:47
Pins AXE_JS in configuration and adds a download block to
update-html-dependencies.ts (axe.min.js plus the MPL-2.0 LICENSE and
LICENSE-3RD-PARTY.txt, which must ship alongside it). Files are vendored
byte-for-byte from npm via unpkg; the /*! banner in axe.min.js carries the
required Deque copyright and MPL notice.
Injects axe.min.js as a classic script ahead of axe-check.js (a deferred
module), so window.axe is set before the checker runs. Replaces the pinned
Skypack dynamic import, making the axe overlay work offline and removing
the CDN request from every reader's browser. Same axe version (4.10.3),
identical scan results.

Also waits for the window load event before the first scan. Deferred
modules run before DOMContentLoaded, but the dashboard layout runs on
DOMContentLoaded and keeps <html> hidden until then, so an immediate scan
sees only hidden elements and reports nothing. The Skypack import's
network latency masked this pre-existing race; removing it exposed the
bug (8 dashboard Playwright tests).
@posit-snyk-bot

posit-snyk-bot commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cwickham cwickham marked this pull request as ready for review July 13, 2026 16:59
@cwickham cwickham requested a review from cderv July 13, 2026 16:59
@cscheid

cscheid commented Jul 13, 2026

Copy link
Copy Markdown
Member

The upcoming quarto axe scanner and the overlay will provably run the same axe version from the same file.

This will be a really nice feature to have!

cwickham added a commit that referenced this pull request Jul 13, 2026
The overlay is injected after the page scan, so it never audits itself
and nothing else guards against new violations as the report UI grows.
Scan it with the vendored axe-core build the page already loaded
(window.axe), on the long-report page where the overlay is scrollable.
Requires the vendored axe-core from #14677, which this branch is now
stacked on. Verified the test fails with scrollable-region-focusable
when the tabindex fix is removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants