From f25c32bed6d816e843ffa3cb4ab1fc48e0242b19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:46:55 +0000 Subject: [PATCH 1/7] POC TypeScript plugin loading with esbuild fallback Agent-Logs-Url: https://github.com/github/accessibility-scanner/sessions/874fd265-9e9f-4ac4-930d-8d26cb424035 Co-authored-by: abdulahmad307 <204748719+abdulahmad307@users.noreply.github.com> --- .github/actions/find/package-lock.json | 458 ++++++++++++++++++ .github/actions/find/package.json | 3 +- .github/actions/find/src/dynamicImport.ts | 4 + .github/actions/find/src/pluginManager.ts | 48 +- .../actions/find/tests/pluginManager.test.ts | 72 +++ 5 files changed, 583 insertions(+), 2 deletions(-) diff --git a/.github/actions/find/package-lock.json b/.github/actions/find/package-lock.json index 60c8fcf8..b84b795f 100644 --- a/.github/actions/find/package-lock.json +++ b/.github/actions/find/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@actions/core": "^3.0.0", "@axe-core/playwright": "^4.11.1", + "esbuild": "^0.28.0", "playwright": "^1.58.2" }, "devDependencies": { @@ -65,6 +66,422 @@ "playwright-core": ">= 1.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@types/node": { "version": "25.4.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", @@ -84,6 +501,47 @@ "node": ">=4" } }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", diff --git a/.github/actions/find/package.json b/.github/actions/find/package.json index f68e6c7a..3bc52a1a 100644 --- a/.github/actions/find/package.json +++ b/.github/actions/find/package.json @@ -15,10 +15,11 @@ "dependencies": { "@actions/core": "^3.0.0", "@axe-core/playwright": "^4.11.1", + "esbuild": "^0.28.0", "playwright": "^1.58.2" }, "devDependencies": { "@types/node": "^25.4.0", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/.github/actions/find/src/dynamicImport.ts b/.github/actions/find/src/dynamicImport.ts index d8e17446..19eff70f 100644 --- a/.github/actions/find/src/dynamicImport.ts +++ b/.github/actions/find/src/dynamicImport.ts @@ -19,5 +19,9 @@ import {pathToFileURL} from 'url' // // - so this looks like a reasonable approach export function dynamicImport(path: string) { + if (path.startsWith('data:') || path.startsWith('file:')) { + return import(path) + } + return import(pathToFileURL(path).href) } diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index bec7ddfe..72463b11 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -1,6 +1,7 @@ import * as fs from 'fs' import * as path from 'path' import {fileURLToPath} from 'url' +import * as esbuild from 'esbuild' import {dynamicImport} from './dynamicImport.js' import type {Finding} from './types.d.js' import playwright from 'playwright' @@ -96,7 +97,14 @@ export async function loadPluginsFromPath({ const pluginFolderPath = path.join(pluginsPath, pluginFolder) if (fs.existsSync(pluginFolderPath) && fs.lstatSync(pluginFolderPath).isDirectory()) { - const plugin = await dynamicImport(path.join(pluginsPath, pluginFolder, 'index.js')) + const pluginEntryPath = resolvePluginEntryPath(pluginFolderPath) + + if (!pluginEntryPath) { + core.info(`Skipping plugin folder without index.ts or index.js: ${pluginFolder}`) + continue + } + + const plugin = await loadPluginModule(pluginEntryPath) if (skipBuiltInPlugins?.includes(plugin.name)) { core.info(`Skipping built-in plugin: ${plugin.name}`) @@ -116,6 +124,44 @@ export async function loadPluginsFromPath({ } } +function resolvePluginEntryPath(pluginFolderPath: string) { + const typescriptPluginPath = path.join(pluginFolderPath, 'index.ts') + if (fs.existsSync(typescriptPluginPath)) { + return typescriptPluginPath + } + + const javascriptPluginPath = path.join(pluginFolderPath, 'index.js') + if (fs.existsSync(javascriptPluginPath)) { + return javascriptPluginPath + } + + return null +} + +async function loadPluginModule(pluginEntryPath: string) { + if (pluginEntryPath.endsWith('.js')) { + return dynamicImport(pluginEntryPath) + } + + const esbuildResult = await esbuild.build({ + entryPoints: [pluginEntryPath], + write: false, + bundle: true, + format: 'esm', + platform: 'node', + target: 'node24', + sourcemap: 'inline', + }) + + const outputFileContents = esbuildResult.outputFiles[0]?.text + if (!outputFileContents) { + throw new Error(`failed to compile plugin: ${pluginEntryPath}`) + } + + const base64CompiledPlugin = Buffer.from(outputFileContents).toString('base64') + return dynamicImport(`data:text/javascript;base64,${base64CompiledPlugin}`) +} + type InvokePluginParams = PluginDefaultParams & { plugin: Plugin } diff --git a/.github/actions/find/tests/pluginManager.test.ts b/.github/actions/find/tests/pluginManager.test.ts index 3aced15d..a5d61663 100644 --- a/.github/actions/find/tests/pluginManager.test.ts +++ b/.github/actions/find/tests/pluginManager.test.ts @@ -1,6 +1,7 @@ import {describe, it, expect, vi, beforeEach} from 'vitest' import * as fs from 'fs' +import * as esbuild from 'esbuild' import * as dynamicImportModule from '../src/dynamicImport.js' import * as pluginManager from '../src/pluginManager.js' import * as core from '@actions/core' @@ -8,6 +9,7 @@ import * as core from '@actions/core' // - enable spying on fs // https://vitest.dev/guide/browser/#limitations vi.mock('fs', {spy: true}) +vi.mock('esbuild', {spy: true}) vi.mock('../src/pluginManager.js', {spy: true}) vi.mock('@actions/core', {spy: true}) @@ -30,6 +32,9 @@ describe('loadPlugins', () => { } as unknown as fs.Stats }) vi.spyOn(fs, 'existsSync').mockImplementation(() => true) + vi.spyOn(esbuild, 'build').mockResolvedValue({ + outputFiles: [{text: 'export const name = "compiled-plugin"; export default async function run() {}'}], + } as unknown as esbuild.BuildResult) }) describe('when plugins are not loaded', () => { @@ -89,4 +94,71 @@ describe('loadPlugins', () => { expect(infoSpy).toHaveBeenCalledWith('Skipping built-in plugin: reflow-scan') }) }) + + describe('plugin entry resolution', () => { + it('prefers index.ts over index.js when both exist', async () => { + pluginManager.clearCache() + const existsSyncSpy = vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + const pathText = String(filePath) + if (pathText.endsWith('/folder-a') || pathText.endsWith('/folder-b')) { + return true + } + + if (pathText.endsWith('/index.ts')) { + return true + } + + if (pathText.endsWith('/index.js')) { + return true + } + + return false + }) + const dynamicImportSpy = vi.spyOn(dynamicImportModule, 'dynamicImport') + const buildSpy = vi.spyOn(esbuild, 'build') + const startingBuildCalls = buildSpy.mock.calls.length + const startingDynamicImportCalls = dynamicImportSpy.mock.calls.length + + await pluginManager.loadPluginsFromPath({pluginsPath: '/tmp/plugins'}) + + expect(existsSyncSpy).toHaveBeenCalled() + expect(buildSpy.mock.calls.length - startingBuildCalls).toBe(2) + const newDynamicImportCalls = dynamicImportSpy.mock.calls.slice(startingDynamicImportCalls) + expect(newDynamicImportCalls[0]?.[0]).toMatch(/^data:text\/javascript;base64,/) + expect(newDynamicImportCalls[1]?.[0]).toMatch(/^data:text\/javascript;base64,/) + }) + + it('falls back to index.js when index.ts does not exist', async () => { + pluginManager.clearCache() + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + const pathText = String(filePath) + if (pathText.endsWith('/folder-a') || pathText.endsWith('/folder-b')) { + return true + } + + if (pathText.endsWith('/index.ts')) { + return false + } + + if (pathText.endsWith('/index.js')) { + return true + } + + return false + }) + const dynamicImportSpy = vi.spyOn(dynamicImportModule, 'dynamicImport') + const buildSpy = vi.spyOn(esbuild, 'build') + const startingBuildCalls = buildSpy.mock.calls.length + const startingDynamicImportCalls = dynamicImportSpy.mock.calls.length + + await pluginManager.loadPluginsFromPath({pluginsPath: '/tmp/plugins'}) + + expect(buildSpy.mock.calls.length - startingBuildCalls).toBe(0) + const newDynamicImportCalls = dynamicImportSpy.mock.calls.slice(startingDynamicImportCalls) + expect(newDynamicImportCalls.map(call => call[0])).toEqual([ + '/tmp/plugins/folder-a/index.js', + '/tmp/plugins/folder-b/index.js', + ]) + }) + }) }) From 3646661aa720c055acd7ddd31c6f4c976e38e81d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:48:07 +0000 Subject: [PATCH 2/7] Improve esbuild no-output error messaging Agent-Logs-Url: https://github.com/github/accessibility-scanner/sessions/874fd265-9e9f-4ac4-930d-8d26cb424035 Co-authored-by: abdulahmad307 <204748719+abdulahmad307@users.noreply.github.com> --- .github/actions/find/src/pluginManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 72463b11..0a5c1c33 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -155,7 +155,7 @@ async function loadPluginModule(pluginEntryPath: string) { const outputFileContents = esbuildResult.outputFiles[0]?.text if (!outputFileContents) { - throw new Error(`failed to compile plugin: ${pluginEntryPath}`) + throw new Error(`esbuild produced no output for plugin: ${pluginEntryPath}`) } const base64CompiledPlugin = Buffer.from(outputFileContents).toString('base64') From 4d62c1a7a7acc4d4be0f499b18d889f9abcdac38 Mon Sep 17 00:00:00 2001 From: Abdul Ahmad Date: Mon, 13 Apr 2026 13:07:56 -0400 Subject: [PATCH 3/7] refactor copilot code - change reflow to .ts file - add temporary test scan for testing js files --- .github/actions/find/src/dynamicImport.ts | 5 +- .github/actions/find/src/pluginManager.ts | 59 ++++++++++--------- .../reflow-scan/{index.js => index.ts} | 4 +- .../scanner-plugins/temp-test-scan/index.js | 5 ++ 4 files changed, 41 insertions(+), 32 deletions(-) rename .github/scanner-plugins/reflow-scan/{index.js => index.ts} (91%) create mode 100644 .github/scanner-plugins/temp-test-scan/index.js diff --git a/.github/actions/find/src/dynamicImport.ts b/.github/actions/find/src/dynamicImport.ts index 19eff70f..e5aa3e4e 100644 --- a/.github/actions/find/src/dynamicImport.ts +++ b/.github/actions/find/src/dynamicImport.ts @@ -1,4 +1,4 @@ -import {pathToFileURL} from 'url' +import { pathToFileURL } from 'url' // - this exists because it looks like there's no straight-forward // way to mock the dynamic import function, so mocking this instead @@ -19,6 +19,9 @@ import {pathToFileURL} from 'url' // // - so this looks like a reasonable approach export function dynamicImport(path: string) { + // - this check is for non-file imports. + // - this can be encountered when using esbuild to compile TS plugin files + // at run-time if (path.startsWith('data:') || path.startsWith('file:')) { return import(path) } diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 0a5c1c33..098cbd75 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -1,9 +1,9 @@ import * as fs from 'fs' import * as path from 'path' -import {fileURLToPath} from 'url' +import { fileURLToPath } from 'url' import * as esbuild from 'esbuild' -import {dynamicImport} from './dynamicImport.js' -import type {Finding} from './types.d.js' +import { dynamicImport } from './dynamicImport.js' +import type { Finding } from './types.d.js' import playwright from 'playwright' import * as core from '@actions/core' @@ -60,7 +60,7 @@ export async function loadBuiltInPlugins() { core.info('Loading built-in plugins') const pluginsPath = path.join(__dirname, '../../../scanner-plugins/') - await loadPluginsFromPath({pluginsPath}) + await loadPluginsFromPath({ pluginsPath }) } // exported for mocking/testing. not for actual use @@ -80,7 +80,7 @@ export async function loadCustomPlugins() { return } - await loadPluginsFromPath({pluginsPath, skipBuiltInPlugins: BUILT_IN_PLUGINS}) + await loadPluginsFromPath({ pluginsPath, skipBuiltInPlugins: BUILT_IN_PLUGINS }) } // exported for mocking/testing. not for actual use @@ -97,15 +97,17 @@ export async function loadPluginsFromPath({ const pluginFolderPath = path.join(pluginsPath, pluginFolder) if (fs.existsSync(pluginFolderPath) && fs.lstatSync(pluginFolderPath).isDirectory()) { - const pluginEntryPath = resolvePluginEntryPath(pluginFolderPath) + let plugin = await loadPluginViaTsFile(pluginFolderPath) - if (!pluginEntryPath) { + if (!plugin) { + plugin = await loadPluginViaJsFile(pluginFolderPath) + } + + if (!plugin) { core.info(`Skipping plugin folder without index.ts or index.js: ${pluginFolder}`) continue } - const plugin = await loadPluginModule(pluginEntryPath) - if (skipBuiltInPlugins?.includes(plugin.name)) { core.info(`Skipping built-in plugin: ${plugin.name}`) continue @@ -124,23 +126,11 @@ export async function loadPluginsFromPath({ } } -function resolvePluginEntryPath(pluginFolderPath: string) { - const typescriptPluginPath = path.join(pluginFolderPath, 'index.ts') - if (fs.existsSync(typescriptPluginPath)) { - return typescriptPluginPath - } - - const javascriptPluginPath = path.join(pluginFolderPath, 'index.js') - if (fs.existsSync(javascriptPluginPath)) { - return javascriptPluginPath - } - - return null -} - -async function loadPluginModule(pluginEntryPath: string) { - if (pluginEntryPath.endsWith('.js')) { - return dynamicImport(pluginEntryPath) +async function loadPluginViaTsFile(pluginFolderPath: string) { + const pluginEntryPath = path.join(pluginFolderPath, 'index.ts') + if (!fs.existsSync(pluginEntryPath)) { + core.info(`No index.ts found for plugin at path: ${pluginFolderPath}`) + return } const esbuildResult = await esbuild.build({ @@ -155,16 +145,27 @@ async function loadPluginModule(pluginEntryPath: string) { const outputFileContents = esbuildResult.outputFiles[0]?.text if (!outputFileContents) { - throw new Error(`esbuild produced no output for plugin: ${pluginEntryPath}`) + core.info(`esbuild produced no output for plugin: ${pluginEntryPath}`) + return } const base64CompiledPlugin = Buffer.from(outputFileContents).toString('base64') return dynamicImport(`data:text/javascript;base64,${base64CompiledPlugin}`) } +async function loadPluginViaJsFile(pluginFolderPath: string) { + const pluginEntryPath = path.join(pluginFolderPath, 'index.js') + if (!fs.existsSync(pluginEntryPath)) { + core.info(`No index.js found for plugin at path: ${pluginFolderPath}`) + return + } + + return dynamicImport(pluginEntryPath) +} + type InvokePluginParams = PluginDefaultParams & { plugin: Plugin } -export function invokePlugin({plugin, page, addFinding}: InvokePluginParams) { - return plugin.default({page, addFinding}) +export function invokePlugin({ plugin, page, addFinding }: InvokePluginParams) { + return plugin.default({ page, addFinding }) } diff --git a/.github/scanner-plugins/reflow-scan/index.js b/.github/scanner-plugins/reflow-scan/index.ts similarity index 91% rename from .github/scanner-plugins/reflow-scan/index.js rename to .github/scanner-plugins/reflow-scan/index.ts index 077835fe..fa4fa5d1 100644 --- a/.github/scanner-plugins/reflow-scan/index.js +++ b/.github/scanner-plugins/reflow-scan/index.ts @@ -1,9 +1,9 @@ -export default async function reflowScan({page, addFinding} = {}) { +export default async function reflowScan({ page, addFinding } = {}) { const originalViewport = page.viewportSize() const url = page.url() // Check for horizontal scrolling at 320x256 viewport try { - await page.setViewportSize({width: 320, height: 256}) + await page.setViewportSize({ width: 320, height: 256 }) const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth) const clientWidth = await page.evaluate(() => document.documentElement.clientWidth) diff --git a/.github/scanner-plugins/temp-test-scan/index.js b/.github/scanner-plugins/temp-test-scan/index.js new file mode 100644 index 00000000..2e8787ca --- /dev/null +++ b/.github/scanner-plugins/temp-test-scan/index.js @@ -0,0 +1,5 @@ +export default async function reflowScan({ page, addFinding } = {}) { + console.log('testing js file import') +} + +export const name = 'temp-test-scan' From 588a2cc3a98ccc4f463384e6ea269d6a240f54f1 Mon Sep 17 00:00:00 2001 From: Abdul Ahmad Date: Mon, 13 Apr 2026 13:17:57 -0400 Subject: [PATCH 4/7] formatting fixes --- .github/actions/find/src/dynamicImport.ts | 6 +++--- .github/actions/find/src/pluginManager.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/actions/find/src/dynamicImport.ts b/.github/actions/find/src/dynamicImport.ts index e5aa3e4e..493aa029 100644 --- a/.github/actions/find/src/dynamicImport.ts +++ b/.github/actions/find/src/dynamicImport.ts @@ -1,4 +1,4 @@ -import { pathToFileURL } from 'url' +import {pathToFileURL} from 'url' // - this exists because it looks like there's no straight-forward // way to mock the dynamic import function, so mocking this instead @@ -19,9 +19,9 @@ import { pathToFileURL } from 'url' // // - so this looks like a reasonable approach export function dynamicImport(path: string) { - // - this check is for non-file imports. + // - this if condition is for non-file imports. // - this can be encountered when using esbuild to compile TS plugin files - // at run-time + // at run-time. if (path.startsWith('data:') || path.startsWith('file:')) { return import(path) } diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 098cbd75..7c7e84e7 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -1,9 +1,9 @@ import * as fs from 'fs' import * as path from 'path' -import { fileURLToPath } from 'url' +import {fileURLToPath} from 'url' import * as esbuild from 'esbuild' -import { dynamicImport } from './dynamicImport.js' -import type { Finding } from './types.d.js' +import {dynamicImport} from './dynamicImport.js' +import type {Finding} from './types.d.js' import playwright from 'playwright' import * as core from '@actions/core' @@ -60,7 +60,7 @@ export async function loadBuiltInPlugins() { core.info('Loading built-in plugins') const pluginsPath = path.join(__dirname, '../../../scanner-plugins/') - await loadPluginsFromPath({ pluginsPath }) + await loadPluginsFromPath({pluginsPath}) } // exported for mocking/testing. not for actual use @@ -80,7 +80,7 @@ export async function loadCustomPlugins() { return } - await loadPluginsFromPath({ pluginsPath, skipBuiltInPlugins: BUILT_IN_PLUGINS }) + await loadPluginsFromPath({pluginsPath, skipBuiltInPlugins: BUILT_IN_PLUGINS}) } // exported for mocking/testing. not for actual use @@ -166,6 +166,6 @@ async function loadPluginViaJsFile(pluginFolderPath: string) { type InvokePluginParams = PluginDefaultParams & { plugin: Plugin } -export function invokePlugin({ plugin, page, addFinding }: InvokePluginParams) { - return plugin.default({ page, addFinding }) +export function invokePlugin({plugin, page, addFinding}: InvokePluginParams) { + return plugin.default({page, addFinding}) } From ef53f462da8c5619bf33b9b2185e31e25291adca Mon Sep 17 00:00:00 2001 From: Abdul Ahmad Date: Tue, 14 Apr 2026 09:41:30 -0400 Subject: [PATCH 5/7] log custom plugins --- .github/actions/find/src/pluginManager.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 7c7e84e7..cd19ef5e 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -67,6 +67,13 @@ export async function loadBuiltInPlugins() { export async function loadCustomPlugins() { core.info('Loading custom plugins') const pluginsPath = path.join(process.cwd(), '.github/scanner-plugins/') + console.log('plugins path', pluginsPath); + + const fileNames = fs.readdirSync(process.cwd()) + fileNames.forEach((fn: string) => { + console.log('file in cwd:', fn) + }); + // - currently, the plugin manager will abort loading // all plugins if there's an error From 3472732cd81e43cc7547e72c4c1172c57dc55493 Mon Sep 17 00:00:00 2001 From: Abdul Ahmad Date: Tue, 14 Apr 2026 11:27:49 -0400 Subject: [PATCH 6/7] testing --- .github/actions/find/src/pluginManager.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index cd19ef5e..9c241e6f 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -69,6 +69,18 @@ export async function loadCustomPlugins() { const pluginsPath = path.join(process.cwd(), '.github/scanner-plugins/') console.log('plugins path', pluginsPath); + const cwd = process.cwd() + console.log('current working directory:', cwd) + const underRootPath = path.join(cwd,'..') + console.log('path under root:', underRootPath) + const underRoot = fs.readdirSync(underRootPath) + if (Array.isArray(underRoot)) { + underRoot.forEach(element => { + console.log('file under root:', element) + }); + } + + const fileNames = fs.readdirSync(process.cwd()) fileNames.forEach((fn: string) => { console.log('file in cwd:', fn) From ec52e36f824b74e25762174407beeecde3496d84 Mon Sep 17 00:00:00 2001 From: Abdul Ahmad Date: Wed, 15 Apr 2026 11:05:49 -0400 Subject: [PATCH 7/7] remove testing logs - add new success path logs --- .github/actions/find/src/pluginManager.ts | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/.github/actions/find/src/pluginManager.ts b/.github/actions/find/src/pluginManager.ts index 9c241e6f..2adb75ea 100644 --- a/.github/actions/find/src/pluginManager.ts +++ b/.github/actions/find/src/pluginManager.ts @@ -67,25 +67,6 @@ export async function loadBuiltInPlugins() { export async function loadCustomPlugins() { core.info('Loading custom plugins') const pluginsPath = path.join(process.cwd(), '.github/scanner-plugins/') - console.log('plugins path', pluginsPath); - - const cwd = process.cwd() - console.log('current working directory:', cwd) - const underRootPath = path.join(cwd,'..') - console.log('path under root:', underRootPath) - const underRoot = fs.readdirSync(underRootPath) - if (Array.isArray(underRoot)) { - underRoot.forEach(element => { - console.log('file under root:', element) - }); - } - - - const fileNames = fs.readdirSync(process.cwd()) - fileNames.forEach((fn: string) => { - console.log('file in cwd:', fn) - }); - // - currently, the plugin manager will abort loading // all plugins if there's an error @@ -123,7 +104,7 @@ export async function loadPluginsFromPath({ } if (!plugin) { - core.info(`Skipping plugin folder without index.ts or index.js: ${pluginFolder}`) + core.info(`Skipping plugin without index.ts or index.js file: ${pluginFolder}`) continue } @@ -152,6 +133,7 @@ async function loadPluginViaTsFile(pluginFolderPath: string) { return } + core.info(`index.ts found for plugin at path: ${pluginFolderPath}`) const esbuildResult = await esbuild.build({ entryPoints: [pluginEntryPath], write: false, @@ -179,6 +161,7 @@ async function loadPluginViaJsFile(pluginFolderPath: string) { return } + core.info(`index.js found for plugin at path: ${pluginFolderPath}`) return dynamicImport(pluginEntryPath) }