Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
bbd8648
feat: use jiti for plugin loading
BioPhoton Jan 21, 2026
b2a0154
Merge branch 'refs/heads/main' into feat/jiti-module-loading
BioPhoton Jan 22, 2026
cc442d1
refactor: move loadTargetConfig into utils
BioPhoton Jan 22, 2026
c9cb895
refactor: move importModule into extra file
BioPhoton Jan 22, 2026
aee50d9
refactor: use jiti to load module
BioPhoton Jan 23, 2026
4c75af5
refactor: fix lint
BioPhoton Jan 23, 2026
2f53690
refactor: fix lint
BioPhoton Jan 23, 2026
3428c50
refactor: fix int-test
BioPhoton Jan 23, 2026
e477cbc
refactor: fix unit-test
BioPhoton Jan 23, 2026
81fd996
refactor: fix unit-test
BioPhoton Jan 23, 2026
23ff44d
refactor: fix build
BioPhoton Jan 23, 2026
8447ae2
refactor: fix int-test
BioPhoton Jan 23, 2026
5301aa7
refactor: fix lint
BioPhoton Jan 23, 2026
bf7dd0a
refactor: fix jiti context
BioPhoton Jan 23, 2026
a5319d8
refactor: fix jiti context
BioPhoton Jan 23, 2026
5a67328
refactor: fix int test
BioPhoton Jan 23, 2026
11c493b
refactor: fix e2e test
BioPhoton Jan 23, 2026
1b679b8
refactor: move mocks
BioPhoton Jan 23, 2026
933daf1
refactor: adjust path resolution
BioPhoton Jan 23, 2026
1a51ca9
refactor: fix axe
BioPhoton Jan 23, 2026
51106e9
refactor: fix eslint
BioPhoton Jan 23, 2026
7c3341b
refactor: fix eslint
BioPhoton Jan 23, 2026
03ff1f2
refactor: fix axe
BioPhoton Jan 23, 2026
481b536
refactor: fix import order
BioPhoton Jan 23, 2026
d5125c3
refactor: fix lint
BioPhoton Jan 23, 2026
b7109fb
refactor: add safe axeCore import helper
BioPhoton Jan 23, 2026
ba61c0a
refactor: wip
BioPhoton Jan 23, 2026
1cbe69a
refactor: revert changes
BioPhoton Jan 31, 2026
ce7e644
Merge branch 'main' into feat/jiti-module-loading
BioPhoton Jan 31, 2026
515577c
refactor: add axe-core polyfill
BioPhoton Jan 31, 2026
f0b9c6b
refactor: wip
BioPhoton Jan 31, 2026
85fbcbf
refactor: fix mocking in tests
BioPhoton Jan 31, 2026
c74b5e1
refactor: disable jit cache in tests
BioPhoton Jan 31, 2026
5dbcebe
refactor: fix importModule usage
BioPhoton Jan 31, 2026
a41c71d
refactor: jiti cache test handling
BioPhoton Feb 1, 2026
c42051d
refactor: wip
BioPhoton Feb 1, 2026
44d8e35
refactor: wip
BioPhoton Feb 1, 2026
e951629
refactor: wip
BioPhoton Feb 1, 2026
06f55a1
refactor: wip
BioPhoton Feb 1, 2026
a8e38cb
refactor: wip
BioPhoton Feb 1, 2026
9ea45db
refactor: wip
BioPhoton Feb 1, 2026
9e799f4
refactor: wip
BioPhoton Feb 1, 2026
5da4812
refactor: wip
BioPhoton Feb 1, 2026
196b26d
refactor: add issue repro
BioPhoton Feb 1, 2026
6a90e80
refactor: wip
BioPhoton Feb 1, 2026
98c1439
refactor: wip
BioPhoton Feb 1, 2026
2266a0b
refactor: wip
BioPhoton Feb 1, 2026
8f99f41
refactor: wip
BioPhoton Feb 2, 2026
203cb69
refactor: wip
BioPhoton Feb 2, 2026
d27bc8c
refactor: wip
BioPhoton Feb 2, 2026
560b1e7
refactor: wip
BioPhoton Feb 2, 2026
36aa855
refactor: wip
BioPhoton Feb 2, 2026
d953e35
refactor: wip
BioPhoton Feb 2, 2026
ec537fe
refactor: wip
BioPhoton Feb 2, 2026
2f72c82
refactor: wip
BioPhoton Feb 2, 2026
56c04db
Merge branch 'main' into feat/jiti-module-loading
BioPhoton Feb 8, 2026
28cccef
refactor: wip
BioPhoton Feb 8, 2026
8ca4ab1
refactor: wip
BioPhoton Feb 8, 2026
655c9c2
refactor: wip
BioPhoton Feb 8, 2026
fb078f0
refactor: wip
BioPhoton Feb 8, 2026
52da218
refactor: wip
BioPhoton Feb 8, 2026
7854b62
refactor: wip
BioPhoton Feb 8, 2026
e452315
refactor: wip
BioPhoton Feb 8, 2026
12fd5ca
refactor: wip
BioPhoton Feb 8, 2026
7384905
refactor: wip
BioPhoton Feb 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
/coverage
/.nx
__snapshots__

# Test fixtures with intentional syntax errors
packages/utils/mocks/fixtures/actually-invalid.js
308 changes: 258 additions & 50 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,13 @@
"@types/benchmark": "^2.1.5",
"@types/debug": "^4.1.12",
"@types/eslint": "^8.44.2",
"@types/jsdom": "^27.0.0",
"@types/node": "^22.13.4",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.0",
"@vitejs/plugin-react": "^5.0.0",
"@vitest/coverage-v8": "1.3.1",
"@vitest/eslint-plugin": "^1.1.38",
"@vitest/eslint-plugin": "^1.6.6",
"@vitest/ui": "1.3.1",
"benchmark": "^2.1.4",
"chrome-launcher": "^1.1.2",
Expand All @@ -102,7 +103,7 @@
"husky": "^8.0.0",
"inquirer": "^9.3.7",
"jest-extended": "^6.0.0",
"jiti": "2.4.2",
"jiti": "^2.4.2",
"jsdom": "~24.0.0",
"jsonc-eslint-parser": "^2.4.0",
"knip": "^5.33.3",
Expand Down
56 changes: 56 additions & 0 deletions packages/cli/mocks/core-config-middleware.int-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env node

/**
* Helper script for testing coreConfigMiddleware with tsconfig path resolution.
* This script is executed in a subprocess to test the middleware in isolation.
*
* Usage: tsx core-config-middleware.int-helper.ts <configPath> [tsconfigPath]
*
* Note: Logger output is redirected to stderr to avoid interfering with JSON output on stdout.
*/
import { coreConfigMiddleware } from '../src/lib/implementation/core-config.middleware.js';

// Redirect console.log and process.stdout.write to stderr to prevent logger output
// from interfering with JSON output
const originalLog = console.log;
const originalWrite = process.stdout.write.bind(process.stdout);

console.log = (...args: unknown[]) => {
process.stderr.write(args.join(' ') + '\n');
};

process.stdout.write = ((chunk: any, ...args: any[]): boolean => {
return process.stderr.write(chunk, ...args);
}) as typeof process.stdout.write;

const [configPath, tsconfigPath] = process.argv.slice(2);

if (!configPath) {
console.error('Error: configPath is required');
process.exit(1);
}

try {
const result = await coreConfigMiddleware({
config: configPath,
...(tsconfigPath && { tsconfig: tsconfigPath }),
plugins: [],
onlyPlugins: [],
skipPlugins: [],
});

// Restore original stdout.write before outputting JSON
process.stdout.write = originalWrite;

// Use originalLog to write JSON to stdout
originalLog(
JSON.stringify({
success: true,
config: result.config,
}),
);
process.exit(0);
} catch (error) {
console.error(error);
process.exit(1);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { executeProcess } from '@code-pushup/utils';
import { coreConfigMiddleware } from './core-config.middleware.js';

const configDirPath = path.join(
Expand All @@ -12,7 +13,33 @@ const configDirPath = path.join(
'mocks',
'configs',
);

const helperPath = path.join(
fileURLToPath(path.dirname(import.meta.url)),
'..',
'..',
'..',
'..',
'cli',
'mocks',
'core-config-middleware.int-helper.ts',
);
const runMiddlewareInCwd = async (configPath: string, tsconfigPath?: string) =>
await executeProcess({
command: 'npx',
args: [
'tsx',
helperPath,
configPath,
...(tsconfigPath ? [tsconfigPath] : []),
],
cwd: configDirPath,
env: {
...process.env,
// Disable all logger output to avoid interfering with JSON output
CP_VERBOSE: 'false',
CI: 'false',
},
});
describe('coreConfigMiddleware', () => {
const CLI_DEFAULTS = {
plugins: [],
Expand All @@ -30,15 +57,16 @@ describe('coreConfigMiddleware', () => {
});

it('should load config which relies on provided --tsconfig', async () => {
await expect(
coreConfigMiddleware({
config: path.join(
configDirPath,
'code-pushup.needs-tsconfig.config.ts',
),
tsconfig: path.join(configDirPath, 'tsconfig.json'),
...CLI_DEFAULTS,
}),
).resolves.toBeTruthy();
const { stdout, code } = await runMiddlewareInCwd(
'code-pushup.needs-tsconfig.config.ts',
path.join(configDirPath, 'tsconfig.json'),
);

expect(code).toBe(0);
const output = JSON.parse(stdout);
expect(output).toStrictEqual({
success: true,
config: expect.any(String),
});
});
});
46 changes: 46 additions & 0 deletions packages/plugin-axe/src/lib/axe-core-polyfilled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Axe-core Polyfilled Import
*
* This file ensures the jsdom polyfill runs BEFORE axe-core is imported.
* Static imports are evaluated in order, so the polyfill import must come first.
*
* WHY THIS EXISTS:
* axe-core has side effects on import - it expects global `window` and `document` objects
* to be available when the module is loaded. In Node.js environments, these don't exist
* by default. This polyfill creates a virtual DOM using JSDOM and sets these globals
* before axe-core is imported.
*
* IMPORT ORDER IS CRITICAL:
* 1. jsdom.polyfill.ts is imported first (sets up globalThis.window and globalThis.document)
* 2. axe-core is imported second (now globals exist)
*
* IMPORT CHAIN:
* 1. This file (imports polyfill, then imports axe-core)
* 2. safe-axe-core-import.ts (re-exports for clean imports)
*
* USAGE:
* Do NOT import from this file directly. Use safe-axe-core-import.ts instead.
*
* @see https://github.com/dequelabs/axe-core/issues/3962
*/
// CRITICAL: This import MUST come before axe-core import
// It sets up globalThis.window and globalThis.document as side effects
// eslint-disable-next-line import/no-unassigned-import
// Now safe to import axe-core - globals exist due to polyfill above
// This import MUST come after the polyfill import
import axe from 'axe-core';
import './jsdom.polyfill.js';

// Re-export axe as default
export default axe;

// Re-export all types used throughout the codebase
export type {
AxeResults,
NodeResult,
Result,
IncompleteResult,
RuleMetadata,
ImpactValue,
CrossTreeSelector,
} from 'axe-core';
2 changes: 1 addition & 1 deletion packages/plugin-axe/src/lib/groups.int.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axe from 'axe-core';
import { axeCategoryGroupSlugSchema, axeWcagTagSchema } from './groups.js';
import axe from './safe-axe-core-import.js';

describe('axeCategoryGroupSlugSchema', () => {
const axeCategoryTags = axe
Expand Down
32 changes: 32 additions & 0 deletions packages/plugin-axe/src/lib/jsdom.polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* JSDOM Polyfill Setup
*
* WHY THIS EXISTS:
* axe-core has side effects on import - it expects global `window` and `document` objects
* to be available when the module is loaded. In Node.js environments, these don't exist
* by default. This polyfill creates a virtual DOM using JSDOM and sets these globals
* before axe-core is imported.
*
* HOW IT WORKS:
* - Creates a minimal JSDOM instance with a basic HTML document
* - Sets globalThis.window and globalThis.document to the JSDOM window/document
* - This must be imported BEFORE any axe-core imports to ensure globals exist
*
* IMPORT CHAIN:
* This file is imported first by axe-core-polyfilled.ts, which then safely imports
* axe-core. All other files should import from safe-axe-core-import.ts, not directly
* from this file or from 'axe-core'.
*
* @see https://github.com/dequelabs/axe-core/issues/3962
*/
// @ts-ignore - jsdom types are in devDependencies at root level
import { JSDOM } from 'jsdom';

const html = `<!DOCTYPE html>\n<html></html>`;
const { window: jsdomWindow } = new JSDOM(html);

// Set globals for axe-core compatibility
// eslint-disable-next-line functional/immutable-data
globalThis.window = jsdomWindow as unknown as Window & typeof globalThis;
// eslint-disable-next-line functional/immutable-data
globalThis.document = jsdomWindow.document;
2 changes: 1 addition & 1 deletion packages/plugin-axe/src/lib/meta/transform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import axe from 'axe-core';
import type { Audit, Group } from '@code-pushup/models';
import { objectToEntries, wrapTags } from '@code-pushup/utils';
import type { AxePreset } from '../config.js';
Expand All @@ -7,6 +6,7 @@ import {
CATEGORY_GROUPS,
getWcagPresetTags,
} from '../groups.js';
import axe from '../safe-axe-core-import.js';

/** Loads Axe rules filtered by the specified preset. */
export function loadAxeRules(preset: AxePreset): axe.RuleMetadata[] {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-axe/src/lib/runner/run-axe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AxeBuilder } from '@axe-core/playwright';
import ansis from 'ansis';
import type { AxeResults } from 'axe-core';
import { createRequire } from 'node:module';
import path from 'node:path';
import {
Expand All @@ -17,6 +16,7 @@ import {
logger,
pluralizeToken,
} from '@code-pushup/utils';
import type { AxeResults } from '../safe-axe-core-import.js';
import { type SetupFunction, runSetup } from './setup.js';
import { toAuditOutputs } from './transform.js';

Expand Down
6 changes: 5 additions & 1 deletion packages/plugin-axe/src/lib/runner/runner.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { AxeResults, IncompleteResult, Result } from 'axe-core';
import { type AuditOutput, DEFAULT_PERSIST_CONFIG } from '@code-pushup/models';
import type {
AxeResults,
IncompleteResult,
Result,
} from '../safe-axe-core-import.js';
import type { AxeUrlResult } from './run-axe.js';
import { createRunnerFunction } from './runner.js';
import * as setup from './setup.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-axe/src/lib/runner/transform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import axe from 'axe-core';
import type {
AuditOutput,
AuditOutputs,
Expand All @@ -10,6 +9,7 @@ import {
pluralizeToken,
truncateIssueMessage,
} from '@code-pushup/utils';
import axe from '../safe-axe-core-import.js';

/**
* Transforms Axe results into audit outputs.
Expand Down
6 changes: 5 additions & 1 deletion packages/plugin-axe/src/lib/runner/transform.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { AxeResults, CheckResult, NodeResult, Result } from 'axe-core';
import type { AuditOutput } from '@code-pushup/models';
import type {
AxeResults,
NodeResult,
Result,
} from '../safe-axe-core-import.js';
import { toAuditOutputs } from './transform.js';

function createMockCheck(overrides: Partial<CheckResult> = {}): CheckResult {
Expand Down
33 changes: 33 additions & 0 deletions packages/plugin-axe/src/lib/safe-axe-core-import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Safe Axe-core Import Entry Point
*
* This is the ONLY safe way to import axe-core in this codebase.
* All files should import from this module instead of importing directly from 'axe-core'.
*
* WHY THIS EXISTS:
* axe-core requires global `window` and `document` objects to exist when imported.
* Due to ES module import hoisting, we need a fixed import chain to ensure the
* jsdom polyfill runs before axe-core loads.
*
* IMPORT CHAIN:
* jsdom.polyfill.ts → axe-core-polyfilled.ts → safe-axe-core-import.ts → your code
*
* USAGE:
* Instead of: import axe from 'axe-core';
* Use: import axe from './safe-axe-core-import.js';
*
* Instead of: import type { AxeResults } from 'axe-core';
* Use: import type { AxeResults } from './safe-axe-core-import.js';
*/

// Re-export everything from the polyfilled version
export { default } from './axe-core-polyfilled.js';
export type {
AxeResults,
NodeResult,
Result,
IncompleteResult,
RuleMetadata,
ImpactValue,
CrossTreeSelector,
} from './axe-core-polyfilled.js';
4 changes: 4 additions & 0 deletions packages/utils/mocks/fixtures/actually-invalid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This is an actually invalid JavaScript file with syntax errors
const x = {
missing: "closing brace"
// Unclosed object
2 changes: 2 additions & 0 deletions packages/utils/mocks/fixtures/helper-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const helperValue = 'helper-module-value';
export default helperValue;
1 change: 0 additions & 1 deletion packages/utils/mocks/fixtures/invalid-js-file.json

This file was deleted.

7 changes: 7 additions & 0 deletions packages/utils/mocks/fixtures/tsconfig-paths-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@test/helper": ["./helper-module.ts"]
}
}
}
8 changes: 8 additions & 0 deletions packages/utils/mocks/fixtures/uses-path-alias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// eslint-disable-next-line import/no-unresolved
import helperValue from '@test/helper';

const config = {
value: helperValue,
};

export default config;
5 changes: 3 additions & 2 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@
"@code-pushup/models": "0.113.0",
"ansis": "^3.3.0",
"build-md": "^0.4.2",
"bundle-require": "^5.1.0",
"esbuild": "^0.25.2",
"ora": "^9.0.0",
"semver": "^7.6.0",
"simple-git": "^3.20.0",
"string-width": "^8.1.0",
"wrap-ansi": "^9.0.2",
"zod": "^4.2.1"
"zod": "^4.2.1",
"jiti": "^2.4.2",
"typescript": "5.7.3"
},
"peerDependencies": {
"@nx/devkit": ">=17.0.0"
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export {
type ProcessObserver,
type ProcessResult,
} from './lib/execute-process.js';
export { loadTargetConfig } from './lib/load-ts-config.js';
export {
crawlFileSystem,
createReportPath,
Expand Down
Loading
Loading