diff --git a/code-pushup.preset.ts b/code-pushup.preset.ts index ab6224535..241f6879d 100644 --- a/code-pushup.preset.ts +++ b/code-pushup.preset.ts @@ -11,6 +11,7 @@ import coveragePlugin, { } from './packages/plugin-coverage/src/index.js'; import eslintPlugin, { eslintConfigFromAllNxProjects, + eslintConfigFromNxProject, } from './packages/plugin-eslint/src/index.js'; import jsPackagesPlugin from './packages/plugin-js-packages/src/index.js'; import jsDocsPlugin from './packages/plugin-jsdocs/src/index.js'; @@ -43,18 +44,15 @@ export async function configureEslintPlugin( return { plugins: [ projectName - ? await eslintPlugin( - { eslintrc: `packages/${projectName}/eslint.config.js` }, - { - artifacts: { - // We leverage Nx dependsOn to only run all lint targets before we run code-pushup - // generateArtifactsCommand: 'npx nx run-many -t lint', - artifactsPaths: [ - `packages/${projectName}/.eslint/eslint-report.json`, - ], - }, + ? await eslintPlugin(await eslintConfigFromNxProject(projectName), { + artifacts: { + // We leverage Nx dependsOn to only run all lint targets before we run code-pushup + // generateArtifactsCommand: 'npx nx run-many -t lint', + artifactsPaths: [ + `packages/${projectName}/.eslint/eslint-report.json`, + ], }, - ) + }) : await eslintPlugin(await eslintConfigFromAllNxProjects()), ], categories: [ diff --git a/package-lock.json b/package-lock.json index b9c0986ff..400824270 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,12 +64,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", @@ -92,7 +93,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", @@ -3209,9 +3210,10 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -9278,6 +9280,18 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -9380,6 +9394,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -9430,6 +9451,67 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.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/project-service/node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "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/project-service/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/@typescript-eslint/project-service/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/@typescript-eslint/scope-manager": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", @@ -9447,6 +9529,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "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.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", @@ -9548,16 +9647,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", - "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.29.0", - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/typescript-estree": "8.29.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9568,18 +9667,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", - "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0" + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9590,9 +9689,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", - "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", "dev": true, "license": "MIT", "engines": { @@ -9604,20 +9703,21 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", - "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.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" @@ -9627,18 +9727,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", - "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9648,10 +9748,28 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/utils/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/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9677,10 +9795,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/utils/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/@typescript-eslint/utils/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/utils/node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "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": { @@ -10550,15 +10688,21 @@ } }, "node_modules/@vitest/eslint-plugin": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.38.tgz", - "integrity": "sha512-KcOTZyVz8RiM5HyriiDVrP1CyBGuhRxle+lBsmSs6NTJEO/8dKVAq+f5vQzHj1/Kc7bYXSDO6yBe62Zx0t5iaw==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.6.6.tgz", + "integrity": "sha512-bwgQxQWRtnTVzsUHK824tBmHzjV0iTx3tZaiQIYDjX3SA7TsQS8CuDVqxXrRY3FaOUMgbGavesCxI9MOfFLm7Q==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.51.0", + "@typescript-eslint/utils": "^8.51.0" + }, + "engines": { + "node": ">=18" + }, "peerDependencies": { - "@typescript-eslint/utils": "^8.24.0", - "eslint": ">= 8.57.0", - "typescript": ">= 5.0.0", + "eslint": ">=8.57.0", + "typescript": ">=5.0.0", "vitest": "*" }, "peerDependenciesMeta": { @@ -10570,6 +10714,69 @@ } } }, + "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "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/@vitest/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitest/expect": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", @@ -22270,6 +22477,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", "dev": true, + "license": "MIT", "dependencies": { "cssstyle": "^4.0.1", "data-urls": "^5.0.0", diff --git a/package.json b/package.json index a7b55cf4f..b9d44b758 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/packages/cli/src/lib/implementation/core-config.middleware.int.test.ts b/packages/cli/src/lib/implementation/core-config.middleware.int.test.ts index 4aeba1175..6634b1b90 100644 --- a/packages/cli/src/lib/implementation/core-config.middleware.int.test.ts +++ b/packages/cli/src/lib/implementation/core-config.middleware.int.test.ts @@ -64,6 +64,6 @@ describe('coreConfigMiddleware', () => { ), ...CLI_DEFAULTS, }), - ).rejects.toThrow("Cannot find package '@example/custom-plugin'"); + ).rejects.toThrow(/Cannot find (module|package) '@example\/custom-plugin'/); }); }); diff --git a/packages/core/src/lib/implementation/read-rc-file.ts b/packages/core/src/lib/implementation/read-rc-file.ts index 090ad2c0e..fb27a0733 100644 --- a/packages/core/src/lib/implementation/read-rc-file.ts +++ b/packages/core/src/lib/implementation/read-rc-file.ts @@ -27,7 +27,6 @@ export async function readRcByPath( const result = await importModule({ filepath: filePath, tsconfig, - format: 'esm', }); return { result, message: `Imported config from ${formattedTarget}` }; }, diff --git a/packages/core/src/lib/implementation/read-rc-file.unit.test.ts b/packages/core/src/lib/implementation/read-rc-file.unit.test.ts index 9f72ca34b..f2ec3d16b 100644 --- a/packages/core/src/lib/implementation/read-rc-file.unit.test.ts +++ b/packages/core/src/lib/implementation/read-rc-file.unit.test.ts @@ -3,32 +3,30 @@ import { CONFIG_FILE_NAME, type CoreConfig } from '@code-pushup/models'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import { autoloadRc } from './read-rc-file.js'; -// mock bundleRequire inside importEsmModule used for fetching config -vi.mock('bundle-require', async () => { +// mock importModule from @code-pushup/utils to bypass jiti which doesn't work with memfs +vi.mock('@code-pushup/utils', async () => { + const utils: object = await vi.importActual('@code-pushup/utils'); const { CORE_CONFIG_MOCK }: Record = await vi.importActual('@code-pushup/test-fixtures'); + const mockImportModule = async (options: { filepath: string }) => { + const extension = options.filepath.split('.').at(-1); + return { + ...CORE_CONFIG_MOCK, + upload: { + ...CORE_CONFIG_MOCK?.upload, + project: extension, // returns loaded file extension to check format precedence + }, + }; + }; + return { - bundleRequire: vi - .fn() - .mockImplementation((options: { filepath: string }) => { - const extension = options.filepath.split('.').at(-1); - return { - mod: { - default: { - ...CORE_CONFIG_MOCK, - upload: { - ...CORE_CONFIG_MOCK?.upload, - project: extension, // returns loaded file extension to check format precedence - }, - }, - }, - }; - }), + ...utils, + importModule: mockImportModule, }; }); -// Note: memfs files are only listed to satisfy a system check, value is used from bundle-require mock +// Note: memfs files are only listed to satisfy a system check, value is used from the mocked importModule describe('autoloadRc', () => { it('prioritise a .ts configuration file', async () => { vol.fromJSON( diff --git a/packages/nx-plugin/eslint.config.js b/packages/nx-plugin/eslint.config.js index f0ea93505..0a40d719b 100644 --- a/packages/nx-plugin/eslint.config.js +++ b/packages/nx-plugin/eslint.config.js @@ -1,14 +1,21 @@ -const tseslint = require('typescript-eslint'); -const baseConfig = require('../../eslint.config.js').default; +import tseslint from 'typescript-eslint'; +import baseConfig from '../../eslint.config.js'; -module.exports = tseslint.config( +if (process.env['CP_DEBUG_ESLINT_IMPORTS'] === 'true') { + // eslint-disable-next-line no-console + console.log('[CP_DEBUG] Loaded packages/nx-plugin/eslint.config.js', { + baseConfigLength: baseConfig.length, + }); +} + +export default tseslint.config( ...baseConfig, { files: ['**/*.ts'], languageOptions: { parserOptions: { projectService: true, - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, }, diff --git a/packages/nx-plugin/eslint.config.mjs b/packages/nx-plugin/eslint.config.mjs new file mode 100644 index 000000000..94316d263 --- /dev/null +++ b/packages/nx-plugin/eslint.config.mjs @@ -0,0 +1,63 @@ +import tseslint from 'typescript-eslint'; +import baseConfig from '../../eslint.config.js'; + +if (process.env['CP_DEBUG_ESLINT_IMPORTS'] === 'true') { + // eslint-disable-next-line no-console + console.log('[CP_DEBUG] Loaded packages/nx-plugin/eslint.config.js', { + baseConfigLength: baseConfig.length, + }); +} + +export default tseslint.config( + ...baseConfig, + { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + files: ['**/*.ts'], + rules: { + // Nx plugins don't yet support ESM: https://github.com/nrwl/nx/issues/15682 + 'unicorn/prefer-module': 'off', + // used instead of verbatimModuleSyntax tsconfig flag (requires ESM) + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { + fixStyle: 'inline-type-imports', + disallowTypeAnnotations: false, + }, + ], + '@typescript-eslint/consistent-type-exports': [ + 'warn', + { fixMixedExportsWithInlineTypeSpecifier: true }, + ], + // `import path from 'node:path'` incompatible with CJS runtime, prefer `import * as path from 'node:path'` + 'unicorn/import-style': [ + 'warn', + { styles: { 'node:path': { namespace: true } } }, + ], + // `import { logger } from '@nx/devkit' is OK here + 'no-restricted-imports': 'off', + }, + }, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'error', + { ignoredDependencies: ['typescript-eslint'] }, + ], + }, + }, + { + files: ['**/package.json', '**/generators.json'], + rules: { + '@nx/nx-plugin-checks': 'error', + }, + }, +); diff --git a/packages/nx-plugin/project.json b/packages/nx-plugin/project.json index 247770900..5e1c5292b 100644 --- a/packages/nx-plugin/project.json +++ b/packages/nx-plugin/project.json @@ -38,6 +38,15 @@ "packages/nx-plugin/**/*.ts", "packages/nx-plugin/package.json", "packages/nx-plugin/generators.json" + ], + "args": [ + "'{projectRoot}/**/*.{ts,cjs,mjs,js}'", + "{projectRoot}/package.json", + "--config={projectRoot}/eslint.config.mjs", + "--max-warnings=0", + "--no-warn-ignored", + "--error-on-unmatched-pattern=false", + "--format=./tools/eslint-formatter-multi/dist/src/index.js" ] } }, diff --git a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts index 631fd04b9..a0e7ed9ae 100644 --- a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts +++ b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts @@ -353,7 +353,7 @@ describe('normalizedCreateNodesV2Context', () => { createNodesV2Context({ workspaceRoot: MEMFS_VOLUME }), projectJsonPath(projectRoot), ), - ).rejects.toThrow( + ).rejects.toThrowError( `Error parsing project.json file ${projectJsonPath(projectRoot)}.`, ); }); @@ -372,7 +372,7 @@ describe('normalizedCreateNodesV2Context', () => { createNodesV2Context({ workspaceRoot: MEMFS_VOLUME }), projectJsonPath(projectRoot), ), - ).rejects.toThrow( + ).rejects.toThrowError( `Error parsing project.json file ${projectJsonPath(projectRoot)}.`, ); }); diff --git a/packages/nx-plugin/src/plugin/utils.unit.test.ts b/packages/nx-plugin/src/plugin/utils.unit.test.ts index 270dffed5..784821e35 100644 --- a/packages/nx-plugin/src/plugin/utils.unit.test.ts +++ b/packages/nx-plugin/src/plugin/utils.unit.test.ts @@ -107,7 +107,9 @@ describe('normalizedCreateNodesV2Context', () => { createNodesV2Context({ workspaceRoot: MEMFS_VOLUME }), projectJsonPath(), ), - ).rejects.toThrow(`Error parsing project.json file ${projectJsonPath()}.`); + ).rejects.toThrowError( + `Error parsing project.json file ${projectJsonPath()}.`, + ); }); it('should provide default targetName in createOptions', async () => { diff --git a/packages/plugin-axe/src/lib/axe-core-polyfilled.ts b/packages/plugin-axe/src/lib/axe-core-polyfilled.ts new file mode 100644 index 000000000..e6c6cc2ea --- /dev/null +++ b/packages/plugin-axe/src/lib/axe-core-polyfilled.ts @@ -0,0 +1,34 @@ +/** + * Axe-core Polyfilled Import + * + * This file ensures the jsdom polyfill runs BEFORE axe-core is imported. + * Due to ES module import hoisting, we must import the polyfill explicitly + * at the top of this file, then import axe-core. This guarantees the correct + * execution order: polyfill setup → axe-core import. + * + * IMPORT CHAIN: + * 1. jsdom.polyfill.ts (sets globalThis.window and globalThis.document) + * 2. This file (imports polyfill, then imports axe-core) + * 3. 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. + */ +// Import polyfill FIRST to ensure globals are set before axe-core loads +// eslint-disable-next-line import/no-unassigned-import +// Now safe to import axe-core - globals exist due to polyfill import above +import axe from 'axe-core'; +import './jsdom.polyfill.js'; + +// Re-export axe default and all types used throughout the codebase +export default axe; + +export type { + AxeResults, + NodeResult, + Result, + IncompleteResult, + RuleMetadata, + ImpactValue, + CrossTreeSelector, +} from 'axe-core'; diff --git a/packages/plugin-axe/src/lib/groups.int.test.ts b/packages/plugin-axe/src/lib/groups.int.test.ts index acf24e541..79932c4d0 100644 --- a/packages/plugin-axe/src/lib/groups.int.test.ts +++ b/packages/plugin-axe/src/lib/groups.int.test.ts @@ -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 diff --git a/packages/plugin-axe/src/lib/jsdom.polyfill.ts b/packages/plugin-axe/src/lib/jsdom.polyfill.ts new file mode 100644 index 000000000..2585b5c54 --- /dev/null +++ b/packages/plugin-axe/src/lib/jsdom.polyfill.ts @@ -0,0 +1,31 @@ +/** + * 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 + */ +import { JSDOM } from 'jsdom'; + +const html = `\n`; +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; diff --git a/packages/plugin-axe/src/lib/meta/transform.ts b/packages/plugin-axe/src/lib/meta/transform.ts index f4e8ad03e..274e5cc62 100644 --- a/packages/plugin-axe/src/lib/meta/transform.ts +++ b/packages/plugin-axe/src/lib/meta/transform.ts @@ -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'; @@ -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[] { diff --git a/packages/plugin-axe/src/lib/runner/run-axe.ts b/packages/plugin-axe/src/lib/runner/run-axe.ts index ccc6712a9..3410cedf8 100644 --- a/packages/plugin-axe/src/lib/runner/run-axe.ts +++ b/packages/plugin-axe/src/lib/runner/run-axe.ts @@ -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 { @@ -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'; diff --git a/packages/plugin-axe/src/lib/runner/runner.unit.test.ts b/packages/plugin-axe/src/lib/runner/runner.unit.test.ts index cf742b512..2b4af8d5a 100644 --- a/packages/plugin-axe/src/lib/runner/runner.unit.test.ts +++ b/packages/plugin-axe/src/lib/runner/runner.unit.test.ts @@ -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'; diff --git a/packages/plugin-axe/src/lib/runner/transform.ts b/packages/plugin-axe/src/lib/runner/transform.ts index 6f4e1e2c2..8d55f7f01 100644 --- a/packages/plugin-axe/src/lib/runner/transform.ts +++ b/packages/plugin-axe/src/lib/runner/transform.ts @@ -1,4 +1,3 @@ -import axe from 'axe-core'; import type { AuditOutput, AuditOutputs, @@ -11,6 +10,7 @@ import { pluralizeToken, truncateIssueMessage, } from '@code-pushup/utils'; +import axe from '../safe-axe-core-import.js'; /** * Transforms Axe results into audit outputs. diff --git a/packages/plugin-axe/src/lib/runner/transform.unit.test.ts b/packages/plugin-axe/src/lib/runner/transform.unit.test.ts index 63178b785..7d12e3f2e 100644 --- a/packages/plugin-axe/src/lib/runner/transform.unit.test.ts +++ b/packages/plugin-axe/src/lib/runner/transform.unit.test.ts @@ -1,5 +1,9 @@ -import type { AxeResults, 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 createMockNode(overrides: Partial = {}): NodeResult { diff --git a/packages/plugin-axe/src/lib/safe-axe-core-import.ts b/packages/plugin-axe/src/lib/safe-axe-core-import.ts new file mode 100644 index 000000000..00ec99165 --- /dev/null +++ b/packages/plugin-axe/src/lib/safe-axe-core-import.ts @@ -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'; diff --git a/packages/plugin-coverage/src/lib/nx/coverage-paths.ts b/packages/plugin-coverage/src/lib/nx/coverage-paths.ts index 1890e5509..b95234f52 100644 --- a/packages/plugin-coverage/src/lib/nx/coverage-paths.ts +++ b/packages/plugin-coverage/src/lib/nx/coverage-paths.ts @@ -147,7 +147,6 @@ export async function getCoveragePathForVitest( const vitestConfig = await importModule({ filepath: config, - format: 'esm', }); const reportsDirectory = diff --git a/packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts b/packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts index 5bcaaa422..51a60b1ee 100644 --- a/packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts +++ b/packages/plugin-coverage/src/lib/nx/coverage-paths.unit.test.ts @@ -13,80 +13,86 @@ import { getCoveragePathsForTarget, } from './coverage-paths.js'; -vi.mock('bundle-require', () => ({ - bundleRequire: vi.fn().mockImplementation((options: { filepath: string }) => { - const VITEST_VALID: VitestCoverageConfig = { - test: { - coverage: { - reporter: ['lcov'], - reportsDirectory: path.join('coverage', 'cli'), - }, +// mock importModule from @code-pushup/utils to bypass jiti which doesn't work with memfs +vi.mock('@code-pushup/utils', async () => { + const utils: object = await vi.importActual('@code-pushup/utils'); + + const VITEST_VALID: VitestCoverageConfig = { + test: { + coverage: { + reporter: ['lcov'], + reportsDirectory: path.join('coverage', 'cli'), }, - }; + }, + }; - const VITEST_NO_DIR: VitestCoverageConfig = { - test: { coverage: { reporter: ['lcov'] } }, - }; + const VITEST_NO_DIR: VitestCoverageConfig = { + test: { coverage: { reporter: ['lcov'] } }, + }; - const VITEST_NO_LCOV: VitestCoverageConfig = { - test: { - coverage: { - reporter: ['json'], - reportsDirectory: 'coverage', - }, + const VITEST_NO_LCOV: VitestCoverageConfig = { + test: { + coverage: { + reporter: ['json'], + reportsDirectory: 'coverage', }, - }; + }, + }; - const JEST_VALID: JestCoverageConfig = { - coverageReporters: ['lcov'], - coverageDirectory: path.join('coverage', 'core'), - }; + const JEST_VALID: JestCoverageConfig = { + coverageReporters: ['lcov'], + coverageDirectory: path.join('coverage', 'core'), + }; - const JEST_NO_DIR: JestCoverageConfig = { - coverageReporters: ['lcov'], - }; + const JEST_NO_DIR: JestCoverageConfig = { + coverageReporters: ['lcov'], + }; - const JEST_NO_LCOV: JestCoverageConfig = { - coverageReporters: ['json'], - coverageDirectory: 'coverage', - }; + const JEST_NO_LCOV: JestCoverageConfig = { + coverageReporters: ['json'], + coverageDirectory: 'coverage', + }; - const JEST_PRESET: JestCoverageConfig & { preset?: string } = { - preset: '../../jest.preset.ts', - coverageDirectory: 'coverage', - }; + const JEST_PRESET: JestCoverageConfig & { preset?: string } = { + preset: '../../jest.preset.ts', + coverageDirectory: 'coverage', + }; - const wrapReturnValue = ( - value: VitestCoverageConfig | JestCoverageConfig, - ) => ({ mod: { default: value } }); - - const config = options.filepath.split('.')[0]; - switch (config) { - case 'vitest-valid': - return wrapReturnValue(VITEST_VALID); - case 'vitest-no-lcov': - return wrapReturnValue(VITEST_NO_LCOV); - case 'vitest-no-dir': - return wrapReturnValue(VITEST_NO_DIR); - case 'jest-valid': - return wrapReturnValue(JEST_VALID); - case 'jest-no-lcov': - return wrapReturnValue(JEST_NO_LCOV); - case 'jest-no-dir': - return wrapReturnValue(JEST_NO_DIR); - case 'jest-preset': - return wrapReturnValue(JEST_PRESET); - default: - return wrapReturnValue({}); - } - }), -})); + return { + ...utils, + importModule: vi + .fn() + .mockImplementation((options: { filepath: string }) => { + // Extract config name from filename (handles both absolute and relative paths) + const filename = path.basename(options.filepath); + const config = filename.split('.')[0]; + switch (config) { + case 'vitest-valid': + return Promise.resolve(VITEST_VALID); + case 'vitest-no-lcov': + return Promise.resolve(VITEST_NO_LCOV); + case 'vitest-no-dir': + return Promise.resolve(VITEST_NO_DIR); + case 'jest-valid': + return Promise.resolve(JEST_VALID); + case 'jest-no-lcov': + return Promise.resolve(JEST_NO_LCOV); + case 'jest-no-dir': + return Promise.resolve(JEST_NO_DIR); + case 'jest-preset': + return Promise.resolve(JEST_PRESET); + default: + return Promise.resolve({}); + } + }), + }; +}); describe('getCoveragePathForTarget', () => { beforeEach(() => { vol.fromJSON( { - // values come from bundle-require mock above + // values come from importModule mock above 'vitest-valid.config.ts': '', 'jest-valid.config.ts': '', }, @@ -162,7 +168,7 @@ describe('getCoveragePathForVitest', () => { beforeEach(() => { vol.fromJSON( { - // values come from bundle-require mock above + // values come from importModule mock above 'vitest-valid.config.unit.ts': '', 'vitest-no-dir.config.integration.ts': '', 'vitest-no-lcov.config.integration.ts': '', @@ -260,7 +266,7 @@ describe('getCoveragePathForJest', () => { beforeEach(() => { vol.fromJSON( { - // values come from bundle-require mock above + // values come from importModule mock above 'jest-preset.config.ts': '', 'jest-valid.config.unit.ts': '', 'jest-valid.config.integration.ts': '', diff --git a/packages/plugin-eslint/README.md b/packages/plugin-eslint/README.md index 8a4ca9027..abdecd0a9 100644 --- a/packages/plugin-eslint/README.md +++ b/packages/plugin-eslint/README.md @@ -92,6 +92,19 @@ Detected ESLint rules are mapped to Code PushUp audits. Audit reports are calcul await eslintConfigFromAllNxProjects({ exclude: ['server'] }); ``` + - If you wish to target a specific project only (without dependencies), use the `eslintConfigFromNxProject` helper: + + ```js + import eslintPlugin, { eslintConfigFromNxProject } from '@code-pushup/eslint-plugin'; + + export default { + plugins: [ + // ... + await eslintPlugin(await eslintConfigFromNxProject('')), + ], + }; + ``` + - If you wish to target a specific project along with other projects it depends on, use the `eslintConfigFromNxProjectAndDeps` helper and pass in in your project name: ```js diff --git a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/eslint.config.js b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/eslint.config.js index 6ef27db4c..52f57347c 100644 --- a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/eslint.config.js +++ b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/eslint.config.js @@ -1,12 +1,13 @@ -const nx = require('@nx/eslint-plugin'); +import nx from '@nx/eslint-plugin'; +import jsoncParser from 'jsonc-eslint-parser'; -module.exports = [ +export default [ { files: ['**/*.json'], // Override or add rules here rules: {}, languageOptions: { - parser: require('jsonc-eslint-parser'), + parser: jsoncParser, }, }, ...nx.configs['flat/base'], diff --git a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/cli/eslint.config.js b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/cli/eslint.config.js index 9d2af7a3d..8c9d9ff0d 100644 --- a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/cli/eslint.config.js +++ b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/cli/eslint.config.js @@ -1,6 +1,7 @@ -const baseConfig = require('../../eslint.config.js'); +import jsoncParser from 'jsonc-eslint-parser'; +import baseConfig from '../../eslint.config.js'; -module.exports = [ +export default [ ...baseConfig, { files: ['**/*.json'], @@ -13,7 +14,7 @@ module.exports = [ ], }, languageOptions: { - parser: require('jsonc-eslint-parser'), + parser: jsoncParser, }, }, ]; diff --git a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/core/eslint.config.js b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/core/eslint.config.js index 9d2af7a3d..8c9d9ff0d 100644 --- a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/core/eslint.config.js +++ b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/core/eslint.config.js @@ -1,6 +1,7 @@ -const baseConfig = require('../../eslint.config.js'); +import jsoncParser from 'jsonc-eslint-parser'; +import baseConfig from '../../eslint.config.js'; -module.exports = [ +export default [ ...baseConfig, { files: ['**/*.json'], @@ -13,7 +14,7 @@ module.exports = [ ], }, languageOptions: { - parser: require('jsonc-eslint-parser'), + parser: jsoncParser, }, }, ]; diff --git a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/nx-plugin/eslint.config.js b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/nx-plugin/eslint.config.js index 9327f3f52..88a86f0c8 100644 --- a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/nx-plugin/eslint.config.js +++ b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/nx-plugin/eslint.config.js @@ -1,6 +1,7 @@ -const baseConfig = require('../../eslint.config.js'); +import jsoncParser from 'jsonc-eslint-parser'; +import baseConfig from '../../eslint.config.js'; -module.exports = [ +export default [ ...baseConfig, { files: ['**/*.json'], @@ -13,7 +14,7 @@ module.exports = [ ], }, languageOptions: { - parser: require('jsonc-eslint-parser'), + parser: jsoncParser, }, }, { @@ -22,7 +23,7 @@ module.exports = [ '@nx/nx-plugin-checks': 'error', }, languageOptions: { - parser: require('jsonc-eslint-parser'), + parser: jsoncParser, }, }, ]; diff --git a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/utils/eslint.config.js b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/utils/eslint.config.js index 9d2af7a3d..8c9d9ff0d 100644 --- a/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/utils/eslint.config.js +++ b/packages/plugin-eslint/mocks/fixtures/nx-monorepo/packages/utils/eslint.config.js @@ -1,6 +1,7 @@ -const baseConfig = require('../../eslint.config.js'); +import jsoncParser from 'jsonc-eslint-parser'; +import baseConfig from '../../eslint.config.js'; -module.exports = [ +export default [ ...baseConfig, { files: ['**/*.json'], @@ -13,7 +14,7 @@ module.exports = [ ], }, languageOptions: { - parser: require('jsonc-eslint-parser'), + parser: jsoncParser, }, }, ]; diff --git a/packages/plugin-eslint/mocks/fixtures/todos-app/eslint.config.js b/packages/plugin-eslint/mocks/fixtures/todos-app/eslint.config.js index d4ca65827..9f75ab30a 100644 --- a/packages/plugin-eslint/mocks/fixtures/todos-app/eslint.config.js +++ b/packages/plugin-eslint/mocks/fixtures/todos-app/eslint.config.js @@ -1,9 +1,9 @@ -const react = require('eslint-plugin-react'); -const reactHooks = require('eslint-plugin-react-hooks'); -const globals = require('globals'); +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import globals from 'globals'; /** @type {import('eslint').Linter.Config[]} */ -module.exports = [ +const config = [ { files: ['**/*.jsx', '**/*.js'], ignores: ['eslint.config.js'], @@ -65,3 +65,5 @@ module.exports = [ }, }, ]; + +export default config; diff --git a/packages/plugin-lighthouse/src/lib/runner/utils.ts b/packages/plugin-lighthouse/src/lib/runner/utils.ts index a68ad368e..347c16bb4 100644 --- a/packages/plugin-lighthouse/src/lib/runner/utils.ts +++ b/packages/plugin-lighthouse/src/lib/runner/utils.ts @@ -144,7 +144,6 @@ export async function getConfig( message, result: await importModule({ filepath: configPath, - format: 'esm', }), }; } diff --git a/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts b/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts index a5dfee309..5cf72f500 100644 --- a/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts @@ -25,27 +25,25 @@ import { withLocalTmpDir, } from './utils.js'; -// mock bundleRequire inside importEsmModule used for fetching config -vi.mock('bundle-require', async () => { +// mock importModule from @code-pushup/utils to bypass jiti which doesn't work with memfs +vi.mock('@code-pushup/utils', async () => { + const utils: object = await vi.importActual('@code-pushup/utils'); const { CORE_CONFIG_MOCK }: Record = await vi.importActual('@code-pushup/test-utils'); return { - bundleRequire: vi + ...utils, + importModule: vi .fn() .mockImplementation((options: { filepath: string }) => { const project = options.filepath.split('.').at(-2); - return { - mod: { - default: { - ...CORE_CONFIG_MOCK, - upload: { - ...CORE_CONFIG_MOCK?.upload, - project, // returns loaded file extension to check in test - }, - }, + return Promise.resolve({ + ...CORE_CONFIG_MOCK, + upload: { + ...CORE_CONFIG_MOCK?.upload, + project, // returns loaded file extension to check in test }, - }; + }); }), }; }); diff --git a/packages/utils/package.json b/packages/utils/package.json index ed7d42912..8604578a8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -37,7 +37,9 @@ "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" diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ce3d64ed2..37c1b106d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -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, @@ -41,7 +42,6 @@ export { filePathToCliArg, findLineNumberInText, findNearestFile, - importModule, pluginWorkDir, projectToFilename, readJsonFile, @@ -184,3 +184,4 @@ export type { Prettify, WithRequired, } from './lib/types.js'; +export * from './lib/import-module.js'; diff --git a/packages/utils/src/lib/import-module.ts b/packages/utils/src/lib/import-module.ts new file mode 100644 index 000000000..0e7eeeaa0 --- /dev/null +++ b/packages/utils/src/lib/import-module.ts @@ -0,0 +1,209 @@ +import { createJiti as createJitiSource } from 'jiti'; +import { stat } from 'node:fs/promises'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import type { CompilerOptions } from 'typescript'; +import { fileExists } from './file-system.js'; +import { loadTargetConfig } from './load-ts-config.js'; +import { settlePromise } from './promises.js'; + +// For unknown reason, we can't import `JitiOptions` directly in this repository +type JitiOptions = Exclude[1], undefined>; + +/** + * Known packages that must be loaded natively (not transformed by jiti). + * These packages rely on import.meta.url being a real file:// URL. + * When jiti transforms modules, import.meta.url becomes 'about:blank', + * causing errors in packages that use new URL(..., import.meta.url). + */ +export const JITI_NATIVE_MODULES = [ + '@vitest/eslint-plugin', + '@code-pushup/eslint-config', + 'lighthouse', +] as const; + +export type ImportModuleOptions = JitiOptions & { + filepath: string; + tsconfig?: string; +}; + +export function toFileUrl(filepath: string): string { + return pathToFileURL(filepath).href; +} + +export async function importModule( + options: ImportModuleOptions, +): Promise { + const { filepath, tsconfig, ...jitiOptions } = options; + + if (!filepath) { + throw new Error( + `Importing module failed. File '${filepath}' does not exist`, + ); + } + + const absoluteFilePath = path.resolve(process.cwd(), filepath); + const resolvedStats = await settlePromise(stat(absoluteFilePath)); + if (resolvedStats.status === 'rejected') { + throw new Error(`File '${absoluteFilePath}' does not exist`); + } + if (!resolvedStats.value.isFile()) { + throw new Error(`Expected '${filepath}' to be a file`); + } + + const jitiInstance = await createTsJiti(import.meta.url, { + ...jitiOptions, + tsconfigPath: tsconfig, + }); + + return (await jitiInstance.import(toFileUrl(absoluteFilePath), { + default: true, + })) as T; +} + +/** + * Converts TypeScript paths configuration to jiti alias format + * @param paths TypeScript paths object from compiler options + * @param baseUrl Base URL for resolving relative paths + * @returns Jiti alias object with absolute paths + */ +export function mapTsPathsToJitiAlias( + paths: Record, + baseUrl: string, +): Record { + return Object.entries(paths).reduce( + (aliases, [pathPattern, pathMappings]) => { + if (!Array.isArray(pathMappings) || pathMappings.length === 0) { + return aliases; + } + // Jiti does not support overloads (multiple mappings for the same path pattern) + if (pathMappings.length > 1) { + throw new Error( + `TypeScript path overloads are not supported by jiti. Path pattern '${pathPattern}' has ${pathMappings.length} mappings: ${pathMappings.join(', ')}. Jiti only supports a single alias mapping per pattern.`, + ); + } + const aliasKey = pathPattern.replace(/\/\*$/, ''); + const aliasValue = (pathMappings.at(0) as string).replace(/\/\*$/, ''); + return { + ...aliases, + [aliasKey]: path.isAbsolute(aliasValue) + ? aliasValue + : path.resolve(baseUrl, aliasValue), + }; + }, + {} satisfies Record, + ); +} + +/** + * Maps TypeScript JSX emit mode to Jiti JSX boolean option + * @param tsJsxMode TypeScript JsxEmit enum value (0-5) + * @returns true if JSX processing should be enabled, false otherwise + */ +export const mapTsJsxToJitiJsx = (tsJsxMode: number): boolean => + tsJsxMode !== 0; + +/** + * Possible TS to jiti options mapping + * | Jiti Option | Jiti Type | TS Option | TS Type | Description | + * |-------------------|-------------------------|-----------------------|--------------------------|-------------| + * | alias | Record | paths | Record | Module path aliases for module resolution. | + * | interopDefault | boolean | esModuleInterop | boolean | Enable default import interop. | + * | sourceMaps | boolean | sourceMap | boolean | Enable sourcemap generation. | + * | jsx | boolean | jsx | JsxEmit (0-5) | TS JsxEmit enum (0-5) => boolean JSX processing. | + * | nativeModules | string[] | - | - | Modules to load natively without jiti transformation. | + */ +export type MappableJitiOptions = Partial< + Pick< + JitiOptions, + 'alias' | 'interopDefault' | 'sourceMaps' | 'jsx' | 'nativeModules' + > +>; +/** + * Parse TypeScript compiler options to mappable jiti options + * @param compilerOptions TypeScript compiler options + * @param tsconfigDir Directory of the tsconfig file (for resolving relative baseUrl) + * @returns Mappable jiti options + */ +export function parseTsConfigToJitiConfig( + compilerOptions: CompilerOptions, + tsconfigDir?: string, +): MappableJitiOptions { + const paths = compilerOptions.paths || {}; + const baseUrl = compilerOptions.baseUrl + ? path.isAbsolute(compilerOptions.baseUrl) + ? compilerOptions.baseUrl + : tsconfigDir + ? path.resolve(tsconfigDir, compilerOptions.baseUrl) + : path.resolve(process.cwd(), compilerOptions.baseUrl) + : tsconfigDir || process.cwd(); + + return { + ...(Object.keys(paths).length > 0 + ? { + alias: mapTsPathsToJitiAlias(paths, baseUrl), + } + : {}), + ...(compilerOptions.esModuleInterop == null + ? {} + : { interopDefault: compilerOptions.esModuleInterop }), + ...(compilerOptions.sourceMap == null + ? {} + : { sourceMaps: compilerOptions.sourceMap }), + ...(compilerOptions.jsx == null + ? {} + : { jsx: mapTsJsxToJitiJsx(compilerOptions.jsx) }), + }; +} + +/** + * Create a jiti instance with options derived from tsconfig. + * Used instead of direct jiti.createJiti to allow tsconfig integration. + * @param id + * @param options + * @param jiti + */ +export async function createTsJiti( + id: string, + options: JitiOptions & { tsconfigPath?: string } = {}, + createJiti: (typeof import('jiti'))['createJiti'] = createJitiSource, +) { + const { tsconfigPath, ...jitiOptions } = options; + const fallbackTsconfigPath = path.resolve(process.cwd(), 'tsconfig.json'); + const validPath: null | string = + tsconfigPath == null + ? (await fileExists(fallbackTsconfigPath)) + ? fallbackTsconfigPath + : null + : path.resolve(process.cwd(), tsconfigPath); + const tsDerivedJitiOptions: MappableJitiOptions = validPath + ? await jitiOptionsFromTsConfig(validPath) + : {}; + + return createJiti(id, { + ...jitiOptions, + ...tsDerivedJitiOptions, + alias: { + ...jitiOptions.alias, + ...tsDerivedJitiOptions.alias, + }, + nativeModules: [ + ...new Set([ + ...JITI_NATIVE_MODULES, + ...(jitiOptions.nativeModules ?? []), + ]), + ], + tryNative: true, + }); +} + +/** + * Read tsconfig file and parse options to jiti options + * @param tsconfigPath + */ +export async function jitiOptionsFromTsConfig( + tsconfigPath: string, +): Promise { + const { options } = loadTargetConfig(tsconfigPath); + return parseTsConfigToJitiConfig(options, path.dirname(tsconfigPath)); +} diff --git a/packages/utils/src/lib/import-module.unit.test.ts b/packages/utils/src/lib/import-module.unit.test.ts new file mode 100644 index 000000000..25afed1dc --- /dev/null +++ b/packages/utils/src/lib/import-module.unit.test.ts @@ -0,0 +1,124 @@ +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import type { CompilerOptions } from 'typescript'; +import { describe, expect, it } from 'vitest'; +import { + mapTsPathsToJitiAlias, + parseTsConfigToJitiConfig, + toFileUrl, +} from './import-module.js'; + +describe('mapTsPathsToJitiAlias', () => { + it('returns empty object when paths is empty', () => { + expect(mapTsPathsToJitiAlias({}, '/base')).toStrictEqual({}); + }); + + it('returns empty object when all path mappings are empty arrays', () => { + expect(mapTsPathsToJitiAlias({ '@/*': [] }, '/base')).toStrictEqual({}); + }); + + it('maps single path pattern without wildcards', () => { + expect(mapTsPathsToJitiAlias({ '@': ['src'] }, '/base')).toStrictEqual({ + '@': expect.pathToEndWith('base/src'), + }); + }); + + it('strips /* from path pattern and mapping', () => { + expect(mapTsPathsToJitiAlias({ '@/*': ['src/*'] }, '/base')).toStrictEqual({ + '@': expect.pathToEndWith('base/src'), + }); + }); + + it('resolves relative path mappings to absolute', () => { + expect(mapTsPathsToJitiAlias({ '@/*': ['src/*'] }, '/app')).toStrictEqual({ + '@': expect.pathToEndWith('app/src'), + }); + }); + + it('keeps absolute path mappings as-is', () => { + expect( + mapTsPathsToJitiAlias({ '@/*': ['/absolute/path/*'] }, '/base'), + ).toStrictEqual({ '@': '/absolute/path' }); + }); + + it('throws error when path overloads exist (multiple mappings)', () => { + expect(() => + mapTsPathsToJitiAlias({ '@/*': ['first/*', 'second/*'] }, '/base'), + ).toThrow( + "TypeScript path overloads are not supported by jiti. Path pattern '@/*' has 2 mappings: first/*, second/*. Jiti only supports a single alias mapping per pattern.", + ); + }); + + it('maps multiple path patterns', () => { + expect( + mapTsPathsToJitiAlias( + { + '@/*': ['src/*'], + '~/*': ['lib/*'], + }, + '/base', + ), + ).toStrictEqual({ + '@': expect.pathToEndWith('base/src'), + '~': expect.pathToEndWith('base/lib'), + }); + }); + + it('filters out invalid mappings and keeps valid ones', () => { + expect( + mapTsPathsToJitiAlias( + { + 'invalid/*': [], + '@/*': ['src/*'], + 'also-invalid': [], + }, + '/base', + ), + ).toStrictEqual({ + '@': expect.pathToEndWith('src'), + }); + }); +}); + +describe('parseTsConfigToJitiConfig', () => { + it('returns empty object when compiler options are empty', () => { + expect(parseTsConfigToJitiConfig({})).toStrictEqual({}); + }); + + it('includes all options jiti can use', () => { + const compilerOptions: CompilerOptions = { + paths: { + '@app/*': ['src/*'], + '@lib/*': ['lib/*'], + }, + esModuleInterop: true, + sourceMap: true, + jsx: 2, // JsxEmit.React + include: ['**/*.ts'], + + baseUrl: '/base', + }; + + expect(parseTsConfigToJitiConfig(compilerOptions)).toStrictEqual({ + alias: { + '@app': expect.pathToEndWith('src'), + '@lib': expect.pathToEndWith('lib'), + }, + interopDefault: true, + sourceMaps: true, + jsx: true, + }); + }); +}); + +describe('toFileUrl', () => { + it('returns a file:// URL for an absolute path', () => { + const absolutePath = path.resolve('some', 'config.ts'); + expect(toFileUrl(absolutePath)).toBe(pathToFileURL(absolutePath).href); + }); + + it('normalizes Windows absolute paths to file URLs', () => { + const windowsPath = path.win32.join('C:\\', 'Users', 'me', 'config.ts'); + expect(toFileUrl(windowsPath)).toBe('file:///C:/Users/me/config.ts'); + }); +}); diff --git a/packages/utils/src/lib/load-ts-config.ts b/packages/utils/src/lib/load-ts-config.ts new file mode 100644 index 000000000..010732ebf --- /dev/null +++ b/packages/utils/src/lib/load-ts-config.ts @@ -0,0 +1,29 @@ +import path from 'node:path'; +import { parseJsonConfigFileContent, readConfigFile, sys } from 'typescript'; + +export function loadTargetConfig(tsConfigPath: string) { + const resolvedConfigPath = path.resolve(tsConfigPath); + const { config, error } = readConfigFile(resolvedConfigPath, sys.readFile); + + if (error) { + throw new Error( + `Error reading TypeScript config file at ${tsConfigPath}:\n${error.messageText}`, + ); + } + + const parsedConfig = parseJsonConfigFileContent( + config, + sys, + path.dirname(resolvedConfigPath), + {}, + resolvedConfigPath, + ); + + if (parsedConfig.fileNames.length === 0) { + throw new Error( + 'No files matched by the TypeScript configuration. Check your "include", "exclude" or "files" settings.', + ); + } + + return parsedConfig; +} diff --git a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts index df845928a..08f1ed457 100644 --- a/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-config-factory.unit.test.ts @@ -1,16 +1,20 @@ -import { - type E2ETestOptions, - type TestKind, - createVitestConfig, -} from './vitest-config-factory.js'; +import type { E2ETestOptions, TestKind } from './vitest-config-factory.js'; -vi.mock('./vitest-tsconfig-path-aliases.js', () => ({ +const tsconfigPathAliasesMock = vi.hoisted(() => ({ tsconfigPathAliases: vi .fn() .mockReturnValue([{ find: '@test/alias', replacement: '/mock/path' }]), })); +vi.mock('./vitest-tsconfig-path-aliases.js', () => tsconfigPathAliasesMock); + describe('createVitestConfig', () => { + let createVitestConfig: typeof import('./vitest-config-factory.js').createVitestConfig; + + beforeEach(async () => { + vi.clearAllMocks(); + ({ createVitestConfig } = await import('./vitest-config-factory.js')); + }); describe('unit test configuration', () => { it('should create a complete unit test config with all defaults', () => { const config = createVitestConfig('test-package', 'unit'); @@ -139,6 +143,9 @@ describe('createVitestConfig', () => { const config = createVitestConfig('test-package', 'int'); const setupFiles = config.test!.setupFiles; + expect(setupFiles).toContain( + '../../testing/test-setup/src/lib/jiti.int-setup.ts', + ); expect(setupFiles).toContain( '../../testing/test-setup/src/lib/logger.mock.ts', ); diff --git a/testing/test-setup-config/src/lib/vitest-setup-files.ts b/testing/test-setup-config/src/lib/vitest-setup-files.ts index ccc34bbea..a90a84e70 100644 --- a/testing/test-setup-config/src/lib/vitest-setup-files.ts +++ b/testing/test-setup-config/src/lib/vitest-setup-files.ts @@ -39,6 +39,7 @@ const UNIT_TEST_SETUP_FILES = [ */ const INT_TEST_SETUP_FILES = [ '../../testing/test-setup/src/lib/reset.mocks.ts', + '../../testing/test-setup/src/lib/jiti.int-setup.ts', '../../testing/test-setup/src/lib/logger.mock.ts', '../../testing/test-setup/src/lib/chrome-path.mock.ts', ...CUSTOM_MATCHERS, diff --git a/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts b/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts index 554eaafe0..8b78b7368 100644 --- a/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts +++ b/testing/test-setup-config/src/lib/vitest-setup-presets.unit.test.ts @@ -1,26 +1,27 @@ -import * as configFactory from './vitest-config-factory.js'; -import { - createE2ETestConfig, - createIntTestConfig, - createUnitTestConfig, -} from './vitest-setup-presets.js'; - -vi.mock('./vitest-config-factory.js', () => ({ +const configFactoryMock = vi.hoisted(() => ({ createVitestConfig: vi.fn().mockReturnValue('mocked-config'), })); +vi.mock('./vitest-config-factory.js', () => configFactoryMock); + const MOCK_PROJECT_KEY = 'test-package'; describe('vitest-setup-presets', () => { - beforeEach(() => { + let createUnitTestConfig: typeof import('./vitest-setup-presets.js').createUnitTestConfig; + let createIntTestConfig: typeof import('./vitest-setup-presets.js').createIntTestConfig; + let createE2ETestConfig: typeof import('./vitest-setup-presets.js').createE2ETestConfig; + + beforeEach(async () => { vi.clearAllMocks(); + ({ createUnitTestConfig, createIntTestConfig, createE2ETestConfig } = + await import('./vitest-setup-presets.js')); }); describe('createUnitTestConfig', () => { it('should call createVitestConfig with unit kind', () => { const result = createUnitTestConfig(MOCK_PROJECT_KEY); - expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenCalledWith( MOCK_PROJECT_KEY, 'unit', ); @@ -30,7 +31,7 @@ describe('vitest-setup-presets', () => { it('should handle different project names', () => { createUnitTestConfig('my-custom-package'); - expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenCalledWith( 'my-custom-package', 'unit', ); @@ -39,7 +40,10 @@ describe('vitest-setup-presets', () => { it('should handle empty projectKey', () => { createUnitTestConfig(''); - expect(configFactory.createVitestConfig).toHaveBeenCalledWith('', 'unit'); + expect(configFactoryMock.createVitestConfig).toHaveBeenCalledWith( + '', + 'unit', + ); }); }); @@ -47,7 +51,7 @@ describe('vitest-setup-presets', () => { it('should call createVitestConfig with int kind', () => { const result = createIntTestConfig(MOCK_PROJECT_KEY); - expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenCalledWith( MOCK_PROJECT_KEY, 'int', ); @@ -57,7 +61,7 @@ describe('vitest-setup-presets', () => { it('should handle different project names', () => { createIntTestConfig('integration-package'); - expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenCalledWith( 'integration-package', 'int', ); @@ -68,7 +72,7 @@ describe('vitest-setup-presets', () => { it('should call createVitestConfig with e2e kind and no options', () => { const result = createE2ETestConfig(MOCK_PROJECT_KEY); - expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenCalledWith( MOCK_PROJECT_KEY, 'e2e', undefined, @@ -83,7 +87,7 @@ describe('vitest-setup-presets', () => { createE2ETestConfig(MOCK_PROJECT_KEY, options); - expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenCalledWith( MOCK_PROJECT_KEY, 'e2e', options, @@ -93,7 +97,7 @@ describe('vitest-setup-presets', () => { it('should handle testTimeout option', () => { createE2ETestConfig(MOCK_PROJECT_KEY, { testTimeout: 30_000 }); - expect(configFactory.createVitestConfig).toHaveBeenCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenCalledWith( MOCK_PROJECT_KEY, 'e2e', { testTimeout: 30_000 }, @@ -115,17 +119,17 @@ describe('vitest-setup-presets', () => { createIntTestConfig('pkg2'); createE2ETestConfig('pkg3'); - expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenNthCalledWith( 1, 'pkg1', 'unit', ); - expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenNthCalledWith( 2, 'pkg2', 'int', ); - expect(configFactory.createVitestConfig).toHaveBeenNthCalledWith( + expect(configFactoryMock.createVitestConfig).toHaveBeenNthCalledWith( 3, 'pkg3', 'e2e', @@ -140,7 +144,7 @@ describe('vitest-setup-presets', () => { e2e: { test: 'e2e-config' }, }; - vi.mocked(configFactory.createVitestConfig) + vi.mocked(configFactoryMock.createVitestConfig) .mockReturnValueOnce(mockConfigs.unit as any) .mockReturnValueOnce(mockConfigs.int as any) .mockReturnValueOnce(mockConfigs.e2e as any); diff --git a/testing/test-setup/src/lib/jiti.int-setup.ts b/testing/test-setup/src/lib/jiti.int-setup.ts new file mode 100644 index 000000000..b128c7536 --- /dev/null +++ b/testing/test-setup/src/lib/jiti.int-setup.ts @@ -0,0 +1,26 @@ +import { beforeEach, vi } from 'vitest'; +import type { ImportModuleOptions } from '@code-pushup/utils'; + +// Integration test setup - disable jiti caching to avoid stale module resolution +vi.mock('@code-pushup/utils', async () => { + const utils = + await vi.importActual( + '@code-pushup/utils', + ); + + return { + ...utils, + importModule: async (options: ImportModuleOptions) => + // Disable caching in integration tests + utils.importModule({ + ...options, + fsCache: false, + moduleCache: false, + }), + }; +}); + +beforeEach(() => { + // Clear any cached modules between tests + vi.resetModules(); +}); diff --git a/testing/test-utils/src/lib/utils/omit-trace-json.unit.test.ts b/testing/test-utils/src/lib/utils/omit-trace-json.unit.test.ts deleted file mode 100644 index dbf5a079a..000000000 --- a/testing/test-utils/src/lib/utils/omit-trace-json.unit.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { omitTraceJson } from './omit-trace-json.js'; - -describe('omitTraceJson', () => { - it('should return empty string unchanged', () => { - expect(omitTraceJson('')).toBe(''); - }); - - it('should return whitespace-only string unchanged', () => { - expect(omitTraceJson(' \n\t ')).toBe(' \n\t '); - }); - - it('should return empty JSONL unchanged', () => { - expect(omitTraceJson('\n\n')).toBe('\n\n'); - }); - - it('should return minimal event unchanged', () => { - const input = '{"name":"test"}\n'; - expect(omitTraceJson(input)).toBe(input); - }); - - it('should normalize pid field starting from 10001', () => { - const result = omitTraceJson('{"pid":12345}\n'); - const parsed = JSON.parse(result.trim()); - expect(parsed.pid).toBe(10_001); - }); - - it('should normalize tid field starting from 1', () => { - const result = omitTraceJson('{"tid":999}\n'); - const parsed = JSON.parse(result.trim()); - expect(parsed.tid).toBe(1); - }); - - it('should normalize ts field with default baseTimestampUs', () => { - const result = omitTraceJson('{"ts":1234567890}\n'); - const parsed = JSON.parse(result.trim()); - expect(parsed.ts).toBe(1_700_000_005_000_000); - }); - - it('should normalize ts field with custom baseTimestampUs', () => { - const customBase = 2_000_000_000_000_000; - const result = omitTraceJson('{"ts":1234567890}\n', customBase); - const parsed = JSON.parse(result.trim()); - expect(parsed.ts).toBe(customBase); - }); - - it('should normalize id2.local field starting from 0x1', () => { - const result = omitTraceJson('{"id2":{"local":"0xabc123"}}\n'); - const parsed = JSON.parse(result.trim()); - expect(parsed.id2.local).toBe('0x1'); - }); - - it('should preserve event order when timestamps are out of order', () => { - const input = - '{"ts":300,"name":"third"}\n{"ts":100,"name":"first"}\n{"ts":200,"name":"second"}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].name).toBe('third'); - expect(events[1].name).toBe('first'); - expect(events[2].name).toBe('second'); - expect(events[0].ts).toBe(1_700_000_005_000_002); - expect(events[1].ts).toBe(1_700_000_005_000_000); - expect(events[2].ts).toBe(1_700_000_005_000_001); - }); - - it('should preserve event order when PIDs are out of order', () => { - const input = - '{"pid":300,"name":"third"}\n{"pid":100,"name":"first"}\n{"pid":200,"name":"second"}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].name).toBe('third'); - expect(events[1].name).toBe('first'); - expect(events[2].name).toBe('second'); - expect(events[0].pid).toBe(10_003); - expect(events[1].pid).toBe(10_001); - expect(events[2].pid).toBe(10_002); - }); - - it('should preserve event order when TIDs are out of order', () => { - const input = - '{"tid":30,"name":"third"}\n{"tid":10,"name":"first"}\n{"tid":20,"name":"second"}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].name).toBe('third'); - expect(events[1].name).toBe('first'); - expect(events[2].name).toBe('second'); - expect(events[0].tid).toBe(3); - expect(events[1].tid).toBe(1); - expect(events[2].tid).toBe(2); - }); - - it('should preserve event order with mixed out-of-order fields', () => { - const input = - '{"pid":500,"tid":5,"ts":5000,"name":"e"}\n{"pid":100,"tid":1,"ts":1000,"name":"a"}\n{"pid":300,"tid":3,"ts":3000,"name":"c"}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events.map(e => e.name)).toEqual(['e', 'a', 'c']); - expect(events[0].pid).toBe(10_003); - expect(events[1].pid).toBe(10_001); - expect(events[2].pid).toBe(10_002); - }); - - it('should not normalize non-number pid values', () => { - const input = '{"pid":"string"}\n{"pid":null}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].pid).toBe('string'); - expect(events[1].pid).toBeNull(); - }); - - it('should not normalize non-number tid values', () => { - const input = '{"tid":"string"}\n{"tid":null}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].tid).toBe('string'); - expect(events[1].tid).toBeNull(); - }); - - it('should not normalize non-number ts values', () => { - const input = '{"ts":"string"}\n{"ts":null}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].ts).toBe('string'); - expect(events[1].ts).toBeNull(); - }); - - it('should not normalize id2.local when id2 is missing', () => { - const input = '{"name":"test"}\n'; - const result = omitTraceJson(input); - const parsed = JSON.parse(result.trim()); - expect(parsed.id2).toBeUndefined(); - }); - - it('should not normalize id2.local when id2 is not an object', () => { - const input = '{"id2":"string"}\n{"id2":null}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].id2).toBe('string'); - expect(events[1].id2).toBeNull(); - }); - - it('should not normalize id2.local when local is missing', () => { - const input = '{"id2":{"other":"value"}}\n'; - const result = omitTraceJson(input); - const parsed = JSON.parse(result.trim()); - expect(parsed.id2.local).toBeUndefined(); - expect(parsed.id2.other).toBe('value'); - }); - - it('should not normalize id2.local when local is not a string', () => { - const input = '{"id2":{"local":123}}\n{"id2":{"local":null}}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].id2.local).toBe(123); - expect(events[1].id2.local).toBeNull(); - }); - - it('should map duplicate values to same normalized value', () => { - const input = '{"pid":100}\n{"pid":200}\n{"pid":100}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].pid).toBe(10_001); - expect(events[1].pid).toBe(10_002); - expect(events[2].pid).toBe(10_001); - }); - - it('should handle duplicate timestamps correctly', () => { - const input = '{"ts":1000}\n{"ts":2000}\n{"ts":1000}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - expect(events[0].ts).toBe(1_700_000_005_000_000); - expect(events[1].ts).toBe(1_700_000_005_000_002); - expect(events[2].ts).toBe(1_700_000_005_000_000); - }); - - it('should preserve other id2 properties when normalizing local', () => { - const input = - '{"id2":{"local":"0xabc","other":"value","nested":{"key":123}}}\n'; - const result = omitTraceJson(input); - const parsed = JSON.parse(result.trim()); - expect(parsed.id2.local).toBe('0x1'); - expect(parsed.id2.other).toBe('value'); - expect(parsed.id2.nested).toEqual({ key: 123 }); - }); - - it('should map multiple id2.local values to incremental hex', () => { - const input = - '{"id2":{"local":"0xabc"}}\n{"id2":{"local":"0xdef"}}\n{"id2":{"local":"0x123"}}\n'; - const result = omitTraceJson(input); - const events = result - .trim() - .split('\n') - .map(line => JSON.parse(line)); - const locals = events.map(e => e.id2.local).sort(); - expect(locals).toEqual(['0x1', '0x2', '0x3']); - }); - - it('should output valid JSONL with trailing newline', () => { - const result = omitTraceJson('{"pid":123}\n'); - expect(result).toMatch(/\n$/); - expect(() => JSON.parse(result.trim())).not.toThrow(); - }); -}); diff --git a/tools/zod2md-jsdocs/eslint.config.js b/tools/zod2md-jsdocs/eslint.config.js index 467b6c94b..1f8586919 100644 --- a/tools/zod2md-jsdocs/eslint.config.js +++ b/tools/zod2md-jsdocs/eslint.config.js @@ -1,6 +1,6 @@ -const baseConfig = require('../../eslint.config.js').default; +import baseConfig from '../../eslint.config.js'; -module.exports = [ +export default [ ...baseConfig, { files: ['**/*.json'],