22 * Axe-core Polyfilled Import
33 *
44 * This file ensures the jsdom polyfill runs BEFORE axe-core is imported.
5- * Due to ES module import hoisting, we must import the polyfill explicitly
6- * at the top of this file, then import axe-core. This guarantees the correct
7- * execution order: polyfill setup → axe-core import.
5+ * Uses dynamic import to avoid ES module hoisting issues.
6+ *
7+ * WHY THIS EXISTS:
8+ * axe-core has side effects on import - it expects global `window` and `document` objects
9+ * to be available when the module is loaded. In Node.js environments, these don't exist
10+ * by default. This polyfill creates a virtual DOM using JSDOM and sets these globals
11+ * before axe-core is imported.
12+ *
13+ * HOW IT WORKS:
14+ * 1. Top-level code sets up JSDOM polyfill (runs immediately)
15+ * 2. Axe-core is imported dynamically (runs after polyfill)
16+ * 3. Module exports a promise that resolves to axe-core
817 *
918 * IMPORT CHAIN:
10- * 1. jsdom.polyfill.ts (sets globalThis.window and globalThis.document)
11- * 2. This file (imports polyfill, then imports axe-core)
12- * 3. safe-axe-core-import.ts (re-exports for clean imports)
19+ * 1. This file (sets up polyfill, then dynamically imports axe-core)
20+ * 2. safe-axe-core-import.ts (re-exports for clean imports)
1321 *
1422 * USAGE:
1523 * Do NOT import from this file directly. Use safe-axe-core-import.ts instead.
24+ *
25+ * @see https://github.com/dequelabs/axe-core/issues/3962
1626 */
17- // Import polyfill FIRST to ensure globals are set before axe-core loads
18- // Now safe to import axe-core - globals exist due to polyfill import above
19- import axe from 'axe-core' ;
20- // eslint-disable-next-line import/no-unassigned-import
21- import './jsdom.polyfill.js' ;
27+ import { JSDOM } from 'jsdom' ;
2228
23- // Re-export axe default and all types used throughout the codebase
24- export default axe ;
29+ // Polyfill setup - runs immediately before any axe-core code
30+ const html = `<!DOCTYPE html>\n<html></html>` ;
31+ const { window : jsdomWindow } = new JSDOM ( html ) ;
2532
33+ // Set globals for axe-core compatibility
34+ // eslint-disable-next-line functional/immutable-data
35+ globalThis . window = jsdomWindow as unknown as Window & typeof globalThis ;
36+ // eslint-disable-next-line functional/immutable-data
37+ globalThis . document = jsdomWindow . document ;
38+
39+ // Dynamic import ensures polyfill runs first
40+ // This cannot be a top-level await, so we export the promise
41+ const axePromise = import ( 'axe-core' ) ;
42+
43+ // Re-export types (these are compile-time only, no runtime impact)
2644export type {
2745 AxeResults ,
2846 NodeResult ,
@@ -32,3 +50,7 @@ export type {
3250 ImpactValue ,
3351 CrossTreeSelector ,
3452} from 'axe-core' ;
53+
54+ // Export the axe instance synchronously by awaiting at the top level
55+ // Top-level await is supported in ES modules
56+ export default ( await axePromise ) . default ;
0 commit comments