Skip to content

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

Merged
cderv merged 5 commits into
mainfrom
axe/vendor-axe-core
Jul 17, 2026
Merged

Vendor axe-core, drop the Skypack CDN dependency#14677
cderv merged 5 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):

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.

@cderv cderv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Some ideas to not forget about quarto-web PR

Comment thread news/changelog-1.10.md Outdated
@cwickham

Copy link
Copy Markdown
Member Author

Good catch, PR for docs at quarto-dev/quarto-web#2106

@cderv

cderv commented Jul 17, 2026

Copy link
Copy Markdown
Member

Cool !

Is this ready to merge ? Do we want this in v1.10 or should we release v1.10 and make this new feature for v1.11 first pre-release ?

@cwickham

Copy link
Copy Markdown
Member Author

@cderv Ready to merge!

Most of the PRs that I have queued up for after this are improving/fixing the axe: output: document overlay. I think these would be nice to land in 1.10. I.e. it would be nice to say these are in the release at conf.

The bigger site audit quarto axe work, is probably better in 1.11. This would be fine in a prerelease by conf.

@cderv
cderv merged commit a90065c into main Jul 17, 2026
51 checks passed
@cderv
cderv deleted the axe/vendor-axe-core branch July 17, 2026 18:30
cwickham added a commit that referenced this pull request Jul 17, 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.
cderv added a commit that referenced this pull request Jul 21, 2026
Recent axe-core changes (#14607, #14655, #14667, #14677, #14680)
reworked the accessibility-report feature repeatedly with no dedicated
regression tests guarding several of its behaviors. A coverage audit
found three gaps; this closes them.

- No test guarded against reintroducing a CDN dependency for axe-core,
  despite #14677 vendoring it specifically to drop the Skypack CDN.
- The report overlay's keyboard-scrollable region (added in #14680)
  was only checked for static tabindex/role attributes, never an
  actual keypress.
- The self-scan pattern (added in #14680) only covered the plain-HTML
  overlay, not the RevealJS report slide or dashboard offcanvas that
  render the same violation markup.

Extending the self-scan to RevealJS and the dashboard surfaced a real
pre-existing issue: both report-chrome regions are scrollable but not
keyboard-focusable once they overflow, the same class of gap already
fixed for the plain-HTML overlay. Filed as #14710, not fixed here —
the dashboard test filters that one known node out of the
scrollable-region-focusable violation (not the whole violation), so
it still catches other regressions in that scope.

Related to #14710
cderv pushed a commit to quarto-dev/quarto-web that referenced this pull request Jul 24, 2026
quarto-dev/quarto-cli#14677 vendors axe-core (MPL-2.0) as a bundled
resource, so it now belongs in the license page's dependency table. The
FAQ's dependency list now points to that table rather than growing its
own copy.
cwickham added a commit to quarto-dev/quarto-web that referenced this pull request Jul 29, 2026
Adds a section on scoping accessibility checks to a WCAG conformance
level (quarto-dev/quarto-cli#14607, quarto-dev/quarto-cli#14682) and
updates the CDN note now that axe-core is bundled with Quarto 1.10
(quarto-dev/quarto-cli#14677).

Also adds fig-alt to the two example screenshots — an axe site audit
of this page flagged them as missing alt text (the bracket text of a
figure becomes the caption, not the alt).
cwickham added a commit to quarto-dev/quarto-web that referenced this pull request Jul 30, 2026
* Document axe standard and best-practice options

Adds a section on scoping accessibility checks to a WCAG conformance
level (quarto-dev/quarto-cli#14607, quarto-dev/quarto-cli#14682) and
updates the CDN note now that axe-core is bundled with Quarto 1.10
(quarto-dev/quarto-cli#14677).

Also adds fig-alt to the two example screenshots — an axe site audit
of this page flagged them as missing alt text (the bracket text of a
figure becomes the caption, not the alt).

* Clarify that choosing a standard never adds deprecated rules

* Update axe overlay fig-alt to describe severity and conformance label

* Add prerelease callout for 1.10 axe options

* Remove prerelease callout shortcode

* Replace introduced-in callout notes with prerelease-callout shortcodes

* Simplify

* Dedent best-practice YAML example left over from removed bullet list

* Reorganize accessibility page around interactive and site-wide checks

- Promote the interactive workflow to its own section and move the
  example up into it, with a new DevTools console screenshot for the
  default axe: true output
- Rename Customization to Output location and attach the existing
  overlay screenshots to output: document
- Reframe Planned work as Site-wide accessibility checks
- Add capture-axe-devtools.mjs (window-ID capture of headed Chromium
  with DevTools open) and a console.qmd example page

* Register manual captures in the screenshot manifest

Adds a manual array to manifest.json for screenshots capture.js can't
produce (currently axe-console, which needs the DevTools panel). The
schema validates entries, --list and list.js show them, and
--name <manual-entry> points at the producing script instead of failing.
cwickham added a commit to quarto-dev/quarto-web that referenced this pull request Jul 30, 2026
* Document axe standard and best-practice options

Adds a section on scoping accessibility checks to a WCAG conformance
level (quarto-dev/quarto-cli#14607, quarto-dev/quarto-cli#14682) and
updates the CDN note now that axe-core is bundled with Quarto 1.10
(quarto-dev/quarto-cli#14677).

Also adds fig-alt to the two example screenshots — an axe site audit
of this page flagged them as missing alt text (the bracket text of a
figure becomes the caption, not the alt).

* Clarify that choosing a standard never adds deprecated rules

* Update axe overlay fig-alt to describe severity and conformance label

* Add prerelease callout for 1.10 axe options

* Remove prerelease callout shortcode

* Replace introduced-in callout notes with prerelease-callout shortcodes

* Simplify

* Dedent best-practice YAML example left over from removed bullet list

* Reorganize accessibility page around interactive and site-wide checks

- Promote the interactive workflow to its own section and move the
  example up into it, with a new DevTools console screenshot for the
  default axe: true output
- Rename Customization to Output location and attach the existing
  overlay screenshots to output: document
- Reframe Planned work as Site-wide accessibility checks
- Add capture-axe-devtools.mjs (window-ID capture of headed Chromium
  with DevTools open) and a console.qmd example page

* Register manual captures in the screenshot manifest

Adds a manual array to manifest.json for screenshots capture.js can't
produce (currently axe-console, which needs the DevTools panel). The
schema validates entries, --list and list.js show them, and
--name <manual-entry> points at the producing script instead of failing.

(cherry picked from commit 6979d02)

Co-authored-by: Charlotte Wickham <charlotte.wickham@posit.co>
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.

4 participants