diff --git a/.gitignore b/.gitignore index 06fe1b0..bce315c 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,10 @@ erl_crash.dump # ReScript /lib/bs/ /.bsb.lock +**/lib/bs/ +**/lib/ocaml/ +**/.bsb.lock +**/.merlin # Python (SaltStack only) __pycache__/ diff --git a/rescript.json b/rescript.json deleted file mode 100644 index 51fb15f..0000000 --- a/rescript.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "accessibility-everywhere-res", - "sources": "src", - "package-specs": { "module": "es6", "in-source": true }, - "suffix": ".mjs", - "bs-dependencies": ["@rescript/core"] -} diff --git a/tools/cli/package-lock.json b/tools/cli/package-lock.json new file mode 100644 index 0000000..c5ab389 --- /dev/null +++ b/tools/cli/package-lock.json @@ -0,0 +1,487 @@ +{ + "name": "@accessibility-everywhere/cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@accessibility-everywhere/cli", + "version": "1.0.0", + "dependencies": { + "chalk": "^5.3.0", + "cli-table3": "^0.6.3", + "commander": "^11.1.0", + "fs-extra": "^11.2.0", + "ora": "^7.0.1" + }, + "bin": { + "a11y-scan": "src/Cli.mjs", + "accessibility-scan": "src/Cli.mjs" + }, + "devDependencies": { + "@rescript/core": "^1.5.0", + "rescript": "^11.1.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@rescript/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rescript/core/-/core-1.6.1.tgz", + "integrity": "sha512-vyb5k90ck+65Fgui+5vCja/mUfzKaK3kOPT4Z6aAJdHLH1eljEi1zKhXroCiCtpNLSWp8k4ulh1bdB5WS0hvqA==", + "dev": true, + "peerDependencies": { + "rescript": ">=11.1.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" + }, + "node_modules/ora/node_modules/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rescript": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/rescript/-/rescript-11.1.4.tgz", + "integrity": "sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "bsc": "bsc", + "bstracing": "lib/bstracing", + "rescript": "rescript" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + } + } +} diff --git a/tools/cli/package.json b/tools/cli/package.json index 683c0a6..4c2236a 100644 --- a/tools/cli/package.json +++ b/tools/cli/package.json @@ -2,28 +2,26 @@ "name": "@accessibility-everywhere/cli", "version": "1.0.0", "description": "Command-line tool for accessibility scanning", - "main": "dist/index.js", + "main": "src/Cli.mjs", + "type": "module", "bin": { - "a11y-scan": "dist/cli.js", - "accessibility-scan": "dist/cli.js" + "a11y-scan": "src/Cli.mjs", + "accessibility-scan": "src/Cli.mjs" }, "scripts": { - "build": "tsc", - "dev": "ts-node src/cli.ts", - "test": "jest" + "build": "rescript build", + "dev": "rescript build -w", + "clean": "rescript clean" }, "dependencies": { - "@accessibility-everywhere/scanner": "^1.0.0", "commander": "^11.1.0", - "chalk": "^4.1.2", - "ora": "^5.4.1", + "chalk": "^5.3.0", + "ora": "^7.0.1", "cli-table3": "^0.6.3", "fs-extra": "^11.2.0" }, "devDependencies": { - "@types/node": "^20.10.0", - "@types/fs-extra": "^11.0.4", - "typescript": "^5.3.2", - "ts-node": "^10.9.2" + "rescript": "^11.1.0", + "@rescript/core": "^1.5.0" } } diff --git a/tools/cli/rescript.json b/tools/cli/rescript.json new file mode 100644 index 0000000..cc955e2 --- /dev/null +++ b/tools/cli/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "@accessibility-everywhere/cli", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": { + "module": "esmodule", + "in-source": true + }, + "suffix": ".mjs", + "bs-dependencies": ["@rescript/core"], + "bsc-flags": ["-open RescriptCore"] +} diff --git a/tools/cli/src/Cli.mjs b/tools/cli/src/Cli.mjs new file mode 100644 index 0000000..5bc4bdb --- /dev/null +++ b/tools/cli/src/Cli.mjs @@ -0,0 +1,330 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import Ora from "ora"; +import * as Path from "path"; +import Chalk from "chalk"; +import * as Js_exn from "rescript/lib/es6/js_exn.js"; +import * as FsExtra from "fs-extra"; +import * as Core__Int from "@rescript/core/src/Core__Int.mjs"; +import * as Commander from "commander"; +import CliTable3 from "cli-table3"; +import * as Core__Option from "@rescript/core/src/Core__Option.mjs"; +import * as Caml_js_exceptions from "rescript/lib/es6/caml_js_exceptions.js"; +import * as Scanner from "@accessibility-everywhere/scanner"; + +var Scanner$1 = {}; + +var Commander$1 = {}; + +var Chalk$1 = {}; + +var Ora$1 = {}; + +var Table = {}; + +var Fs = {}; + +var Path$1 = {}; + +var Process = {}; + +var chalk = Chalk; + +function getGrade(score) { + if (score >= 90) { + return "A"; + } else if (score >= 80) { + return "B"; + } else if (score >= 70) { + return "C"; + } else if (score >= 60) { + return "D"; + } else { + return "F"; + } +} + +var bar = "=".repeat(70); + +function parseWcagLevel(s) { + switch (s) { + case "A" : + return "A"; + case "AAA" : + return "AAA"; + default: + return "AA"; + } +} + +function wcagLabel(w) { + if (w === "AA") { + return "AA"; + } else if (w === "AAA") { + return "AAA"; + } else { + return "A"; + } +} + +function impactColored(impact) { + var label = impact === "serious" ? "serious" : ( + impact === "minor" ? "minor" : ( + impact === "critical" ? "critical" : "moderate" + ) + ); + if (impact === "serious") { + return chalk.red(label); + } else if (impact === "minor") { + return chalk.blue(label); + } else if (impact === "critical") { + return chalk.bold.red(label); + } else { + return chalk.yellow(label); + } +} + +function scoreColored(score) { + if (score >= 90) { + return chalk.bold.green(score.toString()); + } else if (score >= 70) { + return chalk.bold.yellow(score.toString()); + } else { + return chalk.bold.red(score.toString()); + } +} + +function generateMarkdown(result, url) { + var header = "# Accessibility Report\n\n**URL:** " + url + "\n**Score:** " + result.score.toString() + "/100\n\n## Violations\n\n"; + var body = result.violations.map(function (v, i) { + return "### " + (i + 1 | 0).toString() + ". " + v.help + "\n\n" + ("- **Impact:** " + v.impact + "\n") + ("- **Instances:** " + v.nodes.length.toString() + "\n") + ("- **Help:** " + v.helpUrl + "\n\n"); + }).join(""); + return header + body; +} + +async function scanAction(url, options) { + var spinner = Ora("Scanning for accessibility issues...").start(); + try { + var scanner = Scanner.createScanner(); + var result = await scanner.scan({ + url: url, + wcagLevel: parseWcagLevel(options.level), + screenshot: options.screenshot + }); + spinner.succeed("Scan complete!"); + console.log("\n" + bar); + console.log(chalk.bold.blue("Accessibility Report")); + console.log(bar); + console.log("URL: " + url); + console.log("WCAG Level: " + options.level); + console.log("Score: " + scoreColored(result.score) + "/100 (Grade " + getGrade(result.score) + ")"); + console.log(bar + "\n"); + var summaryTable = new CliTable3({ + head: [ + "Metric", + "Count" + ], + colWidths: [ + 30, + 10 + ] + }); + summaryTable.push([ + "āœ… Passes", + result.passes.length.toString() + ]); + summaryTable.push([ + "āŒ Violations", + result.violations.length.toString() + ]); + summaryTable.push([ + "āš ļø Needs Review", + result.incomplete.length.toString() + ]); + console.log(summaryTable.toString() + "\n"); + if (result.violations.length > 0) { + console.log(chalk.bold.red("Found " + result.violations.length.toString() + " violations:\n")); + var format = options.format; + if (format === "table") { + var violationsTable = new CliTable3({ + head: [ + "Impact", + "Description", + "Instances", + "WCAG" + ], + colWidths: [ + 12, + 50, + 10, + 15 + ] + }); + result.violations.forEach(function (v) { + violationsTable.push([ + impactColored(v.impact), + v.description, + v.nodes.length.toString(), + v.tags.join(", ") + ]); + }); + console.log(violationsTable.toString()); + } else if (format === "markdown") { + console.log(generateMarkdown(result, url)); + } else { + console.log(JSON.stringify(result, null, 2)); + } + } else { + console.log(chalk.bold.green("šŸŽ‰ No violations found! Great job!")); + } + var out = options.output; + if (out !== undefined) { + await FsExtra.writeJson(out, result, { + spaces: 2 + }); + console.log(chalk.gray("\nāœ“ Results saved to " + out)); + } + return process.exit(result.violations.length > 0 ? 1 : 0); + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + spinner.fail("Scan failed"); + console.error(chalk.red(Core__Option.getOr(err._1.message, "unknown error"))); + return process.exit(1); + } + throw err; + } +} + +async function ciAction(url, options) { + var spinner = Ora("Running CI scan...").start(); + try { + var scanner = Scanner.createScanner(); + var result = await scanner.scan({ + url: url, + wcagLevel: parseWcagLevel(options.level) + }); + spinner.succeed("CI scan complete"); + var minScore = Core__Option.getOr(Core__Int.fromString(options.minScore, undefined), 70); + var failOnViolations = Core__Option.getOr(options.failOnViolations, false); + console.log("Score: " + result.score.toString() + "/100"); + console.log("Violations: " + result.violations.length.toString()); + if (failOnViolations && result.violations.length > 0) { + console.error(chalk.red("āœ— Failed: Found " + result.violations.length.toString() + " violations")); + process.exit(1); + } + if (result.score < minScore) { + console.error(chalk.red("āœ— Failed: Score " + result.score.toString() + " below minimum " + minScore.toString())); + process.exit(1); + } + console.log(chalk.green("āœ“ Passed all checks")); + return process.exit(0); + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + spinner.fail("CI scan failed"); + console.error(chalk.red(Core__Option.getOr(err._1.message, "unknown error"))); + return process.exit(1); + } + throw err; + } +} + +async function batchAction(file, options) { + try { + var raw = await FsExtra.readFile(file, "utf-8"); + var urls = raw.split("\n").map(function (prim) { + return prim.trim(); + }).filter(function (s) { + if (s !== "") { + return !s.startsWith("#"); + } else { + return false; + } + }); + console.log(chalk.blue("Scanning " + urls.length.toString() + " URLs...")); + var outDir = Core__Option.getOr(options.output, "./scan-results"); + await FsExtra.ensureDir(outDir); + var scanner = Scanner.createScanner(); + var completed = 0; + var failed = 0; + var total = urls.length; + for(var i = 0; i < total; ++i){ + var url = urls[i]; + var spinner = Ora("[" + (i + 1 | 0).toString() + "/" + total.toString() + "] " + url).start(); + try { + var result = await scanner.scan({ + url: url, + wcagLevel: parseWcagLevel(options.level) + }); + var safeName = url.replace(/[^a-z0-9]/gi, "_") + ".json"; + var filepath = Path.join(outDir, safeName); + await FsExtra.writeJson(filepath, result, { + spaces: 2 + }); + spinner.succeed(url + " - Score: " + result.score.toString()); + completed = completed + 1 | 0; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + spinner.fail(url + " - " + Core__Option.getOr(err._1.message, "unknown error")); + failed = failed + 1 | 0; + } else { + throw err; + } + } + } + console.log(chalk.green("\nāœ“ Completed: " + completed.toString())); + if (failed > 0) { + console.log(chalk.red("āœ— Failed: " + failed.toString())); + return ; + } else { + return ; + } + } + catch (raw_err$1){ + var err$1 = Caml_js_exceptions.internalToOCamlException(raw_err$1); + if (err$1.RE_EXN_ID === Js_exn.$$Error) { + console.error(chalk.red(Core__Option.getOr(err$1._1.message, "unknown error"))); + return process.exit(1); + } + throw err$1; + } +} + +var program = new Commander.Command().name("accessibility-scan").description("Command-line tool for accessibility scanning").version("1.0.0"); + +program.command("scan").description("Scan a URL for accessibility issues").argument("", "URL to scan").option("-l, --level ", "WCAG level (A, AA, AAA)", "AA").option("-o, --output ", "Output file for results (JSON)").option("-f, --format ", "Output format (json, table, markdown)", "table").option("--screenshot", "Take screenshot").action(scanAction); + +program.command("ci").description("Run accessibility scan for CI/CD").argument("", "URL to scan").option("-l, --level ", "WCAG level (A, AA, AAA)", "AA").option("--min-score ", "Minimum required score", "70").option("--fail-on-violations", "Fail if any violations found").action(ciAction); + +program.command("batch").description("Scan multiple URLs from a file").argument("", "File containing URLs (one per line)").option("-l, --level ", "WCAG level (A, AA, AAA)", "AA").option("-o, --output ", "Output directory for results", "./scan-results").action(batchAction); + +program.parse(); + +export { + Scanner$1 as Scanner, + Commander$1 as Commander, + Chalk$1 as Chalk, + Ora$1 as Ora, + Table , + Fs , + Path$1 as Path, + Process , + chalk , + getGrade , + bar , + parseWcagLevel , + wcagLabel , + impactColored , + scoreColored , + generateMarkdown , + scanAction , + ciAction , + batchAction , + program , +} +/* chalk Not a pure module */ diff --git a/tools/cli/src/Cli.res b/tools/cli/src/Cli.res new file mode 100644 index 0000000..acc41f0 --- /dev/null +++ b/tools/cli/src/Cli.res @@ -0,0 +1,401 @@ +type wcagLevel = [#A | #"AA" | #"AAA"] +type impact = [#critical | #serious | #moderate | #minor] + +type nodeDetail = {target: array} +type violationDetail = { + impact: impact, + description: string, + help: string, + helpUrl: string, + nodes: array, + tags: array, +} +type passDetail = {description: string} +type incompleteDetail = {description: string} + +type scanOptions = { + url: string, + wcagLevel: wcagLevel, + screenshot?: bool, +} + +type scanResult = { + score: int, + violations: array, + passes: array, + incomplete: array, +} + +module Scanner = { + type t + @module("@accessibility-everywhere/scanner") external createScanner: unit => t = "createScanner" + @send external scan: (t, scanOptions) => promise = "scan" +} + +module Commander = { + type t + type cmd + + @module("commander") @new external make: unit => t = "Command" + @send external setName: (t, string) => t = "name" + @send external setDescription: (t, string) => t = "description" + @send external setVersion: (t, string) => t = "version" + @send external command: (t, string) => cmd = "command" + @send external descriptionCmd: (cmd, string) => cmd = "description" + @send external argument: (cmd, string, string) => cmd = "argument" + @send external option: (cmd, string, string) => cmd = "option" + @send external optionWithDefault: (cmd, string, string, string) => cmd = "option" + @send external action: (cmd, (string, {..}) => promise) => cmd = "action" + @send external parse: t => unit = "parse" +} + +module Chalk = { + type c + @module("chalk") external default: c = "default" + @send external red: (c, string) => string = "red" + @send external green: (c, string) => string = "green" + @send external yellow: (c, string) => string = "yellow" + @send external blue: (c, string) => string = "blue" + @send external gray: (c, string) => string = "gray" + @send external bold: (c, string) => string = "bold" + @get external redChain: c => c = "red" + @get external greenChain: c => c = "green" + @get external yellowChain: c => c = "yellow" + @get external blueChain: c => c = "blue" + @get external boldChain: c => c = "bold" +} + +module Ora = { + type t + @module("ora") external make: string => t = "default" + @send external start: t => t = "start" + @send external succeed: (t, string) => t = "succeed" + @send external fail: (t, string) => t = "fail" +} + +module Table = { + type t + type init = {head: array, colWidths: array} + @module("cli-table3") @new external make: init => t = "default" + @send external push: (t, array) => unit = "push" + @send external toString: t => string = "toString" +} + +module Fs = { + type writeJsonOpts = {spaces: int} + @module("fs-extra") external writeJson: (string, 'a, writeJsonOpts) => promise = "writeJson" + @module("fs-extra") + external readFile: (string, @as("utf-8") _) => promise = "readFile" + @module("fs-extra") external ensureDir: string => promise = "ensureDir" +} + +module Path = { + @module("path") external join: (string, string) => string = "join" +} + +module Process = { + @val external exit: int => 'a = "process.exit" +} + +@val external consoleLog: string => unit = "console.log" +@val external consoleError: string => unit = "console.error" +@val external jsonStringifyPretty: ('a, Js.Null.t, int) => string = "JSON.stringify" + +let chalk = Chalk.default + +let getGrade = score => + if score >= 90 { + "A" + } else if score >= 80 { + "B" + } else if score >= 70 { + "C" + } else if score >= 60 { + "D" + } else { + "F" + } + +let bar = String.repeat("=", 70) + +let parseWcagLevel = (s): wcagLevel => + switch s { + | "A" => #A + | "AAA" => #"AAA" + | _ => #"AA" + } + +let wcagLabel = (w: wcagLevel) => + switch w { + | #A => "A" + | #"AA" => "AA" + | #"AAA" => "AAA" + } + +let impactColored = (impact: impact) => { + let label = switch impact { + | #critical => "critical" + | #serious => "serious" + | #moderate => "moderate" + | #minor => "minor" + } + switch impact { + | #critical => chalk->Chalk.boldChain->Chalk.red(label) + | #serious => chalk->Chalk.red(label) + | #moderate => chalk->Chalk.yellow(label) + | #minor => chalk->Chalk.blue(label) + } +} + +let scoreColored = score => + if score >= 90 { + chalk->Chalk.boldChain->Chalk.green(Int.toString(score)) + } else if score >= 70 { + chalk->Chalk.boldChain->Chalk.yellow(Int.toString(score)) + } else { + chalk->Chalk.boldChain->Chalk.red(Int.toString(score)) + } + +let generateMarkdown = (result: scanResult, url: string) => { + let header = `# Accessibility Report\n\n**URL:** ${url}\n**Score:** ${Int.toString( + result.score, + )}/100\n\n## Violations\n\n` + let body = + result.violations + ->Array.mapWithIndex((v, i) => + `### ${Int.toString(i + 1)}. ${v.help}\n\n` ++ + `- **Impact:** ${(v.impact :> string)}\n` ++ + `- **Instances:** ${Int.toString(Array.length(v.nodes))}\n` ++ + `- **Help:** ${v.helpUrl}\n\n` + ) + ->Array.join("") + header ++ body +} + +@get external optLevel: {..} => string = "level" +@get external optScreenshot: {..} => option = "screenshot" +@get external optOutput: {..} => option = "output" +@get external optFormat: {..} => string = "format" +@get external optMinScore: {..} => string = "minScore" +@get external optFailOnViolations: {..} => option = "failOnViolations" + +let scanAction = async (url: string, options: {..}) => { + let spinner = Ora.make("Scanning for accessibility issues...")->Ora.start + try { + let scanner = Scanner.createScanner() + let result = await scanner->Scanner.scan({ + url, + wcagLevel: options->optLevel->parseWcagLevel, + screenshot: ?options->optScreenshot, + }) + let _ = spinner->Ora.succeed("Scan complete!") + + consoleLog("\n" ++ bar) + consoleLog(chalk->Chalk.boldChain->Chalk.blue("Accessibility Report")) + consoleLog(bar) + consoleLog(`URL: ${url}`) + consoleLog(`WCAG Level: ${options->optLevel}`) + consoleLog( + `Score: ${scoreColored(result.score)}/100 (Grade ${getGrade(result.score)})`, + ) + consoleLog(bar ++ "\n") + + let summaryTable = Table.make({ + head: ["Metric", "Count"], + colWidths: [30, 10], + }) + summaryTable->Table.push(["āœ… Passes", Int.toString(Array.length(result.passes))]) + summaryTable->Table.push([ + "āŒ Violations", + Int.toString(Array.length(result.violations)), + ]) + summaryTable->Table.push([ + "āš ļø Needs Review", + Int.toString(Array.length(result.incomplete)), + ]) + consoleLog(summaryTable->Table.toString ++ "\n") + + if Array.length(result.violations) > 0 { + consoleLog( + chalk + ->Chalk.boldChain + ->Chalk.red(`Found ${Int.toString(Array.length(result.violations))} violations:\n`), + ) + let format = options->optFormat + if format === "table" { + let violationsTable = Table.make({ + head: ["Impact", "Description", "Instances", "WCAG"], + colWidths: [12, 50, 10, 15], + }) + result.violations->Array.forEach(v => + violationsTable->Table.push([ + impactColored(v.impact), + v.description, + Int.toString(Array.length(v.nodes)), + v.tags->Array.join(", "), + ]) + ) + consoleLog(violationsTable->Table.toString) + } else if format === "markdown" { + consoleLog(generateMarkdown(result, url)) + } else { + consoleLog(jsonStringifyPretty(result, Js.Null.empty, 2)) + } + } else { + consoleLog(chalk->Chalk.boldChain->Chalk.green("šŸŽ‰ No violations found! Great job!")) + } + + switch options->optOutput { + | Some(out) => { + await Fs.writeJson(out, result, {spaces: 2}) + consoleLog(chalk->Chalk.gray(`\nāœ“ Results saved to ${out}`)) + } + | None => () + } + + Process.exit(Array.length(result.violations) > 0 ? 1 : 0) + } catch { + | Exn.Error(err) => { + let _ = spinner->Ora.fail("Scan failed") + consoleError(chalk->Chalk.red(Exn.message(err)->Option.getOr("unknown error"))) + Process.exit(1) + } + } +} + +let ciAction = async (url: string, options: {..}) => { + let spinner = Ora.make("Running CI scan...")->Ora.start + try { + let scanner = Scanner.createScanner() + let result = await scanner->Scanner.scan({ + url, + wcagLevel: options->optLevel->parseWcagLevel, + }) + let _ = spinner->Ora.succeed("CI scan complete") + + let minScore = options->optMinScore->Int.fromString->Option.getOr(70) + let failOnViolations = options->optFailOnViolations->Option.getOr(false) + + consoleLog(`Score: ${Int.toString(result.score)}/100`) + consoleLog(`Violations: ${Int.toString(Array.length(result.violations))}`) + + if failOnViolations && Array.length(result.violations) > 0 { + consoleError( + chalk->Chalk.red( + `āœ— Failed: Found ${Int.toString(Array.length(result.violations))} violations`, + ), + ) + Process.exit(1) + } + + if result.score < minScore { + consoleError( + chalk->Chalk.red( + `āœ— Failed: Score ${Int.toString(result.score)} below minimum ${Int.toString(minScore)}`, + ), + ) + Process.exit(1) + } + + consoleLog(chalk->Chalk.green("āœ“ Passed all checks")) + Process.exit(0) + } catch { + | Exn.Error(err) => { + let _ = spinner->Ora.fail("CI scan failed") + consoleError(chalk->Chalk.red(Exn.message(err)->Option.getOr("unknown error"))) + Process.exit(1) + } + } +} + +let batchAction = async (file: string, options: {..}) => { + try { + let raw = await Fs.readFile(file) + let urls = + raw + ->String.split("\n") + ->Array.map(String.trim) + ->Array.filter(s => s !== "" && !String.startsWith(s, "#")) + + consoleLog(chalk->Chalk.blue(`Scanning ${Int.toString(Array.length(urls))} URLs...`)) + + let outDir = options->optOutput->Option.getOr("./scan-results") + await Fs.ensureDir(outDir) + + let scanner = Scanner.createScanner() + let completed = ref(0) + let failed = ref(0) + + let total = Array.length(urls) + for i in 0 to total - 1 { + let url = urls->Array.getUnsafe(i) + let spinner = Ora.make(`[${Int.toString(i + 1)}/${Int.toString(total)}] ${url}`)->Ora.start + try { + let result = await scanner->Scanner.scan({ + url, + wcagLevel: options->optLevel->parseWcagLevel, + }) + let safeName = String.replaceRegExp(url, %re("/[^a-z0-9]/gi"), "_") ++ ".json" + let filepath = Path.join(outDir, safeName) + await Fs.writeJson(filepath, result, {spaces: 2}) + let _ = spinner->Ora.succeed(`${url} - Score: ${Int.toString(result.score)}`) + completed := completed.contents + 1 + } catch { + | Exn.Error(err) => { + let _ = spinner->Ora.fail( + `${url} - ${Exn.message(err)->Option.getOr("unknown error")}`, + ) + failed := failed.contents + 1 + } + } + } + + consoleLog(chalk->Chalk.green(`\nāœ“ Completed: ${Int.toString(completed.contents)}`)) + if failed.contents > 0 { + consoleLog(chalk->Chalk.red(`āœ— Failed: ${Int.toString(failed.contents)}`)) + } + } catch { + | Exn.Error(err) => { + consoleError(chalk->Chalk.red(Exn.message(err)->Option.getOr("unknown error"))) + Process.exit(1) + } + } +} + +let program = + Commander.make() + ->Commander.setName("accessibility-scan") + ->Commander.setDescription("Command-line tool for accessibility scanning") + ->Commander.setVersion("1.0.0") + +let _ = + program + ->Commander.command("scan") + ->Commander.descriptionCmd("Scan a URL for accessibility issues") + ->Commander.argument("", "URL to scan") + ->Commander.optionWithDefault("-l, --level ", "WCAG level (A, AA, AAA)", "AA") + ->Commander.option("-o, --output ", "Output file for results (JSON)") + ->Commander.optionWithDefault("-f, --format ", "Output format (json, table, markdown)", "table") + ->Commander.option("--screenshot", "Take screenshot") + ->Commander.action(scanAction) + +let _ = + program + ->Commander.command("ci") + ->Commander.descriptionCmd("Run accessibility scan for CI/CD") + ->Commander.argument("", "URL to scan") + ->Commander.optionWithDefault("-l, --level ", "WCAG level (A, AA, AAA)", "AA") + ->Commander.optionWithDefault("--min-score ", "Minimum required score", "70") + ->Commander.option("--fail-on-violations", "Fail if any violations found") + ->Commander.action(ciAction) + +let _ = + program + ->Commander.command("batch") + ->Commander.descriptionCmd("Scan multiple URLs from a file") + ->Commander.argument("", "File containing URLs (one per line)") + ->Commander.optionWithDefault("-l, --level ", "WCAG level (A, AA, AAA)", "AA") + ->Commander.optionWithDefault("-o, --output ", "Output directory for results", "./scan-results") + ->Commander.action(batchAction) + +program->Commander.parse diff --git a/tools/cli/src/cli.ts b/tools/cli/src/cli.ts deleted file mode 100644 index 049c7b3..0000000 --- a/tools/cli/src/cli.ts +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env node - -import { Command } from 'commander'; -import chalk from 'chalk'; -import ora from 'ora'; -import Table from 'cli-table3'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { createScanner } from '@accessibility-everywhere/scanner'; - -const program = new Command(); - -program - .name('accessibility-scan') - .description('Command-line tool for accessibility scanning') - .version('1.0.0'); - -// Scan command -program - .command('scan') - .description('Scan a URL for accessibility issues') - .argument('', 'URL to scan') - .option('-l, --level ', 'WCAG level (A, AA, AAA)', 'AA') - .option('-o, --output ', 'Output file for results (JSON)') - .option('-f, --format ', 'Output format (json, table, markdown)', 'table') - .option('--screenshot', 'Take screenshot') - .action(async (url: string, options: any) => { - const spinner = ora('Scanning for accessibility issues...').start(); - - try { - const scanner = createScanner(); - const result = await scanner.scan({ - url, - wcagLevel: options.level, - screenshot: options.screenshot, - }); - - spinner.succeed('Scan complete!'); - - // Display results - console.log('\n' + '='.repeat(70)); - console.log(chalk.bold.blue('Accessibility Report')); - console.log('='.repeat(70)); - console.log(`URL: ${url}`); - console.log(`WCAG Level: ${options.level}`); - console.log(`Score: ${getScoreColor(result.score)}${result.score}/100${chalk.reset()} (Grade ${getGrade(result.score)})`); - console.log('='.repeat(70) + '\n'); - - // Summary table - const summaryTable = new Table({ - head: ['Metric', 'Count'], - colWidths: [30, 10], - }); - - summaryTable.push( - ['āœ… Passes', result.passes.length], - ['āŒ Violations', result.violations.length], - ['āš ļø Needs Review', result.incomplete.length] - ); - - console.log(summaryTable.toString() + '\n'); - - // Violations - if (result.violations.length > 0) { - console.log(chalk.bold.red(`Found ${result.violations.length} violations:\n`)); - - if (options.format === 'table') { - const violationsTable = new Table({ - head: ['Impact', 'Description', 'Instances', 'WCAG'], - colWidths: [12, 50, 10, 15], - }); - - result.violations.forEach((v: any) => { - violationsTable.push([ - getImpactColor(v.impact) + v.impact + chalk.reset(), - v.description, - v.nodes.length, - v.wcag.join(', '), - ]); - }); - - console.log(violationsTable.toString()); - } else if (options.format === 'markdown') { - console.log(generateMarkdown(result, url)); - } else { - console.log(JSON.stringify(result, null, 2)); - } - } else { - console.log(chalk.green.bold('šŸŽ‰ No violations found! Great job!')); - } - - // Save to file if requested - if (options.output) { - await fs.writeJson(options.output, result, { spaces: 2 }); - console.log(chalk.gray(`\nāœ“ Results saved to ${options.output}`)); - } - - // Exit with error code if violations found - process.exit(result.violations.length > 0 ? 1 : 0); - } catch (error: any) { - spinner.fail('Scan failed'); - console.error(chalk.red(error.message)); - process.exit(1); - } - }); - -// CI command (for continuous integration) -program - .command('ci') - .description('Run accessibility scan for CI/CD') - .argument('', 'URL to scan') - .option('-l, --level ', 'WCAG level (A, AA, AAA)', 'AA') - .option('--min-score ', 'Minimum required score', '70') - .option('--fail-on-violations', 'Fail if any violations found') - .action(async (url: string, options: any) => { - const spinner = ora('Running CI scan...').start(); - - try { - const scanner = createScanner(); - const result = await scanner.scan({ - url, - wcagLevel: options.level, - }); - - spinner.succeed('CI scan complete'); - - const minScore = parseInt(options.minScore); - const failOnViolations = options.failOnViolations; - - console.log(`Score: ${result.score}/100`); - console.log(`Violations: ${result.violations.length}`); - - if (failOnViolations && result.violations.length > 0) { - console.error(chalk.red(`āœ— Failed: Found ${result.violations.length} violations`)); - process.exit(1); - } - - if (result.score < minScore) { - console.error(chalk.red(`āœ— Failed: Score ${result.score} below minimum ${minScore}`)); - process.exit(1); - } - - console.log(chalk.green('āœ“ Passed all checks')); - process.exit(0); - } catch (error: any) { - spinner.fail('CI scan failed'); - console.error(chalk.red(error.message)); - process.exit(1); - } - }); - -// Multi-scan command -program - .command('batch') - .description('Scan multiple URLs from a file') - .argument('', 'File containing URLs (one per line)') - .option('-l, --level ', 'WCAG level (A, AA, AAA)', 'AA') - .option('-o, --output ', 'Output directory for results', './scan-results') - .action(async (file: string, options: any) => { - try { - const urls = (await fs.readFile(file, 'utf-8')) - .split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - console.log(chalk.blue(`Scanning ${urls.length} URLs...`)); - - await fs.ensureDir(options.output); - - const scanner = createScanner(); - let completed = 0; - let failed = 0; - - for (const url of urls) { - const spinner = ora(`[${completed + 1}/${urls.length}] ${url}`).start(); - - try { - const result = await scanner.scan({ - url, - wcagLevel: options.level, - }); - - const filename = url.replace(/[^a-z0-9]/gi, '_') + '.json'; - const filepath = path.join(options.output, filename); - await fs.writeJson(filepath, result, { spaces: 2 }); - - spinner.succeed(`${url} - Score: ${result.score}`); - completed++; - } catch (error: any) { - spinner.fail(`${url} - ${error.message}`); - failed++; - } - } - - console.log(chalk.green(`\nāœ“ Completed: ${completed}`)); - if (failed > 0) { - console.log(chalk.red(`āœ— Failed: ${failed}`)); - } - } catch (error: any) { - console.error(chalk.red(error.message)); - process.exit(1); - } - }); - -// Helper functions -function getGrade(score: number): string { - if (score >= 90) return 'A'; - if (score >= 80) return 'B'; - if (score >= 70) return 'C'; - if (score >= 60) return 'D'; - return 'F'; -} - -function getScoreColor(score: number): string { - if (score >= 90) return chalk.green.bold(''); - if (score >= 70) return chalk.yellow.bold(''); - return chalk.red.bold(''); -} - -function getImpactColor(impact: string): string { - const colors: Record = { - critical: chalk.red.bold(''), - serious: chalk.red(''), - moderate: chalk.yellow(''), - minor: chalk.blue(''), - }; - return colors[impact] || ''; -} - -function generateMarkdown(result: any, url: string): string { - let md = `# Accessibility Report\n\n`; - md += `**URL:** ${url}\n`; - md += `**Score:** ${result.score}/100\n\n`; - md += `## Violations\n\n`; - - result.violations.forEach((v: any, i: number) => { - md += `### ${i + 1}. ${v.help}\n\n`; - md += `- **Impact:** ${v.impact}\n`; - md += `- **Instances:** ${v.nodes.length}\n`; - md += `- **Help:** ${v.helpUrl}\n\n`; - }); - - return md; -} - -program.parse(); diff --git a/tools/github-action/package-lock.json b/tools/github-action/package-lock.json new file mode 100644 index 0000000..d4bbdeb --- /dev/null +++ b/tools/github-action/package-lock.json @@ -0,0 +1,288 @@ +{ + "name": "@accessibility-everywhere/github-action", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@accessibility-everywhere/github-action", + "version": "1.0.0", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0" + }, + "devDependencies": { + "@rescript/core": "^1.5.0", + "rescript": "^11.1.0" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@rescript/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rescript/core/-/core-1.6.1.tgz", + "integrity": "sha512-vyb5k90ck+65Fgui+5vCja/mUfzKaK3kOPT4Z6aAJdHLH1eljEi1zKhXroCiCtpNLSWp8k4ulh1bdB5WS0hvqA==", + "dev": true, + "peerDependencies": { + "rescript": ">=11.1.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/rescript": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/rescript/-/rescript-11.1.4.tgz", + "integrity": "sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "bsc": "bsc", + "bstracing": "lib/bstracing", + "rescript": "rescript" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/tools/github-action/package.json b/tools/github-action/package.json index c1c0b44..021326a 100644 --- a/tools/github-action/package.json +++ b/tools/github-action/package.json @@ -2,19 +2,19 @@ "name": "@accessibility-everywhere/github-action", "version": "1.0.0", "description": "GitHub Action for automated accessibility testing", - "main": "dist/index.js", + "main": "src/Action.mjs", + "type": "module", "scripts": { - "build": "tsc && ncc build dist/index.js -o dist", - "test": "jest" + "build": "rescript build", + "dev": "rescript build -w", + "clean": "rescript clean" }, "dependencies": { "@actions/core": "^1.10.1", - "@actions/github": "^6.0.0", - "@accessibility-everywhere/scanner": "^1.0.0" + "@actions/github": "^6.0.0" }, "devDependencies": { - "@types/node": "^20.10.0", - "@vercel/ncc": "^0.38.1", - "typescript": "^5.3.2" + "rescript": "^11.1.0", + "@rescript/core": "^1.5.0" } } diff --git a/tools/github-action/rescript.json b/tools/github-action/rescript.json new file mode 100644 index 0000000..b1fbd00 --- /dev/null +++ b/tools/github-action/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "@accessibility-everywhere/github-action", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": { + "module": "esmodule", + "in-source": true + }, + "suffix": ".mjs", + "bs-dependencies": ["@rescript/core"], + "bsc-flags": ["-open RescriptCore"] +} diff --git a/tools/github-action/src/Action.mjs b/tools/github-action/src/Action.mjs new file mode 100644 index 0000000..170c864 --- /dev/null +++ b/tools/github-action/src/Action.mjs @@ -0,0 +1,220 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Js_exn from "rescript/lib/es6/js_exn.js"; +import * as Core__Int from "@rescript/core/src/Core__Int.mjs"; +import * as Core__Option from "@rescript/core/src/Core__Option.mjs"; +import * as Core from "@actions/core"; +import * as Github from "@actions/github"; +import * as Caml_js_exceptions from "rescript/lib/es6/caml_js_exceptions.js"; +import * as Scanner from "@accessibility-everywhere/scanner"; + +var Scanner$1 = {}; + +var Core$1 = {}; + +var Github$1 = {}; + +function getGrade(score) { + if (score >= 90) { + return "A"; + } else if (score >= 80) { + return "B"; + } else if (score >= 70) { + return "C"; + } else if (score >= 60) { + return "D"; + } else { + return "F"; + } +} + +function gradeEmoji(grade) { + switch (grade) { + case "A" : + return "🟢"; + case "B" : + return "🟔"; + case "C" : + return "🟠"; + case "D" : + case "F" : + return "šŸ”“"; + default: + return "⚪"; + } +} + +function impactEmoji(impact) { + if (impact === "serious") { + return "🟠"; + } else if (impact === "minor") { + return "šŸ”µ"; + } else if (impact === "critical") { + return "šŸ”“"; + } else { + return "🟔"; + } +} + +function impactLabel(impact) { + if (impact === "serious") { + return "serious"; + } else if (impact === "minor") { + return "minor"; + } else if (impact === "critical") { + return "critical"; + } else { + return "moderate"; + } +} + +function wcagLabel(wcag) { + if (wcag === "AA") { + return "AA"; + } else if (wcag === "AAA") { + return "AAA"; + } else { + return "A"; + } +} + +function parseWcagLevel(s) { + switch (s) { + case "A" : + return "A"; + case "AAA" : + return "AAA"; + default: + return "AA"; + } +} + +function generateSummary(result, url, wcagLevel) { + var grade = getGrade(result.score); + var g = gradeEmoji(grade); + var parts = [ + "# Accessibility Report " + g + "\n\n", + "**URL:** " + url + "\n", + "**WCAG Level:** " + wcagLabel(wcagLevel) + "\n", + "**Score:** " + result.score.toString() + "/100 (Grade " + grade + ")\n\n", + "## Summary\n\n", + "| Metric | Count |\n", + "|--------|-------|\n", + "| āœ… Passes | " + result.passes.length.toString() + " |\n", + "| āŒ Violations | " + result.violations.length.toString() + " |\n", + "| āš ļø Needs Review | " + result.incomplete.length.toString() + " |\n\n" + ]; + var head = parts.join(""); + var violations; + if (result.violations.length > 0) { + var shown = result.violations.slice(0, 10); + var body = shown.map(function (v, i) { + var e = impactEmoji(v.impact); + return "### " + (i + 1 | 0).toString() + ". " + e + " " + v.help + "\n\n" + ("**Impact:** " + impactLabel(v.impact) + "\n\n") + ("**Description:** " + v.description + "\n\n") + ("**Instances:** " + v.nodes.length.toString() + "\n\n") + ("**Learn more:** " + v.helpUrl + "\n\n"); + }).join(""); + var more = result.violations.length > 10 ? "\n*... and " + (result.violations.length - 10 | 0).toString() + " more violations*\n\n" : ""; + violations = "## Violations\n\n" + body + more; + } else { + violations = ""; + } + var footer = "\n---\n\n" + ("[View full report](https://accessibility-everywhere.org/report?url=" + encodeURIComponent(url) + ")\n"); + return head + violations + footer; +} + +async function postPRComment(token, result, url, wcagLevel) { + var octokit = Github.getOctokit(token); + var context = Github.context; + var pr = context.payload.pull_request; + if (pr === undefined) { + return ; + } + var summary = generateSummary(result, url, wcagLevel); + return await octokit.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: summary + }); +} + +async function run() { + try { + var url = Core.getInput("url", { + required: true + }); + var wcagLevel = parseWcagLevel(Core.getInput("wcag-level")); + var failOnViolations = Core.getInput("fail-on-violations") === "true"; + var minScore = Core__Option.getOr(Core__Int.fromString(Core.getInput("min-score"), undefined), 0); + var commentPR = Core.getInput("comment-pr") === "true"; + var githubToken = Core.getInput("github-token"); + Core.info("Scanning " + url + " for WCAG " + wcagLabel(wcagLevel) + " compliance..."); + var scanner = Scanner.createScanner(); + var result = await scanner.scan({ + url: url, + wcagLevel: wcagLevel, + screenshot: false + }); + Core.setOutput("score", result.score); + Core.setOutput("violations", result.violations.length); + Core.setOutput("passes", result.passes.length); + Core.setOutput("report-url", "https://accessibility-everywhere.org/report?url=" + encodeURIComponent(url)); + var summary = generateSummary(result, url, wcagLevel); + await Core.summary.addRaw(summary).write(); + if (commentPR && githubToken !== "" && Github.context.payload.pull_request !== undefined) { + await postPRComment(githubToken, result, url, wcagLevel); + } + var bar = "============================================================"; + Core.info("\n" + bar); + Core.info("Accessibility Score: " + result.score.toString() + "/100"); + Core.info("Violations: " + result.violations.length.toString()); + Core.info("Passes: " + result.passes.length.toString()); + Core.info("Incomplete: " + result.incomplete.length.toString()); + Core.info(bar + "\n"); + if (result.violations.length > 0) { + Core.warning("Found " + result.violations.length.toString() + " accessibility violations:"); + result.violations.forEach(function (v, i) { + Core.warning((i + 1 | 0).toString() + ". [" + impactLabel(v.impact).toUpperCase() + "] " + v.description); + Core.warning(" Help: " + v.helpUrl); + Core.warning(" Instances: " + v.nodes.length.toString()); + }); + } + if (failOnViolations && result.violations.length > 0) { + Core.setFailed("Found " + result.violations.length.toString() + " accessibility violations"); + } + if (minScore > 0 && result.score < minScore) { + Core.setFailed("Accessibility score " + result.score.toString() + " is below minimum required score " + minScore.toString()); + } + if (result.violations.length === 0 && result.score >= minScore) { + Core.info("āœ“ Accessibility check passed!"); + return ; + } else { + return ; + } + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + Core.setFailed("Action failed: " + Core__Option.getOr(err._1.message, "unknown error")); + return ; + } + throw err; + } +} + +run(); + +export { + Scanner$1 as Scanner, + Core$1 as Core, + Github$1 as Github, + getGrade , + gradeEmoji , + impactEmoji , + impactLabel , + wcagLabel , + parseWcagLevel , + generateSummary , + postPRComment , + run , +} +/* Not a pure module */ diff --git a/tools/github-action/src/Action.res b/tools/github-action/src/Action.res new file mode 100644 index 0000000..f2571ec --- /dev/null +++ b/tools/github-action/src/Action.res @@ -0,0 +1,263 @@ +type wcagLevel = [#A | #"AA" | #"AAA"] +type impact = [#critical | #serious | #moderate | #minor] + +type nodeDetail = {target: array} +type violationDetail = { + impact: impact, + description: string, + help: string, + helpUrl: string, + nodes: array, +} +type passDetail = {description: string} +type incompleteDetail = {description: string} + +type scanOptions = { + url: string, + wcagLevel: wcagLevel, + screenshot: bool, +} + +type scanResult = { + score: int, + violations: array, + passes: array, + incomplete: array, +} + +module Scanner = { + type t + @module("@accessibility-everywhere/scanner") external createScanner: unit => t = "createScanner" + @send external scan: (t, scanOptions) => promise = "scan" +} + +module Core = { + type getInputOptions = {required?: bool} + type summary + @module("@actions/core") external getInputBare: string => string = "getInput" + @module("@actions/core") + external getInputWith: (string, getInputOptions) => string = "getInput" + + @module("@actions/core") external info: string => unit = "info" + @module("@actions/core") external warning: string => unit = "warning" + @module("@actions/core") external setFailed: string => unit = "setFailed" + @module("@actions/core") external setOutputStr: (string, string) => unit = "setOutput" + @module("@actions/core") external setOutputInt: (string, int) => unit = "setOutput" + + @module("@actions/core") @scope("summary") external addRaw: string => summary = "addRaw" + @send external write: summary => promise = "write" +} + +module Github = { + type repo = {owner: string, repo: string} + type pullRequest = {number: int} + type payload = {pull_request?: pullRequest} + type context = {payload: payload, repo: repo} + + type createCommentArgs = { + owner: string, + repo: string, + issue_number: int, + body: string, + } + type issuesApi = {createComment: createCommentArgs => promise} + type rest = {issues: issuesApi} + type octokit = {rest: rest} + + @module("@actions/github") external context: context = "context" + @module("@actions/github") external getOctokit: string => octokit = "getOctokit" +} + +let getGrade = score => + if score >= 90 { + "A" + } else if score >= 80 { + "B" + } else if score >= 70 { + "C" + } else if score >= 60 { + "D" + } else { + "F" + } + +let gradeEmoji = grade => + switch grade { + | "A" => "🟢" + | "B" => "🟔" + | "C" => "🟠" + | "D" | "F" => "šŸ”“" + | _ => "⚪" + } + +let impactEmoji = impact => + switch impact { + | #critical => "šŸ”“" + | #serious => "🟠" + | #moderate => "🟔" + | #minor => "šŸ”µ" + } + +let impactLabel = impact => + switch impact { + | #critical => "critical" + | #serious => "serious" + | #moderate => "moderate" + | #minor => "minor" + } + +let wcagLabel = wcag => + switch wcag { + | #A => "A" + | #"AA" => "AA" + | #"AAA" => "AAA" + } + +let parseWcagLevel = (s): wcagLevel => + switch s { + | "A" => #A + | "AAA" => #"AAA" + | _ => #"AA" + } + +let generateSummary = (result: scanResult, url: string, wcagLevel: wcagLevel) => { + let grade = getGrade(result.score) + let g = gradeEmoji(grade) + let parts = [ + `# Accessibility Report ${g}\n\n`, + `**URL:** ${url}\n`, + `**WCAG Level:** ${wcagLabel(wcagLevel)}\n`, + `**Score:** ${Int.toString(result.score)}/100 (Grade ${grade})\n\n`, + `## Summary\n\n`, + `| Metric | Count |\n`, + `|--------|-------|\n`, + `| āœ… Passes | ${Int.toString(Array.length(result.passes))} |\n`, + `| āŒ Violations | ${Int.toString(Array.length(result.violations))} |\n`, + `| āš ļø Needs Review | ${Int.toString(Array.length(result.incomplete))} |\n\n`, + ] + let head = parts->Array.join("") + + let violations = if Array.length(result.violations) > 0 { + let header = "## Violations\n\n" + let shown = result.violations->Array.slice(~start=0, ~end=10) + let body = + shown + ->Array.mapWithIndex((v, i) => { + let e = impactEmoji(v.impact) + `### ${Int.toString(i + 1)}. ${e} ${v.help}\n\n` ++ + `**Impact:** ${impactLabel(v.impact)}\n\n` ++ + `**Description:** ${v.description}\n\n` ++ + `**Instances:** ${Int.toString(Array.length(v.nodes))}\n\n` ++ + `**Learn more:** ${v.helpUrl}\n\n` + }) + ->Array.join("") + let more = if Array.length(result.violations) > 10 { + `\n*... and ${Int.toString(Array.length(result.violations) - 10)} more violations*\n\n` + } else { + "" + } + header ++ body ++ more + } else { + "" + } + + let footer = + `\n---\n\n` ++ + `[View full report](https://accessibility-everywhere.org/report?url=${encodeURIComponent(url)})\n` + + head ++ violations ++ footer +} + +let postPRComment = async (token: string, result: scanResult, url: string, wcagLevel: wcagLevel) => { + let octokit = Github.getOctokit(token) + let context = Github.context + switch context.payload.pull_request { + | None => () + | Some(pr) => { + let summary = generateSummary(result, url, wcagLevel) + await octokit.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: summary, + }) + } + } +} + +let run = async () => { + try { + let url = Core.getInputWith("url", {required: true}) + let wcagLevel = Core.getInputBare("wcag-level")->parseWcagLevel + let failOnViolations = Core.getInputBare("fail-on-violations") === "true" + let minScore = Core.getInputBare("min-score")->Int.fromString->Option.getOr(0) + let commentPR = Core.getInputBare("comment-pr") === "true" + let githubToken = Core.getInputBare("github-token") + + Core.info(`Scanning ${url} for WCAG ${wcagLabel(wcagLevel)} compliance...`) + + let scanner = Scanner.createScanner() + let result = await scanner->Scanner.scan({url, wcagLevel, screenshot: false}) + + Core.setOutputInt("score", result.score) + Core.setOutputInt("violations", Array.length(result.violations)) + Core.setOutputInt("passes", Array.length(result.passes)) + Core.setOutputStr( + "report-url", + `https://accessibility-everywhere.org/report?url=${encodeURIComponent(url)}`, + ) + + let summary = generateSummary(result, url, wcagLevel) + let _ = await Core.addRaw(summary)->Core.write + + if ( + commentPR && + githubToken !== "" && + Github.context.payload.pull_request !== None + ) { + await postPRComment(githubToken, result, url, wcagLevel) + } + + let bar = "============================================================" + Core.info(`\n${bar}`) + Core.info(`Accessibility Score: ${Int.toString(result.score)}/100`) + Core.info(`Violations: ${Int.toString(Array.length(result.violations))}`) + Core.info(`Passes: ${Int.toString(Array.length(result.passes))}`) + Core.info(`Incomplete: ${Int.toString(Array.length(result.incomplete))}`) + Core.info(`${bar}\n`) + + if Array.length(result.violations) > 0 { + Core.warning( + `Found ${Int.toString(Array.length(result.violations))} accessibility violations:`, + ) + result.violations->Array.forEachWithIndex((v, i) => { + Core.warning( + `${Int.toString(i + 1)}. [${impactLabel(v.impact)->String.toUpperCase}] ${v.description}`, + ) + Core.warning(` Help: ${v.helpUrl}`) + Core.warning(` Instances: ${Int.toString(Array.length(v.nodes))}`) + }) + } + + if failOnViolations && Array.length(result.violations) > 0 { + Core.setFailed( + `Found ${Int.toString(Array.length(result.violations))} accessibility violations`, + ) + } + + if minScore > 0 && result.score < minScore { + Core.setFailed( + `Accessibility score ${Int.toString(result.score)} is below minimum required score ${Int.toString(minScore)}`, + ) + } + + if Array.length(result.violations) === 0 && result.score >= minScore { + Core.info("āœ“ Accessibility check passed!") + } + } catch { + | Exn.Error(err) => + Core.setFailed(`Action failed: ${Exn.message(err)->Option.getOr("unknown error")}`) + } +} + +let _ = run() diff --git a/tools/github-action/src/index.ts b/tools/github-action/src/index.ts deleted file mode 100644 index c7a83e4..0000000 --- a/tools/github-action/src/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -import * as core from '@actions/core'; -import * as github from '@actions/github'; -import { createScanner } from '@accessibility-everywhere/scanner'; - -async function run() { - try { - // Get inputs - const url = core.getInput('url', { required: true }); - const wcagLevel = core.getInput('wcag-level') as 'A' | 'AA' | 'AAA'; - const failOnViolations = core.getInput('fail-on-violations') === 'true'; - const minScore = parseInt(core.getInput('min-score') || '0'); - const commentPR = core.getInput('comment-pr') === 'true'; - const githubToken = core.getInput('github-token'); - - core.info(`Scanning ${url} for WCAG ${wcagLevel} compliance...`); - - // Run scan - const scanner = createScanner(); - const result = await scanner.scan({ - url, - wcagLevel, - screenshot: false, - }); - - // Set outputs - core.setOutput('score', result.score); - core.setOutput('violations', result.violations.length); - core.setOutput('passes', result.passes.length); - core.setOutput('report-url', `https://accessibility-everywhere.org/report?url=${encodeURIComponent(url)}`); - - // Generate summary - const summary = generateSummary(result, url, wcagLevel); - core.summary.addRaw(summary).write(); - - // Post PR comment if requested - if (commentPR && githubToken && github.context.payload.pull_request) { - await postPRComment(githubToken, result, url, wcagLevel); - } - - // Log results - core.info(`\n${'='.repeat(60)}`); - core.info(`Accessibility Score: ${result.score}/100`); - core.info(`Violations: ${result.violations.length}`); - core.info(`Passes: ${result.passes.length}`); - core.info(`Incomplete: ${result.incomplete.length}`); - core.info(`${'='.repeat(60)}\n`); - - // Log violations - if (result.violations.length > 0) { - core.warning(`Found ${result.violations.length} accessibility violations:`); - result.violations.forEach((v, i) => { - core.warning(`${i + 1}. [${v.impact.toUpperCase()}] ${v.description}`); - core.warning(` Help: ${v.helpUrl}`); - core.warning(` Instances: ${v.nodes.length}`); - }); - } - - // Check failure conditions - if (failOnViolations && result.violations.length > 0) { - core.setFailed(`Found ${result.violations.length} accessibility violations`); - } - - if (minScore > 0 && result.score < minScore) { - core.setFailed(`Accessibility score ${result.score} is below minimum required score ${minScore}`); - } - - if (result.violations.length === 0 && result.score >= minScore) { - core.info('āœ“ Accessibility check passed!'); - } - } catch (error: any) { - core.setFailed(`Action failed: ${error.message}`); - } -} - -function generateSummary(result: any, url: string, wcagLevel: string): string { - const grade = getGrade(result.score); - const gradeEmoji = { - A: '🟢', - B: '🟔', - C: '🟠', - D: 'šŸ”“', - F: 'šŸ”“', - }[grade]; - - let markdown = `# Accessibility Report ${gradeEmoji}\n\n`; - markdown += `**URL:** ${url}\n`; - markdown += `**WCAG Level:** ${wcagLevel}\n`; - markdown += `**Score:** ${result.score}/100 (Grade ${grade})\n\n`; - - markdown += `## Summary\n\n`; - markdown += `| Metric | Count |\n`; - markdown += `|--------|-------|\n`; - markdown += `| āœ… Passes | ${result.passes.length} |\n`; - markdown += `| āŒ Violations | ${result.violations.length} |\n`; - markdown += `| āš ļø Needs Review | ${result.incomplete.length} |\n\n`; - - if (result.violations.length > 0) { - markdown += `## Violations\n\n`; - result.violations.slice(0, 10).forEach((v: any, i: number) => { - const impact = v.impact as 'critical' | 'serious' | 'moderate' | 'minor'; - const impactEmoji = { - critical: 'šŸ”“', - serious: '🟠', - moderate: '🟔', - minor: 'šŸ”µ', - }[impact] || '⚪'; - - markdown += `### ${i + 1}. ${impactEmoji} ${v.help}\n\n`; - markdown += `**Impact:** ${v.impact}\n\n`; - markdown += `**Description:** ${v.description}\n\n`; - markdown += `**Instances:** ${v.nodes.length}\n\n`; - markdown += `**Learn more:** ${v.helpUrl}\n\n`; - }); - - if (result.violations.length > 10) { - markdown += `\n*... and ${result.violations.length - 10} more violations*\n\n`; - } - } - - markdown += `\n---\n\n`; - markdown += `[View full report](https://accessibility-everywhere.org/report?url=${encodeURIComponent(url)})\n`; - - return markdown; -} - -async function postPRComment(token: string, result: any, url: string, wcagLevel: string) { - const octokit = github.getOctokit(token); - const { context } = github; - - if (!context.payload.pull_request) { - return; - } - - const summary = generateSummary(result, url, wcagLevel); - - await octokit.rest.issues.createComment({ - ...context.repo, - issue_number: context.payload.pull_request.number, - body: summary, - }); -} - -function getGrade(score: number): string { - if (score >= 90) return 'A'; - if (score >= 80) return 'B'; - if (score >= 70) return 'C'; - if (score >= 60) return 'D'; - return 'F'; -} - -run(); diff --git a/tools/monitoring-api/package-lock.json b/tools/monitoring-api/package-lock.json new file mode 100644 index 0000000..49ce04c --- /dev/null +++ b/tools/monitoring-api/package-lock.json @@ -0,0 +1,972 @@ +{ + "name": "@accessibility-everywhere/monitoring-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@accessibility-everywhere/monitoring-api", + "version": "1.0.0", + "dependencies": { + "compression": "^1.7.4", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "joi": "^17.11.0", + "uuid": "^14.0.0" + }, + "devDependencies": { + "@rescript/core": "^1.5.0", + "rescript": "^11.1.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@rescript/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rescript/core/-/core-1.6.1.tgz", + "integrity": "sha512-vyb5k90ck+65Fgui+5vCja/mUfzKaK3kOPT4Z6aAJdHLH1eljEi1zKhXroCiCtpNLSWp8k4ulh1bdB5WS0hvqA==", + "dev": true, + "peerDependencies": { + "rescript": ">=11.1.0" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rescript": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/rescript/-/rescript-11.1.4.tgz", + "integrity": "sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "bsc": "bsc", + "bstracing": "lib/bstracing", + "rescript": "rescript" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/tools/monitoring-api/package.json b/tools/monitoring-api/package.json index 32db49d..0cd4d25 100644 --- a/tools/monitoring-api/package.json +++ b/tools/monitoring-api/package.json @@ -2,16 +2,15 @@ "name": "@accessibility-everywhere/monitoring-api", "version": "1.0.0", "description": "Monitoring API for accessibility violation reporting and analytics", - "main": "dist/server.js", + "main": "src/Server.mjs", + "type": "module", "scripts": { - "build": "tsc", - "dev": "ts-node-dev --respawn src/server.ts", - "start": "node dist/server.js", - "test": "jest" + "build": "rescript build", + "dev": "rescript build -w", + "start": "node src/Server.mjs", + "clean": "rescript clean" }, "dependencies": { - "@accessibility-everywhere/core": "^1.0.0", - "@accessibility-everywhere/scanner": "^1.0.0", "express": "^4.18.2", "cors": "^2.8.5", "helmet": "^7.1.0", @@ -22,14 +21,7 @@ "uuid": "^14.0.0" }, "devDependencies": { - "@types/express": "^4.17.21", - "@types/cors": "^2.8.17", - "@types/compression": "^1.7.5", - "@types/uuid": "^9.0.7", - "@types/node": "^20.10.0", - "typescript": "^5.3.2", - "ts-node-dev": "^2.0.0", - "jest": "^29.7.0", - "@types/jest": "^29.5.8" + "rescript": "^11.1.0", + "@rescript/core": "^1.5.0" } } diff --git a/tools/monitoring-api/rescript.json b/tools/monitoring-api/rescript.json new file mode 100644 index 0000000..7ef37e2 --- /dev/null +++ b/tools/monitoring-api/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "@accessibility-everywhere/monitoring-api", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": { + "module": "esmodule", + "in-source": true + }, + "suffix": ".mjs", + "bs-dependencies": ["@rescript/core"], + "bsc-flags": ["-open RescriptCore"] +} diff --git a/tools/monitoring-api/src/Db.mjs b/tools/monitoring-api/src/Db.mjs new file mode 100644 index 0000000..d856702 --- /dev/null +++ b/tools/monitoring-api/src/Db.mjs @@ -0,0 +1,2 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE +/* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */ diff --git a/tools/monitoring-api/src/Db.res b/tools/monitoring-api/src/Db.res new file mode 100644 index 0000000..e8834ed --- /dev/null +++ b/tools/monitoring-api/src/Db.res @@ -0,0 +1,127 @@ +type collection +type cursor +type savedDoc = {_key: string} + +@send external collSave: (collection, 'a) => promise = "save" +@send external collUpdate: (collection, string, 'a) => promise = "update" +@send external collDocument: (collection, string) => promise<'a> = "document" +@send external collByExample: (collection, 'a) => promise = "byExample" +@send external collCount: collection => promise<{"count": int}> = "count" + +@send external cursorAll: cursor => promise> = "all" +@get external cursorCount: cursor => option = "count" + +type wcagLevel = [#A | #"AA" | #"AAA"] +type siteStatus = [#active | #inactive | #failed] + +type site = { + _key: string, + url: string, + domain: string, + firstScanned: Date.t, + lastScanned: Date.t, + scanCount: int, + currentScore: int, + previousScore?: int, + status: siteStatus, +} + +type scan = { + _key: string, + siteKey: string, + timestamp: string, + score: int, + violations: int, + passes: int, + incomplete: int, + url: string, + wcagLevel: wcagLevel, + duration: int, + userAgent?: string, +} + +type violation = { + _key: string, + scanKey: string, + siteKey: string, + wcagCriterion: string, + wcagLevel: wcagLevel, + impact: string, + description: string, + helpUrl: string, + selector: string, + html: string, + timestamp: Date.t, + fixed: bool, +} + +type organization = { + _key: string, + name: string, + tier: string, +} + +type criterionCount = {criterion: string, count: int} +type trendPoint = {timestamp: Date.t, violations: int, score: int} + +type service = { + sites: collection, + scans: collection, + violations: collection, + wcagCriteria: collection, + organizations: collection, + siteScans: collection, + scanViolations: collection, + violationCriteria: collection, + orgSites: collection, +} + +@module("@accessibility-everywhere/core") +external createArangoDBService: unit => service = "createArangoDBService" +@module("@accessibility-everywhere/core") external initialize: service => promise = "initialize" +@module("@accessibility-everywhere/core") external getSiteByUrl: (service, string) => promise> = "getSiteByUrl" +@module("@accessibility-everywhere/core") +external getRecentScansForSite: (service, string, ~limit: int=?) => promise> = "getRecentScansForSite" +@module("@accessibility-everywhere/core") +external getViolationsForScan: (service, string) => promise> = "getViolationsForScan" +@module("@accessibility-everywhere/core") +external getTopSites: (service, ~limit: int=?) => promise> = "getTopSites" +@module("@accessibility-everywhere/core") +external getCommonViolations: (service, ~limit: int=?) => promise> = "getCommonViolations" +@module("@accessibility-everywhere/core") +external getSiteViolationTrend: (service, string, ~days: int=?) => promise> = "getSiteViolationTrend" +@module("@accessibility-everywhere/core") +external getOrganizationSites: (service, string) => promise> = "getOrganizationSites" + +type scanner + +type scanOptions = { + url: string, + wcagLevel: wcagLevel, + screenshot?: bool, +} + +type scanNodeDetail = {target: array, html: string} +type scanViolationDetail = { + impact: string, + description: string, + helpUrl: string, + wcag: array, + nodes: array, +} +type scanPassDetail = {description: string} +type scanIncompleteDetail = {description: string} +type scanMetadata = {userAgent: string} +type scanResult = { + url: string, + timestamp: string, + score: int, + duration: int, + violations: array, + passes: array, + incomplete: array, + metadata: scanMetadata, +} + +@module("@accessibility-everywhere/scanner") external createScanner: unit => scanner = "createScanner" +@send external runScan: (scanner, scanOptions) => promise = "scan" diff --git a/tools/monitoring-api/src/Express.mjs b/tools/monitoring-api/src/Express.mjs new file mode 100644 index 0000000..96b6bb0 --- /dev/null +++ b/tools/monitoring-api/src/Express.mjs @@ -0,0 +1,21 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +var Cors = {}; + +var Helmet = {}; + +var Compression = {}; + +var RateLimit = {}; + +var Dotenv = {}; + +export { + Cors , + Helmet , + Compression , + RateLimit , + Dotenv , +} +/* No side effect */ diff --git a/tools/monitoring-api/src/Express.res b/tools/monitoring-api/src/Express.res new file mode 100644 index 0000000..8aa35bd --- /dev/null +++ b/tools/monitoring-api/src/Express.res @@ -0,0 +1,63 @@ +type app +type router +type req +type res +type nextFn = Exn.t => unit +type errorMiddleware = (Exn.t, req, res, nextFn) => unit +type middleware = (req, res, nextFn) => unit +type asyncHandler = (req, res, nextFn) => promise + +@module("express") external make: unit => app = "default" +@module("express") @scope("default") external router: unit => router = "Router" +@module("express") @scope("default") external jsonParser: {..} => middleware = "json" + +@send external use: (app, middleware) => unit = "use" +@send external usePath: (app, string, middleware) => unit = "use" +@send external useRouter: (app, string, router) => unit = "use" +@send external useError: (app, errorMiddleware) => unit = "use" +@send external useFinal: (app, middleware) => unit = "use" + +@send external get: (app, string, asyncHandler) => unit = "get" +@send external listen: (app, int, unit => unit) => unit = "listen" + +@send external routerGet: (router, string, asyncHandler) => unit = "get" +@send external routerPost: (router, string, asyncHandler) => unit = "post" +@send external routerPatch: (router, string, asyncHandler) => unit = "patch" +@send external routerPut: (router, string, asyncHandler) => unit = "put" +@send external routerDelete: (router, string, asyncHandler) => unit = "delete" + +@get external body: req => Dict.t = "body" +@get external params: req => Dict.t = "params" +@get external query: req => Dict.t = "query" +@get external protocol: req => string = "protocol" +@send external getHeader: (req, string) => string = "get" + +@send external resJson: (res, 'a) => unit = "json" +@send external status: (res, int) => res = "status" +@send external send: (res, string) => unit = "send" +@send external setHeader: (res, string, string) => unit = "setHeader" + +module Cors = { + @module("cors") external make: unit => middleware = "default" +} + +module Helmet = { + @module("helmet") external make: unit => middleware = "default" +} + +module Compression = { + @module("compression") external make: unit => middleware = "default" +} + +module RateLimit = { + type opts = { + windowMs: int, + max: int, + message: string, + } + @module("express-rate-limit") external make: opts => middleware = "default" +} + +module Dotenv = { + @module("dotenv") external config: unit => unit = "config" +} diff --git a/tools/monitoring-api/src/Joi.mjs b/tools/monitoring-api/src/Joi.mjs new file mode 100644 index 0000000..d856702 --- /dev/null +++ b/tools/monitoring-api/src/Joi.mjs @@ -0,0 +1,2 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE +/* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */ diff --git a/tools/monitoring-api/src/Joi.res b/tools/monitoring-api/src/Joi.res new file mode 100644 index 0000000..c63bbf4 --- /dev/null +++ b/tools/monitoring-api/src/Joi.res @@ -0,0 +1,16 @@ +type schema +type validationDetail = {message: string} +type validationError = {details: array} +type validationResult = {error: option, value: Dict.t} + +@module("joi") external object: Dict.t => schema = "object" + +@module("joi") external string: unit => schema = "string" +@module("joi") external boolean: unit => schema = "boolean" +@send external uri: schema => schema = "uri" +@send external required: schema => schema = "required" +@send external valid: (schema, array) => schema = "valid" +@send external defaultStr: (schema, string) => schema = "default" +@send external defaultBool: (schema, bool) => schema = "default" + +@send external validate: (schema, 'a) => validationResult = "validate" diff --git a/tools/monitoring-api/src/Server.mjs b/tools/monitoring-api/src/Server.mjs new file mode 100644 index 0000000..af93c0b --- /dev/null +++ b/tools/monitoring-api/src/Server.mjs @@ -0,0 +1,733 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Joi from "joi"; +import Cors from "cors"; +import * as Js_exn from "rescript/lib/es6/js_exn.js"; +import * as Dotenv from "dotenv"; +import Helmet from "helmet"; +import * as Express from "express"; +import Express$1 from "express"; +import * as Core__Int from "@rescript/core/src/Core__Int.mjs"; +import * as Caml_int32 from "rescript/lib/es6/caml_int32.js"; +import * as Core__JSON from "@rescript/core/src/Core__JSON.mjs"; +import * as Core__Array from "@rescript/core/src/Core__Array.mjs"; +import Compression from "compression"; +import * as Core__Option from "@rescript/core/src/Core__Option.mjs"; +import * as Core__Promise from "@rescript/core/src/Core__Promise.mjs"; +import * as Caml_js_exceptions from "rescript/lib/es6/caml_js_exceptions.js"; +import ExpressRateLimit from "express-rate-limit"; +import * as Core from "@accessibility-everywhere/core"; +import * as Scanner from "@accessibility-everywhere/scanner"; + +Dotenv.config(); + +var db = Core.createArangoDBService(); + +var scanner = Scanner.createScanner(); + +var app = Express$1(); + +var s = process.env.PORT; + +var port; + +switch (s) { + case "" : + case "undefined" : + port = 3000; + break; + default: + port = Core__Option.getOr(Core__Int.fromString(s, undefined), 3000); +} + +function nowIso() { + return (new Date().toISOString()); +} + +function nowDate() { + return new Date(); +} + +app.use(Helmet()); + +app.use(Cors()); + +app.use(Compression()); + +app.use(Express.default.json({ + limit: "10mb" + })); + +var limiter = ExpressRateLimit({ + windowMs: 900000, + max: 100, + message: "Too many requests from this IP, please try again later." + }); + +app.use("/v1/", limiter); + +async function initializeDatabase() { + try { + await Core.initialize(db); + console.log("āœ“ Database initialized successfully"); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + console.error("āœ— Database initialization failed:", err._1); + return (process.exit(1)); + } + throw err; + } +} + +function respondError(res, status, message) { + res.status(status); + res.json({ + error: { + message: message, + status: status + } + }); +} + +app.get("/health", (async function (_req, res, _next) { + res.json({ + status: "healthy", + timestamp: nowIso(), + version: "1.0.0" + }); + })); + +app.get("/v1", (async function (_req, res, _next) { + res.json({ + name: "Accessibility Everywhere Monitoring API", + version: "1.0.0", + description: "Accessibility violation reporting and analytics API", + endpoints: { + scan: "/v1/scan", + violations: "/v1/violations", + leaderboard: "/v1/leaderboard", + badge: "/v1/badge/:domain", + stats: "/v1/stats", + dashboard: "/v1/dashboard/:orgId" + }, + documentation: "https://docs.accessibility-everywhere.org/api" + }); + })); + +var scanRouter = Express.default.Router(); + +var scanSchema = Joi.object(Object.fromEntries([ + [ + "url", + Joi.string().uri().required() + ], + [ + "wcagLevel", + Joi.string().valid([ + "A", + "AA", + "AAA" + ]).default("AA") + ], + [ + "screenshot", + Joi.boolean().default(false) + ] + ])); + +function parseWcagLevel(s) { + switch (s) { + case "A" : + return "A"; + case "AAA" : + return "AAA"; + default: + return "AA"; + } +} + +scanRouter.post("/", (async function (req, res, next) { + try { + var result = scanSchema.validate(req.body); + var err = result.error; + if (err !== undefined) { + var msg = err.details[0]; + return respondError(res, 400, msg.message); + } + var value = result.value; + var url = Core__Option.getOr(Core__JSON.Decode.string(value["url"]), ""); + var wcagLevelStr = Core__Option.getOr(Core__JSON.Decode.string(value["wcagLevel"]), "AA"); + var wcagLevel = parseWcagLevel(wcagLevelStr); + var screenshot = Core__Option.getOr(Core__JSON.Decode.bool(value["screenshot"]), false); + var scanResult = await scanner.scan({ + url: url, + wcagLevel: wcagLevel, + screenshot: screenshot + }); + var urlObj = new URL(url); + var domain = urlObj.hostname; + var existingSite = await Core.getSiteByUrl(db, url); + var siteKey = existingSite !== undefined ? (await db.sites.update(existingSite._key, { + lastScanned: new Date(), + scanCount: existingSite.scanCount + 1 | 0, + previousScore: existingSite.currentScore, + currentScore: scanResult.score + }), existingSite._key) : (await db.sites.save({ + url: url, + domain: domain, + firstScanned: new Date(), + lastScanned: new Date(), + scanCount: 1, + currentScore: scanResult.score, + status: "active" + }))._key; + var scanDoc = await db.scans.save({ + siteKey: siteKey, + timestamp: scanResult.timestamp, + score: scanResult.score, + violations: scanResult.violations.length, + passes: scanResult.passes.length, + incomplete: scanResult.incomplete.length, + url: url, + wcagLevel: wcagLevelStr, + duration: scanResult.duration, + userAgent: scanResult.metadata.userAgent + }); + for(var vi = 0 ,vi_finish = scanResult.violations.length; vi < vi_finish; ++vi){ + var v = scanResult.violations[vi]; + for(var ni = 0 ,ni_finish = v.nodes.length; ni < ni_finish; ++ni){ + var node = v.nodes[ni]; + await db.violations.save({ + scanKey: scanDoc._key, + siteKey: siteKey, + wcagCriterion: Core__Option.getOr(v.wcag[0], "unknown"), + wcagLevel: wcagLevelStr, + impact: v.impact, + description: v.description, + helpUrl: v.helpUrl, + selector: node.target.join(" > "), + html: node.html, + timestamp: new Date(), + fixed: false + }); + } + } + res.json({ + success: true, + data: { + url: url, + scanId: scanDoc._key, + score: scanResult.score, + violations: scanResult.violations.length, + passes: scanResult.passes.length, + incomplete: scanResult.incomplete.length, + wcagLevel: wcagLevelStr, + timestamp: scanResult.timestamp, + details: { + violations: scanResult.violations, + passes: scanResult.passes, + incomplete: scanResult.incomplete + } + } + }); + return ; + } + catch (raw_err){ + var err$1 = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err$1.RE_EXN_ID === Js_exn.$$Error) { + next(err$1._1); + return ; + } + throw err$1; + } + })); + +scanRouter.get("/:scanId", (async function (req, res, next) { + try { + var scanId = req.params["scanId"]; + var scanDoc = await db.scans.document(scanId); + var violations = await Core.getViolationsForScan(db, scanId); + res.json({ + success: true, + data: { + scan: scanDoc, + violations: violations + } + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +app.use("/v1/scan", scanRouter); + +var violationsRouter = Express.default.Router(); + +violationsRouter.post("/", (async function (req, res, next) { + try { + var body = req.body; + var url = Core__Option.flatMap(body["url"], Core__JSON.Decode.string); + var violation = body["violation"]; + if (url === undefined) { + return respondError(res, 400, "URL and violation are required"); + } + if (violation === undefined) { + return respondError(res, 400, "URL and violation are required"); + } + var existing = await Core.getSiteByUrl(db, url); + var siteKey; + if (existing !== undefined) { + siteKey = existing._key; + } else { + var urlObj = new URL(url); + siteKey = (await db.sites.save({ + url: url, + domain: urlObj.hostname, + firstScanned: new Date(), + lastScanned: new Date(), + scanCount: 0, + currentScore: 0, + status: "active" + }))._key; + } + await db.violations.save({ + siteKey: siteKey, + scanKey: "", + violationData: violation, + timestamp: new Date(), + fixed: false + }); + res.json({ + success: true, + message: "Violation reported successfully" + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +violationsRouter.get("/common", (async function (req, res, next) { + try { + var limit = Core__Option.getOr(Core__Option.flatMap(req.query["limit"], (function (s) { + return Core__Int.fromString(s, undefined); + })), 10); + var violations = await Core.getCommonViolations(db, limit); + res.json({ + success: true, + data: violations + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +violationsRouter.get("/site/:siteKey", (async function (req, res, next) { + try { + var siteKey = req.params["siteKey"]; + var fixed = req.query["fixed"] === "true"; + var cursor = await db.violations.byExample({ + siteKey: siteKey, + fixed: fixed + }); + var violations = await cursor.all(); + res.json({ + success: true, + data: violations + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +violationsRouter.patch("/:violationId/fixed", (async function (req, res, next) { + try { + var violationId = req.params["violationId"]; + await db.violations.update(violationId, { + fixed: true + }); + res.json({ + success: true, + message: "Violation marked as fixed" + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +app.use("/v1/violations", violationsRouter); + +var leaderboardRouter = Express.default.Router(); + +leaderboardRouter.get("/", (async function (req, res, next) { + try { + var limit = Core__Option.getOr(Core__Option.flatMap(req.query["limit"], (function (s) { + return Core__Int.fromString(s, undefined); + })), 100); + var sites = await Core.getTopSites(db, limit); + var leaderboard = sites.map(function (site, i) { + var prev = site.previousScore; + var trend = prev !== undefined ? site.currentScore - prev | 0 : 0; + return { + rank: i + 1 | 0, + domain: site.domain, + url: site.url, + score: site.currentScore, + violations: site.scanCount, + lastScanned: site.lastScanned, + trend: trend + }; + }); + res.json({ + success: true, + data: { + sites: leaderboard, + total: leaderboard.length, + lastUpdated: nowIso() + } + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +leaderboardRouter.get("/category/:category", (async function (req, res, next) { + try { + var category = req.params["category"]; + var limit = Core__Option.getOr(Core__Option.flatMap(req.query["limit"], (function (s) { + return Core__Int.fromString(s, undefined); + })), 100); + var sites = await Core.getTopSites(db, limit); + res.json({ + success: true, + data: { + category: category, + sites: sites, + total: sites.length + } + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +app.use("/v1/leaderboard", leaderboardRouter); + +var badgeRouter = Express.default.Router(); + +function getGrade(score) { + if (score >= 90) { + return "A"; + } else if (score >= 80) { + return "B"; + } else if (score >= 70) { + return "C"; + } else if (score >= 60) { + return "D"; + } else { + return "F"; + } +} + +function gradeColor(grade) { + switch (grade) { + case "A" : + return "#28a745"; + case "B" : + return "#8bc34a"; + case "C" : + return "#ffc107"; + case "D" : + return "#ff9800"; + default: + return "#dc3545"; + } +} + +function generateBadgeSVG(score) { + var grade = getGrade(score); + var color = gradeColor(grade); + var scoreStr = score.toString(); + return ("\n \n Accessibility Score: " + scoreStr + " (Grade " + grade + ")\n \n \n \n \n \n \n \n \n \n \n \n \n \n accessibility\n accessibility\n " + grade + " (" + scoreStr + ")\n " + grade + " (" + scoreStr + ")\n \n \n ").trim(); +} + +badgeRouter.get("/:domain", (async function (req, res, next) { + try { + var domain = req.params["domain"]; + var format = Core__Option.getOr(req.query["format"], "json"); + var cursor = await db.sites.byExample({ + domain: domain + }); + var sites = await cursor.all(); + if (sites.length === 0) { + return respondError(res, 404, "Site not found"); + } + var site = sites[0]; + if (format === "svg") { + var svg = generateBadgeSVG(site.currentScore); + res.setHeader("Content-Type", "image/svg+xml"); + res.setHeader("Cache-Control", "public, max-age=3600"); + res.send(svg); + return ; + } + var host = req.get("host"); + var proto = req.protocol; + res.json({ + success: true, + data: { + domain: domain, + score: site.currentScore, + grade: getGrade(site.currentScore), + lastScanned: site.lastScanned, + badgeUrl: proto + "://" + host + "/v1/badge/" + domain + "?format=svg" + } + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +app.use("/v1/badge", badgeRouter); + +var statsRouter = Express.default.Router(); + +statsRouter.get("/", (async function (_req, res, next) { + try { + var sitesCount = await db.sites.count(); + var scansCount = await db.scans.count(); + var violationsCount = await db.violations.count(); + var commonViolations = await Core.getCommonViolations(db, 5); + res.json({ + success: true, + data: { + totalSites: sitesCount.count, + totalScans: scansCount.count, + totalViolations: violationsCount.count, + commonViolations: commonViolations, + timestamp: nowIso() + } + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +statsRouter.get("/site/:siteKey", (async function (req, res, next) { + try { + var siteKey = req.params["siteKey"]; + var site = await db.sites.document(siteKey); + var scans = await Core.getRecentScansForSite(db, siteKey, 30); + var trend = await Core.getSiteViolationTrend(db, siteKey, 30); + res.json({ + success: true, + data: { + site: site, + recentScans: scans, + trend: trend + } + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +app.use("/v1/stats", statsRouter); + +var dashboardRouter = Express.default.Router(); + +dashboardRouter.get("/:orgId", (async function (req, res, next) { + try { + var orgId = req.params["orgId"]; + var org = await db.organizations.document(orgId); + var sites = await Core.getOrganizationSites(db, orgId); + var totalSites = sites.length; + var averageScore; + if (totalSites > 0) { + var sum = Core__Array.reduce(sites, 0, (function (acc, site) { + return acc + site.currentScore | 0; + })); + averageScore = Caml_int32.div(sum, totalSites); + } else { + averageScore = 0; + } + var totalViolations = 0; + for(var i = 0 ,i_finish = sites.length; i < i_finish; ++i){ + var site = sites[i]; + var cursor = await db.violations.byExample({ + siteKey: site._key, + fixed: false + }); + var count = Core__Option.getOr(cursor.count, 0); + totalViolations = totalViolations + count | 0; + } + var siteSummaries = sites.map(function (site) { + return { + domain: site.domain, + url: site.url, + score: site.currentScore, + lastScanned: site.lastScanned + }; + }); + res.json({ + success: true, + data: { + organization: { + name: org.name, + tier: org.tier + }, + stats: { + totalSites: totalSites, + averageScore: averageScore, + totalViolations: totalViolations + }, + sites: siteSummaries + } + }); + return ; + } + catch (raw_err){ + var err = Caml_js_exceptions.internalToOCamlException(raw_err); + if (err.RE_EXN_ID === Js_exn.$$Error) { + next(err._1); + return ; + } + throw err; + } + })); + +app.use("/v1/dashboard", dashboardRouter); + +app.use(function (err, _req, res, _next) { + console.error("Error:", err); + var message = Core__Option.getOr(err.message, "Internal server error"); + res.status(500); + res.json({ + error: { + message: message, + status: 500 + } + }); + }); + +app.use(function (_req, res, _next) { + res.status(404); + res.json({ + error: { + message: "Endpoint not found", + status: 404 + } + }); + }); + +async function startServer() { + await initializeDatabase(); + app.listen(port, (function () { + console.log("āœ“ Accessibility Everywhere API listening on port " + port.toString()); + var env = Core__Option.getOr(process.env.NODE_ENV, "development"); + console.log("āœ“ Environment: " + env); + console.log("āœ“ Health check: http://localhost:" + port.toString() + "/health"); + console.log("āœ“ API docs: http://localhost:" + port.toString() + "/v1"); + })); +} + +Core__Promise.$$catch(startServer(), (function (err) { + console.error("Failed to start server:", err); + return (process.exit(1)); + })); + +export { + db , + scanner , + app , + port , + nowIso , + nowDate , + limiter , + initializeDatabase , + respondError , + scanRouter , + scanSchema , + parseWcagLevel , + violationsRouter , + leaderboardRouter , + badgeRouter , + getGrade , + gradeColor , + generateBadgeSVG , + statsRouter , + dashboardRouter , + startServer , +} +/* Not a pure module */ diff --git a/tools/monitoring-api/src/Server.res b/tools/monitoring-api/src/Server.res new file mode 100644 index 0000000..a47a6c0 --- /dev/null +++ b/tools/monitoring-api/src/Server.res @@ -0,0 +1,631 @@ +Express.Dotenv.config() + +let db = Db.createArangoDBService() +let scanner = Db.createScanner() + +let app = Express.make() +let port = switch %raw(`process.env.PORT`) { +| "" | "undefined" => 3000 +| s => s->Int.fromString->Option.getOr(3000) +} + +@val external nodeEnv: option = "process.env.NODE_ENV" +@val external consoleLog: string => unit = "console.log" +@val external consoleError: (string, 'a) => unit = "console.error" + +@new external newUrl: string => {"hostname": string} = "URL" +let nowIso = (): string => %raw(`new Date().toISOString()`) +let nowDate = (): Date.t => Date.make() + +let _ = Express.use(app, Express.Helmet.make()) +let _ = Express.use(app, Express.Cors.make()) +let _ = Express.use(app, Express.Compression.make()) +let _ = Express.use(app, Express.jsonParser({"limit": "10mb"})) + +let limiter = Express.RateLimit.make({ + windowMs: 15 * 60 * 1000, + max: 100, + message: "Too many requests from this IP, please try again later.", +}) +let _ = Express.usePath(app, "/v1/", limiter) + +let initializeDatabase = async () => { + try { + await Db.initialize(db) + consoleLog("āœ“ Database initialized successfully") + } catch { + | Exn.Error(err) => { + consoleError("āœ— Database initialization failed:", err) + %raw(`process.exit(1)`) + } + } +} + +let respondError = (res, status, message) => { + let _ = Express.status(res, status) + Express.resJson(res, {"error": {"message": message, "status": status}}) +} + +let _ = Express.get(app, "/health", async (_req, res, _next) => { + Express.resJson( + res, + { + "status": "healthy", + "timestamp": nowIso(), + "version": "1.0.0", + }, + ) +}) + +let _ = Express.get(app, "/v1", async (_req, res, _next) => { + Express.resJson( + res, + { + "name": "Accessibility Everywhere Monitoring API", + "version": "1.0.0", + "description": "Accessibility violation reporting and analytics API", + "endpoints": { + "scan": "/v1/scan", + "violations": "/v1/violations", + "leaderboard": "/v1/leaderboard", + "badge": "/v1/badge/:domain", + "stats": "/v1/stats", + "dashboard": "/v1/dashboard/:orgId", + }, + "documentation": "https://docs.accessibility-everywhere.org/api", + }, + ) +}) + +// ============================================================ +// /v1/scan +// ============================================================ +let scanRouter = Express.router() + +let scanSchema = Joi.object( + Dict.fromArray([ + ("url", Joi.string()->Joi.uri->Joi.required), + ("wcagLevel", Joi.string()->Joi.valid(["A", "AA", "AAA"])->Joi.defaultStr("AA")), + ("screenshot", Joi.boolean()->Joi.defaultBool(false)), + ]), +) + +let parseWcagLevel = (s): Db.wcagLevel => + switch s { + | "A" => #A + | "AAA" => #"AAA" + | _ => #"AA" + } + +Express.routerPost(scanRouter, "/", async (req, res, next) => { + try { + let result = scanSchema->Joi.validate(Express.body(req)) + switch result.error { + | Some(err) => { + let msg = err.details->Array.getUnsafe(0) + respondError(res, 400, msg.message) + } + | None => { + let value = result.value + let url = value->Dict.getUnsafe("url")->JSON.Decode.string->Option.getOr("") + let wcagLevelStr = + value->Dict.getUnsafe("wcagLevel")->JSON.Decode.string->Option.getOr("AA") + let wcagLevel = parseWcagLevel(wcagLevelStr) + let screenshot = + value->Dict.getUnsafe("screenshot")->JSON.Decode.bool->Option.getOr(false) + + let scanResult = await scanner->Db.runScan({url, wcagLevel, screenshot}) + + let urlObj = newUrl(url) + let domain = urlObj["hostname"] + + let existingSite = await db->Db.getSiteByUrl(url) + let siteKey = switch existingSite { + | None => { + let siteDoc = await Db.collSave( + db.sites, + { + "url": url, + "domain": domain, + "firstScanned": nowDate(), + "lastScanned": nowDate(), + "scanCount": 1, + "currentScore": scanResult.score, + "status": "active", + }, + ) + siteDoc._key + } + | Some(site) => { + await Db.collUpdate( + db.sites, + site._key, + { + "lastScanned": nowDate(), + "scanCount": site.scanCount + 1, + "previousScore": site.currentScore, + "currentScore": scanResult.score, + }, + ) + site._key + } + } + + let scanDoc = await Db.collSave( + db.scans, + { + "siteKey": siteKey, + "timestamp": scanResult.timestamp, + "score": scanResult.score, + "violations": Array.length(scanResult.violations), + "passes": Array.length(scanResult.passes), + "incomplete": Array.length(scanResult.incomplete), + "url": url, + "wcagLevel": wcagLevelStr, + "duration": scanResult.duration, + "userAgent": scanResult.metadata.userAgent, + }, + ) + + for vi in 0 to Array.length(scanResult.violations) - 1 { + let v = scanResult.violations->Array.getUnsafe(vi) + for ni in 0 to Array.length(v.nodes) - 1 { + let node = v.nodes->Array.getUnsafe(ni) + let _ = await Db.collSave( + db.violations, + { + "scanKey": scanDoc._key, + "siteKey": siteKey, + "wcagCriterion": v.wcag->Array.get(0)->Option.getOr("unknown"), + "wcagLevel": wcagLevelStr, + "impact": v.impact, + "description": v.description, + "helpUrl": v.helpUrl, + "selector": node.target->Array.join(" > "), + "html": node.html, + "timestamp": nowDate(), + "fixed": false, + }, + ) + } + } + + Express.resJson( + res, + { + "success": true, + "data": { + "url": url, + "scanId": scanDoc._key, + "score": scanResult.score, + "violations": Array.length(scanResult.violations), + "passes": Array.length(scanResult.passes), + "incomplete": Array.length(scanResult.incomplete), + "wcagLevel": wcagLevelStr, + "timestamp": scanResult.timestamp, + "details": { + "violations": scanResult.violations, + "passes": scanResult.passes, + "incomplete": scanResult.incomplete, + }, + }, + }, + ) + } + } + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +Express.routerGet(scanRouter, "/:scanId", async (req, res, next) => { + try { + let scanId = Express.params(req)->Dict.getUnsafe("scanId") + let scanDoc: Db.scan = await Db.collDocument(db.scans, scanId) + let violations = await db->Db.getViolationsForScan(scanId) + Express.resJson( + res, + { + "success": true, + "data": {"scan": scanDoc, "violations": violations}, + }, + ) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +let _ = Express.useRouter(app, "/v1/scan", scanRouter) + +// ============================================================ +// /v1/violations +// ============================================================ +let violationsRouter = Express.router() + +Express.routerPost(violationsRouter, "/", async (req, res, next) => { + try { + let body = Express.body(req) + let url = body->Dict.get("url")->Option.flatMap(JSON.Decode.string) + let violation = body->Dict.get("violation") + switch (url, violation) { + | (Some(url), Some(_)) => { + let existing = await db->Db.getSiteByUrl(url) + let siteKey = switch existing { + | None => { + let urlObj = newUrl(url) + let saved = await Db.collSave( + db.sites, + { + "url": url, + "domain": urlObj["hostname"], + "firstScanned": nowDate(), + "lastScanned": nowDate(), + "scanCount": 0, + "currentScore": 0, + "status": "active", + }, + ) + saved._key + } + | Some(site) => site._key + } + + let _ = await Db.collSave( + db.violations, + { + "siteKey": siteKey, + "scanKey": "", + "violationData": violation, + "timestamp": nowDate(), + "fixed": false, + }, + ) + + Express.resJson(res, {"success": true, "message": "Violation reported successfully"}) + } + | _ => respondError(res, 400, "URL and violation are required") + } + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +Express.routerGet(violationsRouter, "/common", async (req, res, next) => { + try { + let limit = + Express.query(req)->Dict.get("limit")->Option.flatMap(s => Int.fromString(s))->Option.getOr(10) + let violations = await db->Db.getCommonViolations(~limit) + Express.resJson(res, {"success": true, "data": violations}) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +Express.routerGet(violationsRouter, "/site/:siteKey", async (req, res, next) => { + try { + let siteKey = Express.params(req)->Dict.getUnsafe("siteKey") + let fixed = Express.query(req)->Dict.get("fixed") === Some("true") + let cursor = await Db.collByExample(db.violations, {"siteKey": siteKey, "fixed": fixed}) + let violations: array = await Db.cursorAll(cursor) + Express.resJson(res, {"success": true, "data": violations}) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +Express.routerPatch(violationsRouter, "/:violationId/fixed", async (req, res, next) => { + try { + let violationId = Express.params(req)->Dict.getUnsafe("violationId") + await Db.collUpdate(db.violations, violationId, {"fixed": true}) + Express.resJson(res, {"success": true, "message": "Violation marked as fixed"}) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +let _ = Express.useRouter(app, "/v1/violations", violationsRouter) + +// ============================================================ +// /v1/leaderboard +// ============================================================ +let leaderboardRouter = Express.router() + +Express.routerGet(leaderboardRouter, "/", async (req, res, next) => { + try { + let limit = + Express.query(req)->Dict.get("limit")->Option.flatMap(s => Int.fromString(s))->Option.getOr(100) + let sites = await db->Db.getTopSites(~limit) + let leaderboard = sites->Array.mapWithIndex((site: Db.site, i) => { + let trend = switch site.previousScore { + | Some(prev) => site.currentScore - prev + | None => 0 + } + { + "rank": i + 1, + "domain": site.domain, + "url": site.url, + "score": site.currentScore, + "violations": site.scanCount, + "lastScanned": site.lastScanned, + "trend": trend, + } + }) + Express.resJson( + res, + { + "success": true, + "data": { + "sites": leaderboard, + "total": Array.length(leaderboard), + "lastUpdated": nowIso(), + }, + }, + ) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +Express.routerGet(leaderboardRouter, "/category/:category", async (req, res, next) => { + try { + let category = Express.params(req)->Dict.getUnsafe("category") + let limit = + Express.query(req)->Dict.get("limit")->Option.flatMap(s => Int.fromString(s))->Option.getOr(100) + let sites = await db->Db.getTopSites(~limit) + Express.resJson( + res, + { + "success": true, + "data": { + "category": category, + "sites": sites, + "total": Array.length(sites), + }, + }, + ) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +let _ = Express.useRouter(app, "/v1/leaderboard", leaderboardRouter) + +// ============================================================ +// /v1/badge +// ============================================================ +let badgeRouter = Express.router() + +let getGrade = score => + if score >= 90 { + "A" + } else if score >= 80 { + "B" + } else if score >= 70 { + "C" + } else if score >= 60 { + "D" + } else { + "F" + } + +let gradeColor = grade => + switch grade { + | "A" => "#28a745" + | "B" => "#8bc34a" + | "C" => "#ffc107" + | "D" => "#ff9800" + | _ => "#dc3545" + } + +let generateBadgeSVG = score => { + let grade = getGrade(score) + let color = gradeColor(grade) + let scoreStr = Int.toString(score) + String.trim( + ` + + Accessibility Score: ${scoreStr} (Grade ${grade}) + + + + + + + + + + + + + + + accessibility + + ${grade} (${scoreStr}) + + + `, + ) +} + +Express.routerGet(badgeRouter, "/:domain", async (req, res, next) => { + try { + let domain = Express.params(req)->Dict.getUnsafe("domain") + let format = Express.query(req)->Dict.get("format")->Option.getOr("json") + let cursor = await Db.collByExample(db.sites, {"domain": domain}) + let sites: array = await Db.cursorAll(cursor) + + if Array.length(sites) === 0 { + respondError(res, 404, "Site not found") + } else { + let site = sites->Array.getUnsafe(0) + if format === "svg" { + let svg = generateBadgeSVG(site.currentScore) + Express.setHeader(res, "Content-Type", "image/svg+xml") + Express.setHeader(res, "Cache-Control", "public, max-age=3600") + Express.send(res, svg) + } else { + let host = Express.getHeader(req, "host") + let proto = Express.protocol(req) + Express.resJson( + res, + { + "success": true, + "data": { + "domain": domain, + "score": site.currentScore, + "grade": getGrade(site.currentScore), + "lastScanned": site.lastScanned, + "badgeUrl": `${proto}://${host}/v1/badge/${domain}?format=svg`, + }, + }, + ) + } + } + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +let _ = Express.useRouter(app, "/v1/badge", badgeRouter) + +// ============================================================ +// /v1/stats +// ============================================================ +let statsRouter = Express.router() + +Express.routerGet(statsRouter, "/", async (_req, res, next) => { + try { + let sitesCount = await Db.collCount(db.sites) + let scansCount = await Db.collCount(db.scans) + let violationsCount = await Db.collCount(db.violations) + let commonViolations = await db->Db.getCommonViolations(~limit=5) + Express.resJson( + res, + { + "success": true, + "data": { + "totalSites": sitesCount["count"], + "totalScans": scansCount["count"], + "totalViolations": violationsCount["count"], + "commonViolations": commonViolations, + "timestamp": nowIso(), + }, + }, + ) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +Express.routerGet(statsRouter, "/site/:siteKey", async (req, res, next) => { + try { + let siteKey = Express.params(req)->Dict.getUnsafe("siteKey") + let site: Db.site = await Db.collDocument(db.sites, siteKey) + let scans = await db->Db.getRecentScansForSite(siteKey, ~limit=30) + let trend = await db->Db.getSiteViolationTrend(siteKey, ~days=30) + Express.resJson( + res, + { + "success": true, + "data": { + "site": site, + "recentScans": scans, + "trend": trend, + }, + }, + ) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +let _ = Express.useRouter(app, "/v1/stats", statsRouter) + +// ============================================================ +// /v1/dashboard +// ============================================================ +let dashboardRouter = Express.router() + +Express.routerGet(dashboardRouter, "/:orgId", async (req, res, next) => { + try { + let orgId = Express.params(req)->Dict.getUnsafe("orgId") + let org: Db.organization = await Db.collDocument(db.organizations, orgId) + let sites = await db->Db.getOrganizationSites(orgId) + let totalSites = Array.length(sites) + let averageScore = if totalSites > 0 { + let sum = + sites->Array.reduce(0, (acc, site: Db.site) => acc + site.currentScore) + sum / totalSites + } else { + 0 + } + let totalViolations = ref(0) + for i in 0 to Array.length(sites) - 1 { + let site = sites->Array.getUnsafe(i) + let cursor = await Db.collByExample( + db.violations, + {"siteKey": site._key, "fixed": false}, + ) + let count = Db.cursorCount(cursor)->Option.getOr(0) + totalViolations := totalViolations.contents + count + } + let siteSummaries = sites->Array.map((site: Db.site) => { + "domain": site.domain, + "url": site.url, + "score": site.currentScore, + "lastScanned": site.lastScanned, + }) + Express.resJson( + res, + { + "success": true, + "data": { + "organization": {"name": org.name, "tier": org.tier}, + "stats": { + "totalSites": totalSites, + "averageScore": averageScore, + "totalViolations": totalViolations.contents, + }, + "sites": siteSummaries, + }, + }, + ) + } catch { + | Exn.Error(err) => next(. err)->ignore + } +}) + +let _ = Express.useRouter(app, "/v1/dashboard", dashboardRouter) + +// ============================================================ +// Error handling + 404 + boot +// ============================================================ +Express.useError(app, (err, _req, res, _next) => { + consoleError("Error:", err) + let status = 500 + let message = Exn.message(err)->Option.getOr("Internal server error") + let _ = Express.status(res, status) + Express.resJson(res, {"error": {"message": message, "status": status}}) +}) + +Express.useFinal(app, (_req, res, _next) => { + let _ = Express.status(res, 404) + Express.resJson(res, {"error": {"message": "Endpoint not found", "status": 404}}) +}) + +let startServer = async () => { + await initializeDatabase() + Express.listen(app, port, () => { + consoleLog(`āœ“ Accessibility Everywhere API listening on port ${Int.toString(port)}`) + let env = nodeEnv->Option.getOr("development") + consoleLog(`āœ“ Environment: ${env}`) + consoleLog(`āœ“ Health check: http://localhost:${Int.toString(port)}/health`) + consoleLog(`āœ“ API docs: http://localhost:${Int.toString(port)}/v1`) + }) +} + +let _ = startServer()->Promise.catch(err => { + consoleError("Failed to start server:", err) + %raw(`process.exit(1)`) +}) diff --git a/tools/monitoring-api/src/routes/badge.ts b/tools/monitoring-api/src/routes/badge.ts deleted file mode 100644 index 8d53078..0000000 --- a/tools/monitoring-api/src/routes/badge.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const badgeRouter = Router(); - -// GET /v1/badge/:domain - Get badge for a domain -badgeRouter.get('/:domain', async (req, res, next) => { - try { - const { domain } = req.params; - const format = req.query.format || 'json'; - - // Find site by domain - const sites = await db.sites.byExample({ domain }).then(c => c.all()); - - if (sites.length === 0) { - return res.status(404).json({ - error: { - message: 'Site not found', - status: 404, - }, - }); - } - - const site = sites[0]; - - if (format === 'svg') { - // Generate SVG badge - const svg = generateBadgeSVG(site.currentScore); - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'public, max-age=3600'); - res.send(svg); - } else { - // Return JSON - res.json({ - success: true, - data: { - domain, - score: site.currentScore, - grade: getGrade(site.currentScore), - lastScanned: site.lastScanned, - badgeUrl: `${req.protocol}://${req.get('host')}/v1/badge/${domain}?format=svg`, - }, - }); - } - } catch (error) { - next(error); - } -}); - -function getGrade(score: number): string { - if (score >= 90) return 'A'; - if (score >= 80) return 'B'; - if (score >= 70) return 'C'; - if (score >= 60) return 'D'; - return 'F'; -} - -function generateBadgeSVG(score: number): string { - const grade = getGrade(score); - const color = { - A: '#28a745', - B: '#8bc34a', - C: '#ffc107', - D: '#ff9800', - F: '#dc3545', - }[grade]; - - return ` - - Accessibility Score: ${score} (Grade ${grade}) - - - - - - - - - - - - - - - accessibility - - ${grade} (${score}) - - - `.trim(); -} diff --git a/tools/monitoring-api/src/routes/dashboard.ts b/tools/monitoring-api/src/routes/dashboard.ts deleted file mode 100644 index 4ecf9d5..0000000 --- a/tools/monitoring-api/src/routes/dashboard.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const dashboardRouter = Router(); - -// GET /v1/dashboard/:orgId - Get organization dashboard -dashboardRouter.get('/:orgId', async (req, res, next) => { - try { - const { orgId } = req.params; - - // Get organization - const org = await db.organizations.document(orgId); - if (!org) { - return res.status(404).json({ - error: { - message: 'Organization not found', - status: 404, - }, - }); - } - - // Get organization sites - const sites = await db.getOrganizationSites(orgId); - - // Calculate aggregate stats - const totalSites = sites.length; - const averageScore = - totalSites > 0 - ? sites.reduce((sum, site) => sum + site.currentScore, 0) / totalSites - : 0; - - // Count total violations across all sites - let totalViolations = 0; - for (const site of sites) { - const cursor = await db.violations.byExample({ - siteKey: site._key, - fixed: false, - }); - const count = cursor.count ?? 0; - totalViolations += count; - } - - res.json({ - success: true, - data: { - organization: { - name: org.name, - tier: org.tier, - }, - stats: { - totalSites, - averageScore: Math.round(averageScore), - totalViolations, - }, - sites: sites.map(site => ({ - domain: site.domain, - url: site.url, - score: site.currentScore, - lastScanned: site.lastScanned, - })), - }, - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/routes/leaderboard.ts b/tools/monitoring-api/src/routes/leaderboard.ts deleted file mode 100644 index 92f41a5..0000000 --- a/tools/monitoring-api/src/routes/leaderboard.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const leaderboardRouter = Router(); - -// GET /v1/leaderboard - Get top sites by accessibility score -leaderboardRouter.get('/', async (req, res, next) => { - try { - const limit = parseInt(req.query.limit as string) || 100; - const sites = await db.getTopSites(limit); - - const leaderboard = sites.map((site, index) => ({ - rank: index + 1, - domain: site.domain, - url: site.url, - score: site.currentScore, - violations: site.scanCount || 0, - lastScanned: site.lastScanned, - trend: site.previousScore - ? site.currentScore - site.previousScore - : 0, - })); - - res.json({ - success: true, - data: { - sites: leaderboard, - total: leaderboard.length, - lastUpdated: new Date().toISOString(), - }, - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/leaderboard/category/:category - Get leaderboard by category -leaderboardRouter.get('/category/:category', async (req, res, next) => { - try { - const { category } = req.params; - const limit = parseInt(req.query.limit as string) || 100; - - // This would filter by category in production - // For now, return top sites - const sites = await db.getTopSites(limit); - - res.json({ - success: true, - data: { - category, - sites, - total: sites.length, - }, - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/routes/scan.ts b/tools/monitoring-api/src/routes/scan.ts deleted file mode 100644 index 1d29cce..0000000 --- a/tools/monitoring-api/src/routes/scan.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Router } from 'express'; -import Joi from 'joi'; -import { db, scanner } from '../server'; - -export const scanRouter = Router(); - -// Validation schema -const scanSchema = Joi.object({ - url: Joi.string().uri().required(), - wcagLevel: Joi.string().valid('A', 'AA', 'AAA').default('AA'), - screenshot: Joi.boolean().default(false), -}); - -// POST /v1/scan - Scan a URL for accessibility issues -scanRouter.post('/', async (req, res, next) => { - try { - // Validate request - const { error, value } = scanSchema.validate(req.body); - if (error) { - return res.status(400).json({ - error: { - message: error.details[0].message, - status: 400, - }, - }); - } - - const { url, wcagLevel, screenshot } = value; - - // Run scan - const result = await scanner.scan({ - url, - wcagLevel, - screenshot, - }); - - // Extract domain - const urlObj = new URL(url); - const domain = urlObj.hostname; - - // Store in database - let site = await db.getSiteByUrl(url); - let siteKey: string; - - if (!site) { - // Create new site - const siteDoc = await db.sites.save({ - url, - domain, - firstScanned: new Date(), - lastScanned: new Date(), - scanCount: 1, - currentScore: result.score, - status: 'active', - } as any); - siteKey = siteDoc._key; - } else { - // Update existing site - siteKey = site._key; - await db.sites.update(site._key, { - lastScanned: new Date(), - scanCount: (site.scanCount || 0) + 1, - previousScore: site.currentScore, - currentScore: result.score, - }); - } - - // Store scan - const scanDoc = await db.scans.save({ - siteKey, - timestamp: result.timestamp, - score: result.score, - violations: result.violations.length, - passes: result.passes.length, - incomplete: result.incomplete.length, - url, - wcagLevel, - duration: result.duration, - userAgent: result.metadata.userAgent, - } as any); - - // Store violations - for (const violation of result.violations) { - for (const node of violation.nodes) { - await db.violations.save({ - scanKey: scanDoc._key, - siteKey, - wcagCriterion: violation.wcag[0] || 'unknown', - wcagLevel: wcagLevel, - impact: violation.impact, - description: violation.description, - helpUrl: violation.helpUrl, - selector: node.target.join(' > '), - html: node.html, - timestamp: new Date(), - fixed: false, - } as any); - } - } - - // Return results - res.json({ - success: true, - data: { - url, - scanId: scanDoc._key, - score: result.score, - violations: result.violations.length, - passes: result.passes.length, - incomplete: result.incomplete.length, - wcagLevel, - timestamp: result.timestamp, - details: { - violations: result.violations, - passes: result.passes, - incomplete: result.incomplete, - }, - }, - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/scan/:scanId - Get scan results by ID -scanRouter.get('/:scanId', async (req, res, next) => { - try { - const { scanId } = req.params; - - const scan = await db.scans.document(scanId); - if (!scan) { - return res.status(404).json({ - error: { - message: 'Scan not found', - status: 404, - }, - }); - } - - const violations = await db.getViolationsForScan(scanId); - - res.json({ - success: true, - data: { - scan, - violations, - }, - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/routes/stats.ts b/tools/monitoring-api/src/routes/stats.ts deleted file mode 100644 index 39a8d2f..0000000 --- a/tools/monitoring-api/src/routes/stats.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const statsRouter = Router(); - -// GET /v1/stats - Get global statistics -statsRouter.get('/', async (req, res, next) => { - try { - const sitesCount = await db.sites.count(); - const scansCount = await db.scans.count(); - const violationsCount = await db.violations.count(); - - const commonViolations = await db.getCommonViolations(5); - - res.json({ - success: true, - data: { - totalSites: sitesCount.count, - totalScans: scansCount.count, - totalViolations: violationsCount.count, - commonViolations, - timestamp: new Date().toISOString(), - }, - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/stats/site/:siteKey - Get stats for a specific site -statsRouter.get('/site/:siteKey', async (req, res, next) => { - try { - const { siteKey } = req.params; - - const site = await db.sites.document(siteKey); - const scans = await db.getRecentScansForSite(siteKey, 30); - const trend = await db.getSiteViolationTrend(siteKey, 30); - - res.json({ - success: true, - data: { - site, - recentScans: scans, - trend, - }, - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/routes/violations.ts b/tools/monitoring-api/src/routes/violations.ts deleted file mode 100644 index 0736039..0000000 --- a/tools/monitoring-api/src/routes/violations.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const violationsRouter = Router(); - -// POST /v1/violations - Report a violation (from browser extension) -violationsRouter.post('/', async (req, res, next) => { - try { - const { url, violation, timestamp, userAgent } = req.body; - - if (!url || !violation) { - return res.status(400).json({ - error: { - message: 'URL and violation are required', - status: 400, - }, - }); - } - - // Get or create site - let site = await db.getSiteByUrl(url); - let siteKey: string; - - if (!site) { - const urlObj = new URL(url); - const siteDoc = await db.sites.save({ - url, - domain: urlObj.hostname, - firstScanned: new Date(), - lastScanned: new Date(), - scanCount: 0, - currentScore: 0, - status: 'active', - } as any); - siteKey = siteDoc._key; - } else { - siteKey = site._key; - } - - // Store violation - await db.violations.save({ - siteKey, - scanKey: '', // No scan key for direct reports - wcagCriterion: violation.wcagCriterion || 'unknown', - wcagLevel: violation.wcagLevel || 'AA', - impact: violation.impact || 'moderate', - description: violation.description || '', - helpUrl: violation.helpUrl || '', - selector: violation.selector || '', - html: violation.html || '', - timestamp: new Date(timestamp || Date.now()), - fixed: false, - } as any); - - res.json({ - success: true, - message: 'Violation reported successfully', - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/violations/common - Get most common violations -violationsRouter.get('/common', async (req, res, next) => { - try { - const limit = parseInt(req.query.limit as string) || 10; - const violations = await db.getCommonViolations(limit); - - res.json({ - success: true, - data: violations, - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/violations/site/:siteKey - Get violations for a site -violationsRouter.get('/site/:siteKey', async (req, res, next) => { - try { - const { siteKey } = req.params; - const fixed = req.query.fixed === 'true'; - - const violations = await db.violations.byExample({ - siteKey, - fixed, - }).then(cursor => cursor.all()); - - res.json({ - success: true, - data: violations, - }); - } catch (error) { - next(error); - } -}); - -// PATCH /v1/violations/:violationId/fixed - Mark violation as fixed -violationsRouter.patch('/:violationId/fixed', async (req, res, next) => { - try { - const { violationId } = req.params; - - await db.violations.update(violationId, { - fixed: true, - }); - - res.json({ - success: true, - message: 'Violation marked as fixed', - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/server.ts b/tools/monitoring-api/src/server.ts deleted file mode 100644 index 3f3482d..0000000 --- a/tools/monitoring-api/src/server.ts +++ /dev/null @@ -1,127 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import compression from 'compression'; -import rateLimit from 'express-rate-limit'; -import { createArangoDBService } from '@accessibility-everywhere/core'; -import { createScanner } from '@accessibility-everywhere/scanner'; -import dotenv from 'dotenv'; - -// Load environment variables -dotenv.config(); - -// Import routes -import { scanRouter } from './routes/scan'; -import { violationsRouter } from './routes/violations'; -import { leaderboardRouter } from './routes/leaderboard'; -import { badgeRouter } from './routes/badge'; -import { statsRouter } from './routes/stats'; -import { dashboardRouter } from './routes/dashboard'; - -const app = express(); -const PORT = process.env.PORT || 3000; - -// Middleware -app.use(helmet()); -app.use(cors()); -app.use(compression()); -app.use(express.json({ limit: '10mb' })); - -// Rate limiting -const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per windowMs - message: 'Too many requests from this IP, please try again later.', -}); - -app.use('/v1/', limiter); - -// Initialize database -export const db = createArangoDBService(); -export const scanner = createScanner(); - -// Initialize database connection -async function initializeDatabase() { - try { - await db.initialize(); - console.log('āœ“ Database initialized successfully'); - } catch (error) { - console.error('āœ— Database initialization failed:', error); - process.exit(1); - } -} - -// Health check -app.get('/health', (req, res) => { - res.json({ - status: 'healthy', - timestamp: new Date().toISOString(), - version: '1.0.0', - }); -}); - -// API version info -app.get('/v1', (req, res) => { - res.json({ - name: 'Accessibility Everywhere Monitoring API', - version: '1.0.0', - description: 'Accessibility violation reporting and analytics API', - endpoints: { - scan: '/v1/scan', - violations: '/v1/violations', - leaderboard: '/v1/leaderboard', - badge: '/v1/badge/:domain', - stats: '/v1/stats', - dashboard: '/v1/dashboard/:orgId', - }, - documentation: 'https://docs.accessibility-everywhere.org/api', - }); -}); - -// Routes -app.use('/v1/scan', scanRouter); -app.use('/v1/violations', violationsRouter); -app.use('/v1/leaderboard', leaderboardRouter); -app.use('/v1/badge', badgeRouter); -app.use('/v1/stats', statsRouter); -app.use('/v1/dashboard', dashboardRouter); - -// Error handling -app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { - console.error('Error:', err); - res.status(err.status || 500).json({ - error: { - message: err.message || 'Internal server error', - status: err.status || 500, - }, - }); -}); - -// 404 handler -app.use((req, res) => { - res.status(404).json({ - error: { - message: 'Endpoint not found', - status: 404, - }, - }); -}); - -// Start server -async function startServer() { - await initializeDatabase(); - - app.listen(PORT, () => { - console.log(`āœ“ Accessibility Everywhere API listening on port ${PORT}`); - console.log(`āœ“ Environment: ${process.env.NODE_ENV || 'development'}`); - console.log(`āœ“ Health check: http://localhost:${PORT}/health`); - console.log(`āœ“ API docs: http://localhost:${PORT}/v1`); - }); -} - -startServer().catch(error => { - console.error('Failed to start server:', error); - process.exit(1); -}); - -export default app; diff --git a/tools/stale/packages/core/package-lock.json b/tools/stale/packages/core/package-lock.json new file mode 100644 index 0000000..a96f520 --- /dev/null +++ b/tools/stale/packages/core/package-lock.json @@ -0,0 +1,416 @@ +{ + "name": "@accessibility-everywhere/core", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@accessibility-everywhere/core", + "version": "1.0.0", + "dependencies": { + "arangojs": "^8.8.1", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@rescript/core": "^1.5.0", + "rescript": "^11.1.0" + } + }, + "node_modules/@rescript/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rescript/core/-/core-1.6.1.tgz", + "integrity": "sha512-vyb5k90ck+65Fgui+5vCja/mUfzKaK3kOPT4Z6aAJdHLH1eljEi1zKhXroCiCtpNLSWp8k4ulh1bdB5WS0hvqA==", + "dev": true, + "peerDependencies": { + "rescript": ">=11.1.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/arangojs": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/arangojs/-/arangojs-8.8.1.tgz", + "integrity": "sha512-gVc5BF91nT27lB97mt+XxcGbw7yOhPIkZ0f5Nmq/ZPt1/iP62rDpH961XUyWdzj5m4H8lx2OF/O2AVefZoolXg==", + "dependencies": { + "@types/node": ">=14", + "multi-part": "^4.0.0", + "path-browserify": "^1.0.1", + "x3-linkedlist": "1.2.0", + "xhr": "^2.4.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-kind": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mime-kind/-/mime-kind-4.0.0.tgz", + "integrity": "sha512-qQvglvSpS5mABi30beNFd+uHKtKkxD3dxAmhi2e589XKx+WfVqhg5i5P5LBcVgwwv3BiDpNMBWrHqU+JexW4aA==", + "dependencies": { + "file-type": "^16.5.4", + "mime-types": "^2.1.24" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/multi-part": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multi-part/-/multi-part-4.0.0.tgz", + "integrity": "sha512-YT/CS0PAe62kT8EoQXcQj8yIcSu18HhYv0s6ShdAFsoFly3oV5QaxODnkj0u7zH0/RFyH47cdcMVpcGXliEFVA==", + "dependencies": { + "mime-kind": "^4.0.0", + "multi-part-lite": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/multi-part-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/multi-part-lite/-/multi-part-lite-1.0.0.tgz", + "integrity": "sha512-KxIRbBZZ45hoKX1ROD/19wJr0ql1bef1rE8Y1PCwD3PuNXV42pp7Wo8lEHYuAajoT4vfAFcd3rPjlkyEEyt1nw==", + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/rescript": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/rescript/-/rescript-11.1.4.tgz", + "integrity": "sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "bsc": "bsc", + "bstracing": "lib/bstracing", + "rescript": "rescript" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==" + }, + "node_modules/x3-linkedlist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/x3-linkedlist/-/x3-linkedlist-1.2.0.tgz", + "integrity": "sha512-mH/YwxpYSKNa8bDNF1yOuZCMuV+K80LtDN8vcLDUAwNazCxptDNsYt+zA/EJeYiGbdtKposhKLZjErGVOR8mag==", + "engines": { + "node": ">=10" + } + }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/tools/stale/packages/core/package.json b/tools/stale/packages/core/package.json index 8c6d167..60d295f 100644 --- a/tools/stale/packages/core/package.json +++ b/tools/stale/packages/core/package.json @@ -2,21 +2,19 @@ "name": "@accessibility-everywhere/core", "version": "1.0.0", "description": "Core functionality for accessibility scanning and database operations", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "src/Index.mjs", + "type": "module", "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "test": "jest" + "build": "rescript build", + "dev": "rescript build -w", + "clean": "rescript clean" }, "dependencies": { "arangojs": "^8.8.1", "dotenv": "^16.3.1" }, "devDependencies": { - "@types/node": "^20.10.0", - "typescript": "^5.3.2", - "jest": "^29.7.0", - "@types/jest": "^29.5.8" + "rescript": "^11.1.0", + "@rescript/core": "^1.5.0" } } diff --git a/tools/stale/packages/core/rescript.json b/tools/stale/packages/core/rescript.json new file mode 100644 index 0000000..9a32abc --- /dev/null +++ b/tools/stale/packages/core/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "@accessibility-everywhere/core", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": { + "module": "esmodule", + "in-source": true + }, + "suffix": ".mjs", + "bs-dependencies": ["@rescript/core"], + "bsc-flags": ["-open RescriptCore"] +} diff --git a/tools/stale/packages/core/src/Index.mjs b/tools/stale/packages/core/src/Index.mjs new file mode 100644 index 0000000..42ddf86 --- /dev/null +++ b/tools/stale/packages/core/src/Index.mjs @@ -0,0 +1,40 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Arangodb from "./database/Arangodb.mjs"; + +var Arangodb$1; + +var make = Arangodb.make; + +var createArangoDBService = Arangodb.createArangoDBService; + +var initialize = Arangodb.initialize; + +var getSiteByUrl = Arangodb.getSiteByUrl; + +var getRecentScansForSite = Arangodb.getRecentScansForSite; + +var getViolationsForScan = Arangodb.getViolationsForScan; + +var getTopSites = Arangodb.getTopSites; + +var getCommonViolations = Arangodb.getCommonViolations; + +var getSiteViolationTrend = Arangodb.getSiteViolationTrend; + +var getOrganizationSites = Arangodb.getOrganizationSites; + +export { + Arangodb$1 as Arangodb, + make , + createArangoDBService , + initialize , + getSiteByUrl , + getRecentScansForSite , + getViolationsForScan , + getTopSites , + getCommonViolations , + getSiteViolationTrend , + getOrganizationSites , +} +/* Arangodb Not a pure module */ diff --git a/tools/stale/packages/core/src/Index.res b/tools/stale/packages/core/src/Index.res new file mode 100644 index 0000000..0f7a44c --- /dev/null +++ b/tools/stale/packages/core/src/Index.res @@ -0,0 +1,12 @@ +module Arangodb = Arangodb + +let make = Arangodb.make +let createArangoDBService = Arangodb.createArangoDBService +let initialize = Arangodb.initialize +let getSiteByUrl = Arangodb.getSiteByUrl +let getRecentScansForSite = Arangodb.getRecentScansForSite +let getViolationsForScan = Arangodb.getViolationsForScan +let getTopSites = Arangodb.getTopSites +let getCommonViolations = Arangodb.getCommonViolations +let getSiteViolationTrend = Arangodb.getSiteViolationTrend +let getOrganizationSites = Arangodb.getOrganizationSites diff --git a/tools/stale/packages/core/src/database/Arangodb.mjs b/tools/stale/packages/core/src/database/Arangodb.mjs new file mode 100644 index 0000000..e34d125 --- /dev/null +++ b/tools/stale/packages/core/src/database/Arangodb.mjs @@ -0,0 +1,467 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Arangojs from "arangojs"; + +var Arango = {}; + +function make(config) { + var db = new Arangojs.Database({ + url: config.url, + databaseName: config.database, + auth: { + username: config.username, + password: config.password + } + }); + return { + db: db, + sites: undefined, + scans: undefined, + violations: undefined, + wcagCriteria: undefined, + organizations: undefined, + siteScans: undefined, + scanViolations: undefined, + violationCriteria: undefined, + orgSites: undefined + }; +} + +function getWCAGCriteriaData() { + return [ + { + _key: "1_1_1", + criterion: "1.1.1", + level: "A", + principle: "perceivable", + guideline: "1.1", + title: "Non-text Content", + description: "All non-text content has a text alternative", + successCriteria: "Provide text alternatives for any non-text content", + techniques: [ + "H37", + "H36", + "G94", + "G95" + ], + failures: [ + "F3", + "F13", + "F20", + "F30", + "F38", + "F39", + "F65", + "F67", + "F71", + "F72" + ] + }, + { + _key: "1_3_1", + criterion: "1.3.1", + level: "A", + principle: "perceivable", + guideline: "1.3", + title: "Info and Relationships", + description: "Information, structure, and relationships can be programmatically determined", + successCriteria: "Information, structure, and relationships conveyed through presentation can be programmatically determined", + techniques: [ + "H42", + "H43", + "H44", + "H48", + "H51", + "H63", + "H71", + "H73", + "H85" + ], + failures: [ + "F2", + "F33", + "F34", + "F42", + "F43", + "F46", + "F48", + "F68", + "F87", + "F90", + "F91", + "F92" + ] + }, + { + _key: "1_4_3", + criterion: "1.4.3", + level: "AA", + principle: "perceivable", + guideline: "1.4", + title: "Contrast (Minimum)", + description: "Text has a contrast ratio of at least 4.5:1", + successCriteria: "The visual presentation of text and images of text has a contrast ratio of at least 4.5:1", + techniques: [ + "G17", + "G18", + "G145", + "G148", + "G174" + ], + failures: [ + "F24", + "F83" + ] + }, + { + _key: "2_1_1", + criterion: "2.1.1", + level: "A", + principle: "operable", + guideline: "2.1", + title: "Keyboard", + description: "All functionality is available from a keyboard", + successCriteria: "All functionality of the content is operable through a keyboard interface", + techniques: [ + "G202", + "H91" + ], + failures: [ + "F54", + "F55", + "F42" + ] + }, + { + _key: "2_4_1", + criterion: "2.4.1", + level: "A", + principle: "operable", + guideline: "2.4", + title: "Bypass Blocks", + description: "A mechanism is available to bypass blocks of content", + successCriteria: "A mechanism is available to bypass blocks of content that are repeated on multiple Web pages", + techniques: [ + "G1", + "G123", + "G124", + "H69", + "H70" + ], + failures: ["F"] + }, + { + _key: "2_4_2", + criterion: "2.4.2", + level: "A", + principle: "operable", + guideline: "2.4", + title: "Page Titled", + description: "Web pages have titles that describe topic or purpose", + successCriteria: "Web pages have titles that describe topic or purpose", + techniques: [ + "G88", + "H25" + ], + failures: ["F25"] + }, + { + _key: "3_1_1", + criterion: "3.1.1", + level: "A", + principle: "understandable", + guideline: "3.1", + title: "Language of Page", + description: "The default human language can be programmatically determined", + successCriteria: "The default human language of each Web page can be programmatically determined", + techniques: ["H57"], + failures: [] + }, + { + _key: "3_2_3", + criterion: "3.2.3", + level: "AA", + principle: "understandable", + guideline: "3.2", + title: "Consistent Navigation", + description: "Navigation mechanisms are consistent", + successCriteria: "Navigational mechanisms that are repeated on multiple Web pages occur in the same relative order", + techniques: ["G61"], + failures: ["F66"] + }, + { + _key: "4_1_1", + criterion: "4.1.1", + level: "A", + principle: "robust", + guideline: "4.1", + title: "Parsing", + description: "Content can be parsed by user agents", + successCriteria: "Elements have complete start and end tags, are nested according to specifications", + techniques: [ + "G134", + "G192", + "H88", + "H74", + "H93", + "H94" + ], + failures: [ + "F70", + "F77" + ] + }, + { + _key: "4_1_2", + criterion: "4.1.2", + level: "A", + principle: "robust", + guideline: "4.1", + title: "Name, Role, Value", + description: "User interface components have programmatically determined name, role, and value", + successCriteria: "For all user interface components, the name and role can be programmatically determined", + techniques: [ + "G108", + "H91", + "H44", + "H64", + "H65", + "H88" + ], + failures: [ + "F15", + "F20", + "F59", + "F68", + "F79", + "F86", + "F89" + ] + } + ]; +} + +async function createCollectionIfNotExists(db, name) { + var collections = await db.listCollections(); + if (!collections.some(function (c) { + return c.name === name; + })) { + await db.createCollection(name); + return ; + } + +} + +async function createEdgeCollectionIfNotExists(db, name) { + var collections = await db.listCollections(); + if (!collections.some(function (c) { + return c.name === name; + })) { + await db.createEdgeCollection(name); + return ; + } + +} + +async function createIndexes(svc) { + await svc.sites.ensureIndex({ + type: "persistent", + fields: ["url"], + unique: true + }); + await svc.sites.ensureIndex({ + type: "persistent", + fields: ["domain"] + }); + await svc.sites.ensureIndex({ + type: "persistent", + fields: ["currentScore"] + }); + await svc.scans.ensureIndex({ + type: "persistent", + fields: ["siteKey"] + }); + await svc.scans.ensureIndex({ + type: "persistent", + fields: ["timestamp"] + }); + await svc.scans.ensureIndex({ + type: "persistent", + fields: ["score"] + }); + await svc.violations.ensureIndex({ + type: "persistent", + fields: ["scanKey"] + }); + await svc.violations.ensureIndex({ + type: "persistent", + fields: ["siteKey"] + }); + await svc.violations.ensureIndex({ + type: "persistent", + fields: ["wcagCriterion"] + }); + await svc.violations.ensureIndex({ + type: "persistent", + fields: ["impact"] + }); + await svc.violations.ensureIndex({ + type: "persistent", + fields: ["fixed"] + }); + await svc.wcagCriteria.ensureIndex({ + type: "persistent", + fields: ["criterion"], + unique: true + }); + await svc.wcagCriteria.ensureIndex({ + type: "persistent", + fields: ["level"] + }); + await svc.organizations.ensureIndex({ + type: "persistent", + fields: ["domain"] + }); + return await svc.organizations.ensureIndex({ + type: "persistent", + fields: ["apiKey"], + unique: true, + sparse: true + }); +} + +async function initializeWCAGCriteria(svc) { + var countResult = await svc.wcagCriteria.count(); + if (countResult.count !== 0) { + return ; + } + var criteria = getWCAGCriteriaData(); + for(var i = 0 ,i_finish = criteria.length; i < i_finish; ++i){ + var c = criteria[i]; + await svc.wcagCriteria.save(c); + } +} + +async function initialize(svc) { + var databases = await svc.db.listDatabases(); + var dbName = svc.db.name; + if (!databases.includes(dbName)) { + await svc.db.createDatabase(dbName); + } + await createCollectionIfNotExists(svc.db, "sites"); + await createCollectionIfNotExists(svc.db, "scans"); + await createCollectionIfNotExists(svc.db, "violations"); + await createCollectionIfNotExists(svc.db, "wcag_criteria"); + await createCollectionIfNotExists(svc.db, "organizations"); + await createEdgeCollectionIfNotExists(svc.db, "site_scans"); + await createEdgeCollectionIfNotExists(svc.db, "scan_violations"); + await createEdgeCollectionIfNotExists(svc.db, "violation_criteria"); + await createEdgeCollectionIfNotExists(svc.db, "org_sites"); + svc.sites = svc.db.collection("sites"); + svc.scans = svc.db.collection("scans"); + svc.violations = svc.db.collection("violations"); + svc.wcagCriteria = svc.db.collection("wcag_criteria"); + svc.organizations = svc.db.collection("organizations"); + svc.siteScans = svc.db.collection("site_scans"); + svc.scanViolations = svc.db.collection("scan_violations"); + svc.violationCriteria = svc.db.collection("violation_criteria"); + svc.orgSites = svc.db.collection("org_sites"); + await createIndexes(svc); + return await initializeWCAGCriteria(svc); +} + +async function getSiteByUrl(svc, url) { + var cursor = await svc.db.query("FOR site IN sites FILTER site.url == @url LIMIT 1 RETURN site", { + url: url + }); + var results = await cursor.all(); + if (results.length > 0) { + return results[0]; + } + +} + +async function getRecentScansForSite(svc, siteKey, limitOpt) { + var limit = limitOpt !== undefined ? limitOpt : 10; + var cursor = await svc.db.query("FOR scan IN scans FILTER scan.siteKey == @siteKey SORT scan.timestamp DESC LIMIT @limit RETURN scan", { + siteKey: siteKey, + limit: limit + }); + return await cursor.all(); +} + +async function getViolationsForScan(svc, scanKey) { + var cursor = await svc.db.query("FOR violation IN violations FILTER violation.scanKey == @scanKey SORT violation.impact DESC RETURN violation", { + scanKey: scanKey + }); + return await cursor.all(); +} + +async function getTopSites(svc, limitOpt) { + var limit = limitOpt !== undefined ? limitOpt : 100; + var cursor = await svc.db.query("FOR site IN sites FILTER site.currentScore > 0 SORT site.currentScore DESC LIMIT @limit RETURN site", { + limit: limit + }); + return await cursor.all(); +} + +async function getCommonViolations(svc, limitOpt) { + var limit = limitOpt !== undefined ? limitOpt : 10; + var cursor = await svc.db.query("FOR violation IN violations COLLECT wcagCriterion = violation.wcagCriterion WITH COUNT INTO count SORT count DESC LIMIT @limit RETURN {criterion: wcagCriterion, count: count}", { + limit: limit + }); + return await cursor.all(); +} + +async function getSiteViolationTrend(svc, siteKey, daysOpt) { + var days = daysOpt !== undefined ? daysOpt : 30; + var startDate = new Date(); + startDate.setDate(startDate.getDate() - days | 0); + var cursor = await svc.db.query("FOR scan IN scans FILTER scan.siteKey == @siteKey AND scan.timestamp >= @startDate SORT scan.timestamp ASC RETURN {timestamp: scan.timestamp, violations: scan.violations, score: scan.score}", { + siteKey: siteKey, + startDate: startDate + }); + return await cursor.all(); +} + +async function getOrganizationSites(svc, orgKey) { + var cursor = await svc.db.query("FOR org IN organizations FILTER org._key == @orgKey FOR v, e IN 1..1 OUTBOUND org org_sites RETURN v", { + orgKey: orgKey + }); + return await cursor.all(); +} + +function createArangoDBService(config) { + var defaults_url = (process.env.ARANGO_URL || "http://localhost:8529"); + var defaults_database = (process.env.ARANGO_DATABASE || "accessibility"); + var defaults_username = (process.env.ARANGO_USERNAME || "root"); + var defaults_password = (process.env.ARANGO_PASSWORD || "development"); + var defaults = { + url: defaults_url, + database: defaults_database, + username: defaults_username, + password: defaults_password + }; + var merged = config !== undefined ? config : defaults; + return make(merged); +} + +export { + Arango , + make , + getWCAGCriteriaData , + createCollectionIfNotExists , + createEdgeCollectionIfNotExists , + createIndexes , + initializeWCAGCriteria , + initialize , + getSiteByUrl , + getRecentScansForSite , + getViolationsForScan , + getTopSites , + getCommonViolations , + getSiteViolationTrend , + getOrganizationSites , + createArangoDBService , +} +/* arangojs Not a pure module */ diff --git a/tools/stale/packages/core/src/database/Arangodb.res b/tools/stale/packages/core/src/database/Arangodb.res new file mode 100644 index 0000000..24012ce --- /dev/null +++ b/tools/stale/packages/core/src/database/Arangodb.res @@ -0,0 +1,446 @@ +type wcagLevel = [#A | #"AA" | #"AAA"] +type impact = [#critical | #serious | #moderate | #minor] +type principle = [#perceivable | #operable | #understandable | #robust] +type siteStatus = [#active | #inactive | #failed] +type orgTier = [#free | #pro | #enterprise] + +type arangoConfig = { + url: string, + database: string, + username: string, + password: string, +} + +type site = { + _key: string, + url: string, + domain: string, + firstScanned: Date.t, + lastScanned: Date.t, + scanCount: int, + currentScore: int, + previousScore?: int, + status: siteStatus, + metadata?: Dict.t, +} + +type scan = { + _key: string, + siteKey: string, + timestamp: Date.t, + score: int, + violations: int, + passes: int, + incomplete: int, + url: string, + wcagLevel: wcagLevel, + duration: int, + userAgent?: string, +} + +type violation = { + _key: string, + scanKey: string, + siteKey: string, + wcagCriterion: string, + wcagLevel: wcagLevel, + impact: impact, + description: string, + helpUrl: string, + selector: string, + html: string, + timestamp: Date.t, + fixed: bool, +} + +type wcagCriterion = { + _key: string, + criterion: string, + level: wcagLevel, + principle: principle, + guideline: string, + title: string, + description: string, + successCriteria: string, + techniques: array, + failures: array, +} + +type organization = { + _key: string, + name: string, + domain: string, + contactEmail?: string, + tier: orgTier, + createdAt: Date.t, + apiKey?: string, +} + +module Arango = { + type database + type collection + type cursor + + type collectionInfo = {name: string} + type indexSpec = { + \"type": string, + fields: array, + unique?: bool, + sparse?: bool, + } + type databaseInit = { + url: string, + databaseName: string, + auth: {"username": string, "password": string}, + } + + @module("arangojs") @new external make: databaseInit => database = "Database" + + @get external name: database => string = "name" + @send external listDatabases: database => promise> = "listDatabases" + @send external createDatabase: (database, string) => promise = "createDatabase" + @send external listCollections: database => promise> = "listCollections" + @send external createCollection: (database, string) => promise = "createCollection" + @send external createEdgeCollection: (database, string) => promise = "createEdgeCollection" + @send external collection: (database, string) => collection = "collection" + @send external query: (database, string, 'bind) => promise = "query" + @send external queryNoBind: (database, string) => promise = "query" + + @send external ensureIndex: (collection, indexSpec) => promise = "ensureIndex" + @send external save: (collection, 'a) => promise = "save" + @send external count: collection => promise<{"count": int}> = "count" + + @send external all: cursor => promise> = "all" +} + +type service = { + db: Arango.database, + mutable sites: Arango.collection, + mutable scans: Arango.collection, + mutable violations: Arango.collection, + mutable wcagCriteria: Arango.collection, + mutable organizations: Arango.collection, + mutable siteScans: Arango.collection, + mutable scanViolations: Arango.collection, + mutable violationCriteria: Arango.collection, + mutable orgSites: Arango.collection, +} + +@val external uninitialised: 'a = "undefined" + +let make = (config: arangoConfig): service => { + let db = Arango.make({ + url: config.url, + databaseName: config.database, + auth: {"username": config.username, "password": config.password}, + }) + { + db, + sites: uninitialised, + scans: uninitialised, + violations: uninitialised, + wcagCriteria: uninitialised, + organizations: uninitialised, + siteScans: uninitialised, + scanViolations: uninitialised, + violationCriteria: uninitialised, + orgSites: uninitialised, + } +} + +let getWCAGCriteriaData = (): array => [ + { + _key: "1_1_1", + criterion: "1.1.1", + level: #A, + principle: #perceivable, + guideline: "1.1", + title: "Non-text Content", + description: "All non-text content has a text alternative", + successCriteria: "Provide text alternatives for any non-text content", + techniques: ["H37", "H36", "G94", "G95"], + failures: ["F3", "F13", "F20", "F30", "F38", "F39", "F65", "F67", "F71", "F72"], + }, + { + _key: "1_3_1", + criterion: "1.3.1", + level: #A, + principle: #perceivable, + guideline: "1.3", + title: "Info and Relationships", + description: "Information, structure, and relationships can be programmatically determined", + successCriteria: "Information, structure, and relationships conveyed through presentation can be programmatically determined", + techniques: ["H42", "H43", "H44", "H48", "H51", "H63", "H71", "H73", "H85"], + failures: ["F2", "F33", "F34", "F42", "F43", "F46", "F48", "F68", "F87", "F90", "F91", "F92"], + }, + { + _key: "1_4_3", + criterion: "1.4.3", + level: #"AA", + principle: #perceivable, + guideline: "1.4", + title: "Contrast (Minimum)", + description: "Text has a contrast ratio of at least 4.5:1", + successCriteria: "The visual presentation of text and images of text has a contrast ratio of at least 4.5:1", + techniques: ["G17", "G18", "G145", "G148", "G174"], + failures: ["F24", "F83"], + }, + { + _key: "2_1_1", + criterion: "2.1.1", + level: #A, + principle: #operable, + guideline: "2.1", + title: "Keyboard", + description: "All functionality is available from a keyboard", + successCriteria: "All functionality of the content is operable through a keyboard interface", + techniques: ["G202", "H91"], + failures: ["F54", "F55", "F42"], + }, + { + _key: "2_4_1", + criterion: "2.4.1", + level: #A, + principle: #operable, + guideline: "2.4", + title: "Bypass Blocks", + description: "A mechanism is available to bypass blocks of content", + successCriteria: "A mechanism is available to bypass blocks of content that are repeated on multiple Web pages", + techniques: ["G1", "G123", "G124", "H69", "H70"], + failures: ["F"], + }, + { + _key: "2_4_2", + criterion: "2.4.2", + level: #A, + principle: #operable, + guideline: "2.4", + title: "Page Titled", + description: "Web pages have titles that describe topic or purpose", + successCriteria: "Web pages have titles that describe topic or purpose", + techniques: ["G88", "H25"], + failures: ["F25"], + }, + { + _key: "3_1_1", + criterion: "3.1.1", + level: #A, + principle: #understandable, + guideline: "3.1", + title: "Language of Page", + description: "The default human language can be programmatically determined", + successCriteria: "The default human language of each Web page can be programmatically determined", + techniques: ["H57"], + failures: [], + }, + { + _key: "3_2_3", + criterion: "3.2.3", + level: #"AA", + principle: #understandable, + guideline: "3.2", + title: "Consistent Navigation", + description: "Navigation mechanisms are consistent", + successCriteria: "Navigational mechanisms that are repeated on multiple Web pages occur in the same relative order", + techniques: ["G61"], + failures: ["F66"], + }, + { + _key: "4_1_1", + criterion: "4.1.1", + level: #A, + principle: #robust, + guideline: "4.1", + title: "Parsing", + description: "Content can be parsed by user agents", + successCriteria: "Elements have complete start and end tags, are nested according to specifications", + techniques: ["G134", "G192", "H88", "H74", "H93", "H94"], + failures: ["F70", "F77"], + }, + { + _key: "4_1_2", + criterion: "4.1.2", + level: #A, + principle: #robust, + guideline: "4.1", + title: "Name, Role, Value", + description: "User interface components have programmatically determined name, role, and value", + successCriteria: "For all user interface components, the name and role can be programmatically determined", + techniques: ["G108", "H91", "H44", "H64", "H65", "H88"], + failures: ["F15", "F20", "F59", "F68", "F79", "F86", "F89"], + }, +] + +let createCollectionIfNotExists = async (db: Arango.database, name: string) => { + let collections = await Arango.listCollections(db) + if !(collections->Array.some(c => c.name === name)) { + let _ = await Arango.createCollection(db, name) + } +} + +let createEdgeCollectionIfNotExists = async (db: Arango.database, name: string) => { + let collections = await Arango.listCollections(db) + if !(collections->Array.some(c => c.name === name)) { + let _ = await Arango.createEdgeCollection(db, name) + } +} + +let createIndexes = async (svc: service) => { + await Arango.ensureIndex(svc.sites, {\"type": "persistent", fields: ["url"], unique: true}) + await Arango.ensureIndex(svc.sites, {\"type": "persistent", fields: ["domain"]}) + await Arango.ensureIndex(svc.sites, {\"type": "persistent", fields: ["currentScore"]}) + + await Arango.ensureIndex(svc.scans, {\"type": "persistent", fields: ["siteKey"]}) + await Arango.ensureIndex(svc.scans, {\"type": "persistent", fields: ["timestamp"]}) + await Arango.ensureIndex(svc.scans, {\"type": "persistent", fields: ["score"]}) + + await Arango.ensureIndex(svc.violations, {\"type": "persistent", fields: ["scanKey"]}) + await Arango.ensureIndex(svc.violations, {\"type": "persistent", fields: ["siteKey"]}) + await Arango.ensureIndex(svc.violations, {\"type": "persistent", fields: ["wcagCriterion"]}) + await Arango.ensureIndex(svc.violations, {\"type": "persistent", fields: ["impact"]}) + await Arango.ensureIndex(svc.violations, {\"type": "persistent", fields: ["fixed"]}) + + await Arango.ensureIndex(svc.wcagCriteria, { + \"type": "persistent", + fields: ["criterion"], + unique: true, + }) + await Arango.ensureIndex(svc.wcagCriteria, {\"type": "persistent", fields: ["level"]}) + + await Arango.ensureIndex(svc.organizations, {\"type": "persistent", fields: ["domain"]}) + await Arango.ensureIndex(svc.organizations, { + \"type": "persistent", + fields: ["apiKey"], + unique: true, + sparse: true, + }) +} + +let initializeWCAGCriteria = async (svc: service) => { + let countResult = await Arango.count(svc.wcagCriteria) + if countResult["count"] === 0 { + let criteria = getWCAGCriteriaData() + for i in 0 to Array.length(criteria) - 1 { + let c = criteria->Array.getUnsafe(i) + await Arango.save(svc.wcagCriteria, c) + } + } +} + +let initialize = async (svc: service) => { + let databases = await Arango.listDatabases(svc.db) + let dbName = Arango.name(svc.db) + if !(databases->Array.includes(dbName)) { + let _ = await Arango.createDatabase(svc.db, dbName) + } + + await createCollectionIfNotExists(svc.db, "sites") + await createCollectionIfNotExists(svc.db, "scans") + await createCollectionIfNotExists(svc.db, "violations") + await createCollectionIfNotExists(svc.db, "wcag_criteria") + await createCollectionIfNotExists(svc.db, "organizations") + + await createEdgeCollectionIfNotExists(svc.db, "site_scans") + await createEdgeCollectionIfNotExists(svc.db, "scan_violations") + await createEdgeCollectionIfNotExists(svc.db, "violation_criteria") + await createEdgeCollectionIfNotExists(svc.db, "org_sites") + + svc.sites = Arango.collection(svc.db, "sites") + svc.scans = Arango.collection(svc.db, "scans") + svc.violations = Arango.collection(svc.db, "violations") + svc.wcagCriteria = Arango.collection(svc.db, "wcag_criteria") + svc.organizations = Arango.collection(svc.db, "organizations") + svc.siteScans = Arango.collection(svc.db, "site_scans") + svc.scanViolations = Arango.collection(svc.db, "scan_violations") + svc.violationCriteria = Arango.collection(svc.db, "violation_criteria") + svc.orgSites = Arango.collection(svc.db, "org_sites") + + await createIndexes(svc) + await initializeWCAGCriteria(svc) +} + +let getSiteByUrl = async (svc: service, url: string): option => { + let cursor = await Arango.query( + svc.db, + "FOR site IN sites FILTER site.url == @url LIMIT 1 RETURN site", + {"url": url}, + ) + let results = await Arango.all(cursor) + Array.length(results) > 0 ? Some(results->Array.getUnsafe(0)) : None +} + +let getRecentScansForSite = async (svc: service, siteKey: string, ~limit=10): array => { + let cursor = await Arango.query( + svc.db, + "FOR scan IN scans FILTER scan.siteKey == @siteKey SORT scan.timestamp DESC LIMIT @limit RETURN scan", + {"siteKey": siteKey, "limit": limit}, + ) + await Arango.all(cursor) +} + +let getViolationsForScan = async (svc: service, scanKey: string): array => { + let cursor = await Arango.query( + svc.db, + "FOR violation IN violations FILTER violation.scanKey == @scanKey SORT violation.impact DESC RETURN violation", + {"scanKey": scanKey}, + ) + await Arango.all(cursor) +} + +let getTopSites = async (svc: service, ~limit=100): array => { + let cursor = await Arango.query( + svc.db, + "FOR site IN sites FILTER site.currentScore > 0 SORT site.currentScore DESC LIMIT @limit RETURN site", + {"limit": limit}, + ) + await Arango.all(cursor) +} + +type criterionCount = {criterion: string, count: int} + +let getCommonViolations = async (svc: service, ~limit=10): array => { + let cursor = await Arango.query( + svc.db, + "FOR violation IN violations COLLECT wcagCriterion = violation.wcagCriterion WITH COUNT INTO count SORT count DESC LIMIT @limit RETURN {criterion: wcagCriterion, count: count}", + {"limit": limit}, + ) + await Arango.all(cursor) +} + +type trendPoint = {timestamp: Date.t, violations: int, score: int} + +let getSiteViolationTrend = async (svc: service, siteKey: string, ~days=30): array => { + let startDate = Date.make() + startDate->Date.setDate(Date.getDate(startDate) - days) + let cursor = await Arango.query( + svc.db, + "FOR scan IN scans FILTER scan.siteKey == @siteKey AND scan.timestamp >= @startDate SORT scan.timestamp ASC RETURN {timestamp: scan.timestamp, violations: scan.violations, score: scan.score}", + {"siteKey": siteKey, "startDate": startDate}, + ) + await Arango.all(cursor) +} + +let getOrganizationSites = async (svc: service, orgKey: string): array => { + let cursor = await Arango.query( + svc.db, + "FOR org IN organizations FILTER org._key == @orgKey FOR v, e IN 1..1 OUTBOUND org org_sites RETURN v", + {"orgKey": orgKey}, + ) + await Arango.all(cursor) +} + +@val external envGet: string => option = "process.env" + +let createArangoDBService = (~config: option=?) => { + let defaults: arangoConfig = { + url: %raw(`process.env.ARANGO_URL || "http://localhost:8529"`), + database: %raw(`process.env.ARANGO_DATABASE || "accessibility"`), + username: %raw(`process.env.ARANGO_USERNAME || "root"`), + password: %raw(`process.env.ARANGO_PASSWORD || "development"`), + } + let merged = switch config { + | Some(c) => c + | None => defaults + } + make(merged) +} diff --git a/tools/stale/packages/core/src/database/arangodb.ts b/tools/stale/packages/core/src/database/arangodb.ts deleted file mode 100644 index 383a9ed..0000000 --- a/tools/stale/packages/core/src/database/arangodb.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { Database, aql } from 'arangojs'; -import { DocumentCollection, EdgeCollection } from 'arangojs/collection'; - -export interface ArangoConfig { - url: string; - database: string; - username: string; - password: string; -} - -export interface Site { - _key: string; - url: string; - domain: string; - firstScanned: Date; - lastScanned: Date; - scanCount: number; - currentScore: number; - previousScore?: number; - status: 'active' | 'inactive' | 'failed'; - metadata?: Record; -} - -export interface Scan { - _key: string; - siteKey: string; - timestamp: Date; - score: number; - violations: number; - passes: number; - incomplete: number; - url: string; - wcagLevel: 'A' | 'AA' | 'AAA'; - duration: number; - userAgent?: string; -} - -export interface Violation { - _key: string; - scanKey: string; - siteKey: string; - wcagCriterion: string; - wcagLevel: 'A' | 'AA' | 'AAA'; - impact: 'critical' | 'serious' | 'moderate' | 'minor'; - description: string; - helpUrl: string; - selector: string; - html: string; - timestamp: Date; - fixed: boolean; -} - -export interface WCAGCriterion { - _key: string; - criterion: string; - level: 'A' | 'AA' | 'AAA'; - principle: 'perceivable' | 'operable' | 'understandable' | 'robust'; - guideline: string; - title: string; - description: string; - successCriteria: string; - techniques: string[]; - failures: string[]; -} - -export interface Organization { - _key: string; - name: string; - domain: string; - contactEmail?: string; - tier: 'free' | 'pro' | 'enterprise'; - createdAt: Date; - apiKey?: string; -} - -export class ArangoDBService { - private db: Database; - public sites!: DocumentCollection; - public scans!: DocumentCollection; - public violations!: DocumentCollection; - public wcagCriteria!: DocumentCollection; - public organizations!: DocumentCollection; - public siteScans!: EdgeCollection; - public scanViolations!: EdgeCollection; - public violationCriteria!: EdgeCollection; - public orgSites!: EdgeCollection; - - constructor(config: ArangoConfig) { - this.db = new Database({ - url: config.url, - databaseName: config.database, - auth: { - username: config.username, - password: config.password, - }, - }); - } - - async initialize(): Promise { - // Create database if it doesn't exist - const databases = await this.db.listDatabases(); - if (!databases.includes(this.db.name)) { - await this.db.createDatabase(this.db.name); - } - - // Create collections - await this.createCollectionIfNotExists('sites'); - await this.createCollectionIfNotExists('scans'); - await this.createCollectionIfNotExists('violations'); - await this.createCollectionIfNotExists('wcag_criteria'); - await this.createCollectionIfNotExists('organizations'); - - // Create edge collections for graph relationships - await this.createEdgeCollectionIfNotExists('site_scans'); - await this.createEdgeCollectionIfNotExists('scan_violations'); - await this.createEdgeCollectionIfNotExists('violation_criteria'); - await this.createEdgeCollectionIfNotExists('org_sites'); - - // Assign collections - this.sites = this.db.collection('sites'); - this.scans = this.db.collection('scans'); - this.violations = this.db.collection('violations'); - this.wcagCriteria = this.db.collection('wcag_criteria'); - this.organizations = this.db.collection('organizations'); - this.siteScans = this.db.collection('site_scans'); - this.scanViolations = this.db.collection('scan_violations'); - this.violationCriteria = this.db.collection('violation_criteria'); - this.orgSites = this.db.collection('org_sites'); - - // Create indexes - await this.createIndexes(); - - // Initialize WCAG criteria - await this.initializeWCAGCriteria(); - } - - private async createCollectionIfNotExists(name: string): Promise { - const collections = await this.db.listCollections(); - if (!collections.some(c => c.name === name)) { - await this.db.createCollection(name); - } - } - - private async createEdgeCollectionIfNotExists(name: string): Promise { - const collections = await this.db.listCollections(); - if (!collections.some(c => c.name === name)) { - await this.db.createEdgeCollection(name); - } - } - - private async createIndexes(): Promise { - // Sites indexes - await this.sites.ensureIndex({ type: 'persistent', fields: ['url'], unique: true }); - await this.sites.ensureIndex({ type: 'persistent', fields: ['domain'] }); - await this.sites.ensureIndex({ type: 'persistent', fields: ['currentScore'] }); - - // Scans indexes - await this.scans.ensureIndex({ type: 'persistent', fields: ['siteKey'] }); - await this.scans.ensureIndex({ type: 'persistent', fields: ['timestamp'] }); - await this.scans.ensureIndex({ type: 'persistent', fields: ['score'] }); - - // Violations indexes - await this.violations.ensureIndex({ type: 'persistent', fields: ['scanKey'] }); - await this.violations.ensureIndex({ type: 'persistent', fields: ['siteKey'] }); - await this.violations.ensureIndex({ type: 'persistent', fields: ['wcagCriterion'] }); - await this.violations.ensureIndex({ type: 'persistent', fields: ['impact'] }); - await this.violations.ensureIndex({ type: 'persistent', fields: ['fixed'] }); - - // WCAG Criteria indexes - await this.wcagCriteria.ensureIndex({ type: 'persistent', fields: ['criterion'], unique: true }); - await this.wcagCriteria.ensureIndex({ type: 'persistent', fields: ['level'] }); - - // Organizations indexes - await this.organizations.ensureIndex({ type: 'persistent', fields: ['domain'] }); - await this.organizations.ensureIndex({ type: 'persistent', fields: ['apiKey'], unique: true, sparse: true }); - } - - private async initializeWCAGCriteria(): Promise { - const count = await this.wcagCriteria.count(); - if (count.count === 0) { - // Insert WCAG 2.1 Level AA criteria - const criteria = this.getWCAGCriteriaData(); - for (const criterion of criteria) { - await this.wcagCriteria.save(criterion); - } - } - } - - private getWCAGCriteriaData(): WCAGCriterion[] { - return [ - { - _key: '1_1_1', - criterion: '1.1.1', - level: 'A', - principle: 'perceivable', - guideline: '1.1', - title: 'Non-text Content', - description: 'All non-text content has a text alternative', - successCriteria: 'Provide text alternatives for any non-text content', - techniques: ['H37', 'H36', 'G94', 'G95'], - failures: ['F3', 'F13', 'F20', 'F30', 'F38', 'F39', 'F65', 'F67', 'F71', 'F72'], - }, - { - _key: '1_3_1', - criterion: '1.3.1', - level: 'A', - principle: 'perceivable', - guideline: '1.3', - title: 'Info and Relationships', - description: 'Information, structure, and relationships can be programmatically determined', - successCriteria: 'Information, structure, and relationships conveyed through presentation can be programmatically determined', - techniques: ['H42', 'H43', 'H44', 'H48', 'H51', 'H63', 'H71', 'H73', 'H85'], - failures: ['F2', 'F33', 'F34', 'F42', 'F43', 'F46', 'F48', 'F68', 'F87', 'F90', 'F91', 'F92'], - }, - { - _key: '1_4_3', - criterion: '1.4.3', - level: 'AA', - principle: 'perceivable', - guideline: '1.4', - title: 'Contrast (Minimum)', - description: 'Text has a contrast ratio of at least 4.5:1', - successCriteria: 'The visual presentation of text and images of text has a contrast ratio of at least 4.5:1', - techniques: ['G17', 'G18', 'G145', 'G148', 'G174'], - failures: ['F24', 'F83'], - }, - { - _key: '2_1_1', - criterion: '2.1.1', - level: 'A', - principle: 'operable', - guideline: '2.1', - title: 'Keyboard', - description: 'All functionality is available from a keyboard', - successCriteria: 'All functionality of the content is operable through a keyboard interface', - techniques: ['G202', 'H91'], - failures: ['F54', 'F55', 'F42'], - }, - { - _key: '2_4_1', - criterion: '2.4.1', - level: 'A', - principle: 'operable', - guideline: '2.4', - title: 'Bypass Blocks', - description: 'A mechanism is available to bypass blocks of content', - successCriteria: 'A mechanism is available to bypass blocks of content that are repeated on multiple Web pages', - techniques: ['G1', 'G123', 'G124', 'H69', 'H70'], - failures: ['F'], - }, - { - _key: '2_4_2', - criterion: '2.4.2', - level: 'A', - principle: 'operable', - guideline: '2.4', - title: 'Page Titled', - description: 'Web pages have titles that describe topic or purpose', - successCriteria: 'Web pages have titles that describe topic or purpose', - techniques: ['G88', 'H25'], - failures: ['F25'], - }, - { - _key: '3_1_1', - criterion: '3.1.1', - level: 'A', - principle: 'understandable', - guideline: '3.1', - title: 'Language of Page', - description: 'The default human language can be programmatically determined', - successCriteria: 'The default human language of each Web page can be programmatically determined', - techniques: ['H57'], - failures: [], - }, - { - _key: '3_2_3', - criterion: '3.2.3', - level: 'AA', - principle: 'understandable', - guideline: '3.2', - title: 'Consistent Navigation', - description: 'Navigation mechanisms are consistent', - successCriteria: 'Navigational mechanisms that are repeated on multiple Web pages occur in the same relative order', - techniques: ['G61'], - failures: ['F66'], - }, - { - _key: '4_1_1', - criterion: '4.1.1', - level: 'A', - principle: 'robust', - guideline: '4.1', - title: 'Parsing', - description: 'Content can be parsed by user agents', - successCriteria: 'Elements have complete start and end tags, are nested according to specifications', - techniques: ['G134', 'G192', 'H88', 'H74', 'H93', 'H94'], - failures: ['F70', 'F77'], - }, - { - _key: '4_1_2', - criterion: '4.1.2', - level: 'A', - principle: 'robust', - guideline: '4.1', - title: 'Name, Role, Value', - description: 'User interface components have programmatically determined name, role, and value', - successCriteria: 'For all user interface components, the name and role can be programmatically determined', - techniques: ['G108', 'H91', 'H44', 'H64', 'H65', 'H88'], - failures: ['F15', 'F20', 'F59', 'F68', 'F79', 'F86', 'F89'], - }, - ]; - } - - // Query methods - async getSiteByUrl(url: string): Promise { - const cursor = await this.db.query(aql` - FOR site IN sites - FILTER site.url == ${url} - LIMIT 1 - RETURN site - `); - const results = await cursor.all(); - return results.length > 0 ? results[0] : null; - } - - async getRecentScansForSite(siteKey: string, limit: number = 10): Promise { - const cursor = await this.db.query(aql` - FOR scan IN scans - FILTER scan.siteKey == ${siteKey} - SORT scan.timestamp DESC - LIMIT ${limit} - RETURN scan - `); - return cursor.all(); - } - - async getViolationsForScan(scanKey: string): Promise { - const cursor = await this.db.query(aql` - FOR violation IN violations - FILTER violation.scanKey == ${scanKey} - SORT violation.impact DESC - RETURN violation - `); - return cursor.all(); - } - - async getTopSites(limit: number = 100): Promise { - const cursor = await this.db.query(aql` - FOR site IN sites - FILTER site.currentScore > 0 - SORT site.currentScore DESC - LIMIT ${limit} - RETURN site - `); - return cursor.all(); - } - - async getCommonViolations(limit: number = 10): Promise { - const cursor = await this.db.query(aql` - FOR violation IN violations - COLLECT wcagCriterion = violation.wcagCriterion WITH COUNT INTO count - SORT count DESC - LIMIT ${limit} - RETURN { - criterion: wcagCriterion, - count: count - } - `); - return cursor.all(); - } - - async getSiteViolationTrend(siteKey: string, days: number = 30): Promise { - const startDate = new Date(); - startDate.setDate(startDate.getDate() - days); - - const cursor = await this.db.query(aql` - FOR scan IN scans - FILTER scan.siteKey == ${siteKey} AND scan.timestamp >= ${startDate} - SORT scan.timestamp ASC - RETURN { - timestamp: scan.timestamp, - violations: scan.violations, - score: scan.score - } - `); - return cursor.all(); - } - - async getOrganizationSites(orgKey: string): Promise { - const cursor = await this.db.query(aql` - FOR org IN organizations - FILTER org._key == ${orgKey} - FOR v, e IN 1..1 OUTBOUND org org_sites - RETURN v - `); - return cursor.all(); - } -} - -export function createArangoDBService(config?: Partial): ArangoDBService { - const defaultConfig: ArangoConfig = { - url: process.env.ARANGO_URL || 'http://localhost:8529', - database: process.env.ARANGO_DATABASE || 'accessibility', - username: process.env.ARANGO_USERNAME || 'root', - password: process.env.ARANGO_PASSWORD || 'development', - }; - - return new ArangoDBService({ ...defaultConfig, ...config }); -} diff --git a/tools/stale/packages/core/src/index.ts b/tools/stale/packages/core/src/index.ts deleted file mode 100644 index bbf377a..0000000 --- a/tools/stale/packages/core/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './database/arangodb'; -export { - ArangoDBService, - createArangoDBService, - type ArangoConfig, - type Site, - type Scan, - type Violation, - type WCAGCriterion, - type Organization, -} from './database/arangodb'; diff --git a/tools/stale/packages/scanner/package-lock.json b/tools/stale/packages/scanner/package-lock.json new file mode 100644 index 0000000..93da975 --- /dev/null +++ b/tools/stale/packages/scanner/package-lock.json @@ -0,0 +1,1116 @@ +{ + "name": "@accessibility-everywhere/scanner", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@accessibility-everywhere/scanner", + "version": "1.0.0", + "dependencies": { + "axe-core": "^4.8.2", + "playwright": "^1.40.0", + "puppeteer": "^24.31.0" + }, + "devDependencies": { + "@rescript/core": "^1.5.0", + "rescript": "^11.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rescript/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rescript/core/-/core-1.6.1.tgz", + "integrity": "sha512-vyb5k90ck+65Fgui+5vCja/mUfzKaK3kOPT4Z6aAJdHLH1eljEi1zKhXroCiCtpNLSWp8k4ulh1bdB5WS0hvqA==", + "dev": true, + "peerDependencies": { + "rescript": ">=11.1.0" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "optional": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1595872", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", + "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, + "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==" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.42.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.42.0.tgz", + "integrity": "sha512-94MoPfFp2eY3eYIMdINkez4IOP5TMHntlZbVx06fHlQTtiQiYgaY0L2Zzfod8PVUkPqP7m3Qlre2v8YS8cudPA==", + "hasInstallScript": true, + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1595872", + "puppeteer-core": "24.42.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.42.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.42.0.tgz", + "integrity": "sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1595872", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rescript": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/rescript/-/rescript-11.1.4.tgz", + "integrity": "sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "bsc": "bsc", + "bstracing": "lib/bstracing", + "rescript": "rescript" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==" + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "optional": true + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/tools/stale/packages/scanner/package.json b/tools/stale/packages/scanner/package.json index 3327fbe..f4f30e7 100644 --- a/tools/stale/packages/scanner/package.json +++ b/tools/stale/packages/scanner/package.json @@ -2,23 +2,21 @@ "name": "@accessibility-everywhere/scanner", "version": "1.0.0", "description": "Accessibility scanner using axe-core for WCAG compliance checking", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "src/Index.mjs", + "module": "src/Index.mjs", + "type": "module", "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "test": "jest" + "build": "rescript build", + "dev": "rescript build -w", + "clean": "rescript clean" }, "dependencies": { - "@accessibility-everywhere/core": "^1.0.0", "axe-core": "^4.8.2", "playwright": "^1.40.0", "puppeteer": "^24.31.0" }, "devDependencies": { - "@types/jest": "^29.5.8", - "@types/node": "^20.10.0", - "jest": "^29.7.0", - "typescript": "^5.3.2" + "rescript": "^11.1.0", + "@rescript/core": "^1.5.0" } } diff --git a/tools/stale/packages/scanner/rescript.json b/tools/stale/packages/scanner/rescript.json new file mode 100644 index 0000000..12167bd --- /dev/null +++ b/tools/stale/packages/scanner/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "@accessibility-everywhere/scanner", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": { + "module": "esmodule", + "in-source": true + }, + "suffix": ".mjs", + "bs-dependencies": ["@rescript/core"], + "bsc-flags": ["-open RescriptCore"] +} diff --git a/tools/stale/packages/scanner/src/Index.mjs b/tools/stale/packages/scanner/src/Index.mjs new file mode 100644 index 0000000..3843f92 --- /dev/null +++ b/tools/stale/packages/scanner/src/Index.mjs @@ -0,0 +1,16 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Scanner from "./Scanner.mjs"; + +var Scanner$1; + +var make = Scanner.make; + +var createScanner = Scanner.createScanner; + +export { + Scanner$1 as Scanner, + make , + createScanner , +} +/* Scanner Not a pure module */ diff --git a/tools/stale/packages/scanner/src/Index.res b/tools/stale/packages/scanner/src/Index.res new file mode 100644 index 0000000..d694716 --- /dev/null +++ b/tools/stale/packages/scanner/src/Index.res @@ -0,0 +1,4 @@ +module Scanner = Scanner + +let make = Scanner.make +let createScanner = Scanner.createScanner diff --git a/tools/stale/packages/scanner/src/Scanner.mjs b/tools/stale/packages/scanner/src/Scanner.mjs new file mode 100644 index 0000000..f8352a5 --- /dev/null +++ b/tools/stale/packages/scanner/src/Scanner.mjs @@ -0,0 +1,50 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Fs from "fs"; +import * as PervasivesU from "rescript/lib/es6/pervasivesU.js"; +import * as Core__Option from "@rescript/core/src/Core__Option.mjs"; + +var Puppeteer = {}; + +var Playwright = {}; + +var Fs$1 = {}; + +function make() { + var path = require.resolve("axe-core/axe.min.js"); + return { + axeSource: Fs.readFileSync(path, "utf8") + }; +} + +function calculateScore(_axeResults) { + return PervasivesU.failwith("TODO: calculateScore not yet implemented"); +} + +async function scanWithPuppeteer(_self, _options, _startTime) { + return PervasivesU.failwith("TODO: scanWithPuppeteer not yet implemented"); +} + +async function scan(self, options) { + var startTime = Date.now(); + var engine = Core__Option.getOr(options.engine, "puppeteer"); + if (engine === "playwright") { + return PervasivesU.failwith("TODO: scanWithPlaywright not yet implemented"); + } else { + return await scanWithPuppeteer(self, options, startTime); + } +} + +var createScanner = make; + +export { + Puppeteer , + Playwright , + Fs$1 as Fs, + make , + calculateScore , + scanWithPuppeteer , + scan , + createScanner , +} +/* fs Not a pure module */ diff --git a/tools/stale/packages/scanner/src/Scanner.res b/tools/stale/packages/scanner/src/Scanner.res new file mode 100644 index 0000000..0754830 --- /dev/null +++ b/tools/stale/packages/scanner/src/Scanner.res @@ -0,0 +1,119 @@ +type wcagLevel = [#A | #"AA" | #"AAA"] +type engine = [#puppeteer | #playwright] +type impact = [#critical | #serious | #moderate | #minor] + +type scanOptions = { + url: string, + wcagLevel: wcagLevel, + screenshot?: bool, + engine?: engine, +} + +type nodeDetail = { + html: string, + target: array, + failureSummary?: string, +} + +type violationDetail = { + id: string, + impact: impact, + description: string, + help: string, + helpUrl: string, + tags: array, + nodes: array, +} + +type passDetail = { + id: string, + description: string, + help: string, + tags: array, + nodes: array, +} + +type incompleteDetail = { + id: string, + description: string, + help: string, + nodes: array, +} + +type inapplicableDetail = { + id: string, + description: string, + help: string, +} + +type scanResult = { + url: string, + timestamp: string, + score: int, + duration: int, + wcagLevel: wcagLevel, + violations: array, + passes: array, + incomplete: array, + inapplicable: array, + screenshot?: string, +} + +module Puppeteer = { + type browser + type page + type launchOptions = {headless?: bool, args?: array} + + @module("puppeteer") external launch: launchOptions => promise = "launch" + @send external newPage: browser => promise = "newPage" + @send external close: browser => promise = "close" +} + +module Playwright = { + type browser + type page + + @module("playwright") @scope("chromium") + external launch: {..} => promise = "launch" +} + +module Fs = { + @module("fs") external readFileSync: (string, @as("utf8") _) => string = "readFileSync" +} + +@val external require: {..} = "require" +@send external resolve: ({..}, string) => string = "resolve" + +type t = {axeSource: string} + +let make = (): t => { + let path = require->resolve("axe-core/axe.min.js") + {axeSource: Fs.readFileSync(path)} +} + +let calculateScore = (_axeResults: {..}): int => { + // SCORING KERNEL: penalty weights per impact: + // critical = 10, serious = 5, moderate = 3, minor = 1. + // TODO: implement weighted score from violation nodes. + failwith("TODO: calculateScore not yet implemented") +} + +let scanWithPuppeteer = async ( + _self: t, + _options: scanOptions, + _startTime: float, +): scanResult => { + // TODO: launch headless Chromium, navigate, inject axeSource, run axe.run with WCAG tags, capture screenshot. + failwith("TODO: scanWithPuppeteer not yet implemented") +} + +let scan = async (self: t, options: scanOptions): scanResult => { + let startTime = Date.now() + let engine = options.engine->Option.getOr(#puppeteer) + switch engine { + | #puppeteer => await scanWithPuppeteer(self, options, startTime) + | #playwright => failwith("TODO: scanWithPlaywright not yet implemented") + } +} + +let createScanner = make diff --git a/tools/stale/packages/scanner/src/index.ts b/tools/stale/packages/scanner/src/index.ts deleted file mode 100644 index dea06a3..0000000 --- a/tools/stale/packages/scanner/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './scanner'; -export { AccessibilityScanner, createScanner } from './scanner'; -export type { - ScanOptions, - ScanResult, - ViolationDetail, - NodeDetail, - PassDetail, - IncompleteDetail, - InapplicableDetail, -} from './scanner'; diff --git a/tools/stale/packages/scanner/src/scanner.ts b/tools/stale/packages/scanner/src/scanner.ts deleted file mode 100644 index 27b0e16..0000000 --- a/tools/stale/packages/scanner/src/scanner.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Accessibility Scanner — Multi-Engine Web Audit Kernel. - * - * This module implements the automated accessibility scanning engine for - * the Accessibility Everywhere project. it orchestrates headless browsers - * (Puppeteer or Playwright) to execute the `axe-core` ruleset against - * target URLs. - * - * KEY FEATURES: - * 1. **Multi-Engine Support**: Seamlessly switches between Puppeteer - * and Playwright based on environment capabilities. - * 2. **WCAG Tiering**: Supports auditing against WCAG 2.1/2.2 at - * A, AA, and AAA levels. - * 3. **Weighted Scoring**: Calculates a normalized accessibility score - * based on the impact and frequency of violations. - * 4. **Forensics**: Captures full-page screenshots and detailed - * HTML node selectors for identified issues. - */ - -import { Browser, Page, launch } from 'puppeteer'; -import { chromium, Browser as PlaywrightBrowser, Page as PlaywrightPage } from 'playwright'; -import * as axe from 'axe-core'; -import * as fs from 'fs'; -// ... [other imports] - -export class AccessibilityScanner { - private axeSource: string; - - constructor() { - // BOOTSTRAP: Synchronously loads the minified axe-core kernel - // for injection into the browser context. - this.axeSource = fs.readFileSync(require.resolve('axe-core/axe.min.js'), 'utf8'); - } - - /** - * SCAN: The primary entry point for single-URL auditing. - * Dispatches to the engine-specific runner (Puppeteer/Playwright). - */ - async scan(options: ScanOptions): Promise { - const startTime = Date.now(); - const engine = options.engine || 'puppeteer'; - // ... [Engine dispatch logic] - } - - /** - * PUPPETEER RUNNER: Implements the audit pipeline using Chromium. - * - * SEQUENCE: - * 1. SPAWN: Launch headless browser with security sandboxing disabled - * for container compatibility. - * 2. NAVIGATE: Go to target URL and wait for network stability. - * 3. INJECT: Execute the `axeSource` string within the page. - * 4. EXECUTE: Run `axe.run` with the requested WCAG tags. - * 5. CAPTURE: Record metadata and optional base64 screenshots. - */ - private async scanWithPuppeteer(options: ScanOptions, startTime: number): Promise { - // ... [Implementation using page.evaluate] - } - - /** - * SCORING KERNEL: Computes a safety percentage from 0 to 100. - * - * WEIGHTS: - * - Critical: 10 points penalty per node. - * - Serious: 5 points penalty. - * - Moderate: 3 points penalty. - * - Minor: 1 point penalty. - */ - private calculateScore(axeResults: any): number { - // ... [Heuristic calculation logic] - } -} diff --git a/tools/stale/packages/stale/components/react/package-lock.json b/tools/stale/packages/stale/components/react/package-lock.json new file mode 100644 index 0000000..13d8219 --- /dev/null +++ b/tools/stale/packages/stale/components/react/package-lock.json @@ -0,0 +1,107 @@ +{ + "name": "@accessibility-everywhere/react", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@accessibility-everywhere/react", + "version": "1.0.0", + "devDependencies": { + "@rescript/core": "^1.5.0", + "@rescript/react": "^0.13.0", + "rescript": "^11.1.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@rescript/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rescript/core/-/core-1.6.1.tgz", + "integrity": "sha512-vyb5k90ck+65Fgui+5vCja/mUfzKaK3kOPT4Z6aAJdHLH1eljEi1zKhXroCiCtpNLSWp8k4ulh1bdB5WS0hvqA==", + "dev": true, + "peerDependencies": { + "rescript": ">=11.1.0" + } + }, + "node_modules/@rescript/react": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@rescript/react/-/react-0.13.1.tgz", + "integrity": "sha512-VIWtu/sAJyYmDVoAhit0LHDYQrW6RqZ6z8sh8san5cjEAT4klv8JWkiaSK3FGUfooUDkGUXXgKTkqyj8zRR21w==", + "dev": true, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/rescript": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/rescript/-/rescript-11.1.4.tgz", + "integrity": "sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "bsc": "bsc", + "bstracing": "lib/bstracing", + "rescript": "rescript" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + } + } +} diff --git a/tools/stale/packages/stale/components/react/package.json b/tools/stale/packages/stale/components/react/package.json index 5d2b909..7e4a468 100644 --- a/tools/stale/packages/stale/components/react/package.json +++ b/tools/stale/packages/stale/components/react/package.json @@ -2,34 +2,22 @@ "name": "@accessibility-everywhere/react", "version": "1.0.0", "description": "Production-ready accessible React components following WCAG 2.1 AA standards", - "main": "dist/index.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts", + "main": "src/Index.mjs", + "module": "src/Index.mjs", + "type": "module", "scripts": { - "build": "rollup -c", - "dev": "rollup -c -w", - "test": "jest", - "storybook": "start-storybook -p 6006", - "build-storybook": "build-storybook" + "build": "rescript build", + "dev": "rescript build -w", + "clean": "rescript clean" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "dependencies": { - "@reach/auto-id": "^0.18.0", - "@reach/utils": "^0.18.0" + "react": "^18.0.0", + "react-dom": "^18.0.0" }, "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@rollup/plugin-typescript": "^11.1.5", - "rollup": "^4.6.0", - "rollup-plugin-peer-deps-external": "^2.2.4", - "typescript": "^5.3.2", - "jest": "^29.7.0", - "@testing-library/react": "^14.1.2", - "@storybook/react": "^7.6.0" + "rescript": "^11.1.0", + "@rescript/core": "^1.5.0", + "@rescript/react": "^0.13.0" }, "keywords": [ "react", @@ -38,6 +26,7 @@ "wcag", "components", "ui", - "aria" + "aria", + "rescript" ] } diff --git a/tools/stale/packages/stale/components/react/rescript.json b/tools/stale/packages/stale/components/react/rescript.json new file mode 100644 index 0000000..a4ecc9a --- /dev/null +++ b/tools/stale/packages/stale/components/react/rescript.json @@ -0,0 +1,17 @@ +{ + "name": "@accessibility-everywhere/react", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": { + "module": "esmodule", + "in-source": true + }, + "suffix": ".mjs", + "jsx": { "version": 4, "mode": "automatic" }, + "bs-dependencies": ["@rescript/core", "@rescript/react"], + "bsc-flags": ["-open RescriptCore"] +} diff --git a/tools/stale/packages/stale/components/react/src/Button.mjs b/tools/stale/packages/stale/components/react/src/Button.mjs new file mode 100644 index 0000000..e702dfb --- /dev/null +++ b/tools/stale/packages/stale/components/react/src/Button.mjs @@ -0,0 +1,89 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as React from "react"; +import * as Caml_option from "rescript/lib/es6/caml_option.js"; +import * as Core__Option from "@rescript/core/src/Core__Option.mjs"; +import * as JsxRuntime from "react/jsx-runtime"; + +function variantClass(v) { + if (v === "primary") { + return "a11y-button--primary"; + } else if (v === "ghost") { + return "a11y-button--ghost"; + } else if (v === "danger") { + return "a11y-button--danger"; + } else { + return "a11y-button--secondary"; + } +} + +function sizeClass(s) { + if (s === "md") { + return "a11y-button--md"; + } else if (s === "sm") { + return "a11y-button--sm"; + } else { + return "a11y-button--lg"; + } +} + +var make = React.forwardRef(function (props, ref) { + var variant = Core__Option.getOr(props.variant, "primary"); + var size = Core__Option.getOr(props.size, "md"); + var loading = Core__Option.getOr(props.loading, false); + var disabled = Core__Option.getOr(props.disabled, false); + var className = Core__Option.getOr(props.className, ""); + var type_ = Core__Option.getOr(props.type, "button"); + var busy = disabled || loading; + var classes = [ + "a11y-button", + variantClass(variant), + sizeClass(size), + loading ? "a11y-button--loading" : "", + className + ].filter(function (s) { + return s !== ""; + }).join(" "); + var domRef = Core__Option.map((ref == null) ? undefined : Caml_option.some(ref), (function (prim) { + return prim; + })); + var match = props.iconBefore; + var match$1 = props.iconAfter; + return JsxRuntime.jsxs("button", { + children: [ + loading ? JsxRuntime.jsx("span", { + "aria-hidden": true, + className: "a11y-button__spinner" + }) : null, + loading || match === undefined ? null : JsxRuntime.jsx("span", { + children: Caml_option.valFromOption(match), + "aria-hidden": true, + className: "a11y-button__icon-before" + }), + JsxRuntime.jsx("span", { + children: Core__Option.getOr(props.children, null), + className: "a11y-button__text" + }), + loading || match$1 === undefined ? null : JsxRuntime.jsx("span", { + children: Caml_option.valFromOption(match$1), + "aria-hidden": true, + className: "a11y-button__icon-after" + }) + ], + ref: domRef, + "aria-disabled": busy, + "aria-label": props.ariaLabel, + "aria-busy": loading, + className: classes, + disabled: busy, + type: type_, + onClick: props.onClick + }); + }); + +export { + variantClass , + sizeClass , + make , +} +/* make Not a pure module */ diff --git a/tools/stale/packages/stale/components/react/src/Button.res b/tools/stale/packages/stale/components/react/src/Button.res new file mode 100644 index 0000000..c903075 --- /dev/null +++ b/tools/stale/packages/stale/components/react/src/Button.res @@ -0,0 +1,79 @@ +type variant = [#primary | #secondary | #ghost | #danger] +type size = [#sm | #md | #lg] + +type props = { + variant?: variant, + size?: size, + loading?: bool, + disabled?: bool, + iconBefore?: React.element, + iconAfter?: React.element, + children?: React.element, + className?: string, + \"type"?: string, + onClick?: JsxEvent.Mouse.t => unit, + ariaLabel?: string, +} + +let variantClass = v => + switch v { + | #primary => "a11y-button--primary" + | #secondary => "a11y-button--secondary" + | #ghost => "a11y-button--ghost" + | #danger => "a11y-button--danger" + } + +let sizeClass = s => + switch s { + | #sm => "a11y-button--sm" + | #md => "a11y-button--md" + | #lg => "a11y-button--lg" + } + +let make = React.forwardRef((props: props, ref) => { + let variant = props.variant->Option.getOr(#primary) + let size = props.size->Option.getOr(#md) + let loading = props.loading->Option.getOr(false) + let disabled = props.disabled->Option.getOr(false) + let className = props.className->Option.getOr("") + let type_ = props.\"type"->Option.getOr("button") + let busy = disabled || loading + + let classes = + [ + "a11y-button", + variantClass(variant), + sizeClass(size), + loading ? "a11y-button--loading" : "", + className, + ] + ->Array.filter(s => s !== "") + ->Array.join(" ") + + let domRef = ref->Nullable.toOption->Option.map(ReactDOM.Ref.domRef) + + +}) diff --git a/tools/stale/packages/stale/components/react/src/Button/Button.tsx b/tools/stale/packages/stale/components/react/src/Button/Button.tsx deleted file mode 100644 index baac687..0000000 --- a/tools/stale/packages/stale/components/react/src/Button/Button.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { forwardRef } from 'react'; - -export interface ButtonProps extends React.ButtonHTMLAttributes { - /** Button variant */ - variant?: 'primary' | 'secondary' | 'ghost' | 'danger'; - /** Button size */ - size?: 'sm' | 'md' | 'lg'; - /** Loading state */ - loading?: boolean; - /** Icon to display before text */ - iconBefore?: React.ReactNode; - /** Icon to display after text */ - iconAfter?: React.ReactNode; -} - -/** - * Accessible button component following WCAG 2.1 AA standards - * - * Features: - * - Proper focus management - * - Keyboard navigation - * - Screen reader support - * - Loading states with ARIA - * - Disabled state handling - * - * @example - * - */ -export const Button = forwardRef( - ( - { - variant = 'primary', - size = 'md', - loading = false, - disabled = false, - iconBefore, - iconAfter, - children, - className = '', - type = 'button', - ...props - }, - ref - ) => { - const baseClasses = 'a11y-button'; - const variantClasses = `a11y-button--${variant}`; - const sizeClasses = `a11y-button--${size}`; - const loadingClasses = loading ? 'a11y-button--loading' : ''; - const classes = `${baseClasses} ${variantClasses} ${sizeClasses} ${loadingClasses} ${className}`.trim(); - - return ( - - ); - } -); - -Button.displayName = 'Button'; diff --git a/tools/stale/packages/stale/components/react/src/Index.mjs b/tools/stale/packages/stale/components/react/src/Index.mjs new file mode 100644 index 0000000..24ad52a --- /dev/null +++ b/tools/stale/packages/stale/components/react/src/Index.mjs @@ -0,0 +1,18 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +var Button; + +var Modal; + +var version = "1.0.0"; + +var wcagLevel = "AA"; + +export { + Button , + Modal , + version , + wcagLevel , +} +/* No side effect */ diff --git a/tools/stale/packages/stale/components/react/src/Index.res b/tools/stale/packages/stale/components/react/src/Index.res new file mode 100644 index 0000000..9af1eb5 --- /dev/null +++ b/tools/stale/packages/stale/components/react/src/Index.res @@ -0,0 +1,5 @@ +module Button = Button +module Modal = Modal + +let version = "1.0.0" +let wcagLevel = "AA" diff --git a/tools/stale/packages/stale/components/react/src/Modal.mjs b/tools/stale/packages/stale/components/react/src/Modal.mjs new file mode 100644 index 0000000..12a9b07 --- /dev/null +++ b/tools/stale/packages/stale/components/react/src/Modal.mjs @@ -0,0 +1,230 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as React from "react"; +import * as ReactDom from "react-dom"; +import * as Caml_option from "rescript/lib/es6/caml_option.js"; +import * as Core__Option from "@rescript/core/src/Core__Option.mjs"; +import * as JsxRuntime from "react/jsx-runtime"; + +var DomFFI = {}; + +function getFocusableElements(container) { + if (container === undefined) { + return []; + } + var selector = [ + "button:not([disabled])", + "[href]", + "input:not([disabled])", + "select:not([disabled])", + "textarea:not([disabled])", + "[tabindex]:not([tabindex=\"-1\"])" + ].join(","); + return Caml_option.valFromOption(container).querySelectorAll(selector); +} + +function getFirstFocusable(container) { + return getFocusableElements(container)[0]; +} + +function Modal(props) { + var __className = props.className; + var initialFocusRef = props.initialFocusRef; + var __closeOnEscape = props.closeOnEscape; + var __closeOnOverlayClick = props.closeOnOverlayClick; + var __size = props.size; + var onClose = props.onClose; + var isOpen = props.isOpen; + var size = __size !== undefined ? __size : "md"; + var closeOnOverlayClick = __closeOnOverlayClick !== undefined ? __closeOnOverlayClick : true; + var closeOnEscape = __closeOnEscape !== undefined ? __closeOnEscape : true; + var className = __className !== undefined ? __className : ""; + var titleId = React.useId(); + var contentId = React.useId(); + var overlayRef = React.useRef(null); + var modalRef = React.useRef(null); + var previousFocusRef = React.useRef(null); + var match = React.useState(function () { + return false; + }); + var setMounted = match[1]; + React.useEffect((function () { + setMounted(function (param) { + return true; + }); + return (function () { + setMounted(function (param) { + return false; + }); + }); + }), []); + React.useEffect((function () { + if (!isOpen) { + return ; + } + previousFocusRef.current = document.activeElement; + var modalCurrent = modalRef.current; + var initial = initialFocusRef !== undefined ? Caml_option.nullable_to_opt(initialFocusRef.current) : undefined; + var target = initial !== undefined ? initial : getFirstFocusable((modalCurrent == null) ? undefined : Caml_option.some(modalCurrent)); + Core__Option.forEach(target, (function (prim) { + prim.focus(); + })); + return (function () { + Core__Option.forEach(Caml_option.nullable_to_opt(previousFocusRef.current), (function (prim) { + prim.focus(); + })); + }); + }), [ + isOpen, + initialFocusRef + ]); + React.useEffect((function () { + if (!(isOpen && closeOnEscape)) { + return ; + } + var handleEscape = function (e) { + if (e.key === "Escape") { + return onClose(); + } + + }; + document.addEventListener("keydown", handleEscape); + return (function () { + document.removeEventListener("keydown", handleEscape); + }); + }), [ + isOpen, + closeOnEscape, + onClose + ]); + React.useEffect((function () { + if (!isOpen) { + return ; + } + var handleTab = function (e) { + if (e.key !== "Tab") { + return ; + } + var modalCurrent = modalRef.current; + var focusable = getFocusableElements((modalCurrent == null) ? undefined : Caml_option.some(modalCurrent)); + var len = focusable.length; + if (len <= 0) { + return ; + } + var first = focusable[0]; + var last = focusable[len - 1 | 0]; + var active = document.activeElement; + var active$1 = (active == null) ? undefined : Caml_option.some(active); + if (e.shiftKey) { + if (active$1 === Caml_option.some(first)) { + e.preventDefault(); + last.focus(); + return ; + } else { + return ; + } + } else if (active$1 === Caml_option.some(last)) { + e.preventDefault(); + first.focus(); + return ; + } else { + return ; + } + }; + document.addEventListener("keydown", handleTab); + return (function () { + document.removeEventListener("keydown", handleTab); + }); + }), [isOpen]); + React.useEffect((function () { + if (!isOpen) { + return ; + } + var body = document.body; + var originalOverflow = body.style.overflow; + body.style.overflow = "hidden"; + return (function () { + body.style.overflow = originalOverflow; + }); + }), [isOpen]); + var handleOverlayClick = function (e) { + if (!closeOnOverlayClick) { + return ; + } + var target = e.target; + var o = overlayRef.current; + if (!(o == null) && o === target) { + return onClose(); + } + + }; + if (!isOpen || !match[0]) { + return null; + } + var sizeClass = size === "lg" ? "a11y-modal--lg" : ( + size === "md" ? "a11y-modal--md" : ( + size === "sm" ? "a11y-modal--sm" : "a11y-modal--full" + ) + ); + var modalClasses = [ + "a11y-modal", + sizeClass, + className + ].filter(function (s) { + return s !== ""; + }).join(" "); + var modal = JsxRuntime.jsx("div", { + children: JsxRuntime.jsxs("div", { + children: [ + JsxRuntime.jsxs("div", { + children: [ + JsxRuntime.jsx("h2", { + children: props.title, + className: "a11y-modal__title", + id: titleId + }), + JsxRuntime.jsx("button", { + children: JsxRuntime.jsx("span", { + children: "Ɨ", + "aria-hidden": true + }), + "aria-label": "Close dialog", + className: "a11y-modal__close", + type: "button", + onClick: (function (param) { + onClose(); + }) + }) + ], + className: "a11y-modal__header" + }), + JsxRuntime.jsx("div", { + children: props.children, + className: "a11y-modal__content", + id: contentId + }) + ], + ref: Caml_option.some(modalRef), + "aria-modal": true, + "aria-describedby": contentId, + "aria-labelledby": titleId, + className: modalClasses, + role: "dialog" + }), + ref: Caml_option.some(overlayRef), + className: "a11y-modal-overlay", + role: "presentation", + onClick: handleOverlayClick + }); + return ReactDom.createPortal(modal, document.body); +} + +var make = Modal; + +export { + DomFFI , + getFocusableElements , + getFirstFocusable , + make , +} +/* react Not a pure module */ diff --git a/tools/stale/packages/stale/components/react/src/Modal.res b/tools/stale/packages/stale/components/react/src/Modal.res new file mode 100644 index 0000000..0a498ac --- /dev/null +++ b/tools/stale/packages/stale/components/react/src/Modal.res @@ -0,0 +1,199 @@ +type modalSize = [#sm | #md | #lg | #full] + +module DomFFI = { + type document + type keyboardEvent = {key: string, shiftKey: bool} + + @val external document: document = "document" + @get external activeElement: document => Js.Nullable.t = "activeElement" + @get external body: document => Dom.element = "body" + @get external style: Dom.element => {..} = "style" + @get external overflow: {..} => string = "overflow" + @set external setOverflow: ({..}, string) => unit = "overflow" + + @send external focus: Dom.element => unit = "focus" + @send + external querySelectorAll: (Dom.element, string) => array = "querySelectorAll" + @send + external addKeyListener: (document, @as("keydown") _, keyboardEvent => unit) => unit = + "addEventListener" + @send + external removeKeyListener: (document, @as("keydown") _, keyboardEvent => unit) => unit = + "removeEventListener" + @send external preventDefault: keyboardEvent => unit = "preventDefault" +} + +let getFocusableElements = (container: option): array => + switch container { + | None => [] + | Some(el) => { + let selector = + [ + "button:not([disabled])", + "[href]", + "input:not([disabled])", + "select:not([disabled])", + "textarea:not([disabled])", + "[tabindex]:not([tabindex=\"-1\"])", + ]->Array.join(",") + DomFFI.querySelectorAll(el, selector) + } + } + +let getFirstFocusable = container => getFocusableElements(container)->Array.get(0) + +@react.component +let make = ( + ~isOpen: bool, + ~onClose: unit => unit, + ~title: string, + ~children: React.element, + ~size: modalSize=#md, + ~closeOnOverlayClick: bool=true, + ~closeOnEscape: bool=true, + ~initialFocusRef: option>>=?, + ~className: string="", +) => { + let titleId = React.useId() + let contentId = React.useId() + let overlayRef = React.useRef(Js.Nullable.null: Js.Nullable.t) + let modalRef = React.useRef(Js.Nullable.null: Js.Nullable.t) + let previousFocusRef = React.useRef(Js.Nullable.null: Js.Nullable.t) + let (mounted, setMounted) = React.useState(() => false) + + React.useEffect0(() => { + setMounted(_ => true) + Some(() => setMounted(_ => false)) + }) + + React.useEffect2(() => { + if isOpen { + previousFocusRef.current = DomFFI.activeElement(DomFFI.document) + let modalCurrent = modalRef.current->Js.Nullable.toOption + let initial = switch initialFocusRef { + | Some(r) => r.current->Js.Nullable.toOption + | None => None + } + let target = switch initial { + | Some(_) as s => s + | None => getFirstFocusable(modalCurrent) + } + target->Option.forEach(DomFFI.focus) + Some( + () => + previousFocusRef.current + ->Js.Nullable.toOption + ->Option.forEach(DomFFI.focus), + ) + } else { + None + } + }, (isOpen, initialFocusRef)) + + React.useEffect3(() => { + if isOpen && closeOnEscape { + let handleEscape = (e: DomFFI.keyboardEvent) => + if e.key === "Escape" { + onClose() + } + DomFFI.addKeyListener(DomFFI.document, handleEscape) + Some(() => DomFFI.removeKeyListener(DomFFI.document, handleEscape)) + } else { + None + } + }, (isOpen, closeOnEscape, onClose)) + + React.useEffect1(() => { + if isOpen { + let handleTab = (e: DomFFI.keyboardEvent) => + if e.key === "Tab" { + let modalCurrent = modalRef.current->Js.Nullable.toOption + let focusable = getFocusableElements(modalCurrent) + let len = Array.length(focusable) + if len > 0 { + let first = focusable->Array.getUnsafe(0) + let last = focusable->Array.getUnsafe(len - 1) + let active = DomFFI.activeElement(DomFFI.document)->Js.Nullable.toOption + if e.shiftKey { + if active === Some(first) { + DomFFI.preventDefault(e) + DomFFI.focus(last) + } + } else if active === Some(last) { + DomFFI.preventDefault(e) + DomFFI.focus(first) + } + } + } + DomFFI.addKeyListener(DomFFI.document, handleTab) + Some(() => DomFFI.removeKeyListener(DomFFI.document, handleTab)) + } else { + None + } + }, [isOpen]) + + React.useEffect1(() => { + if isOpen { + let body = DomFFI.body(DomFFI.document) + let originalOverflow = DomFFI.overflow(DomFFI.style(body)) + DomFFI.setOverflow(DomFFI.style(body), "hidden") + Some(() => DomFFI.setOverflow(DomFFI.style(body), originalOverflow)) + } else { + None + } + }, [isOpen]) + + let handleOverlayClick = (e: JsxEvent.Mouse.t) => { + if closeOnOverlayClick { + let target: Dom.element = e->JsxEvent.Mouse.target->Obj.magic + switch overlayRef.current->Js.Nullable.toOption { + | Some(o) if o === target => onClose() + | _ => () + } + } + } + + if !isOpen || !mounted { + React.null + } else { + let sizeClass = switch size { + | #sm => "a11y-modal--sm" + | #md => "a11y-modal--md" + | #lg => "a11y-modal--lg" + | #full => "a11y-modal--full" + } + let modalClasses = + ["a11y-modal", sizeClass, className] + ->Array.filter(s => s !== "") + ->Array.join(" ") + + let modal = +
+
+
+

{React.string(title)}

+ +
+
{children}
+
+
+ + ReactDOM.createPortal(modal, DomFFI.body(DomFFI.document)) + } +} diff --git a/tools/stale/packages/stale/components/react/src/Modal/Modal.tsx b/tools/stale/packages/stale/components/react/src/Modal/Modal.tsx deleted file mode 100644 index 19d6f3c..0000000 --- a/tools/stale/packages/stale/components/react/src/Modal/Modal.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { useId } from '@reach/auto-id'; - -export interface ModalProps { - /** Whether modal is open */ - isOpen: boolean; - /** Callback when modal should close */ - onClose: () => void; - /** Modal title */ - title: string; - /** Modal content */ - children: React.ReactNode; - /** Modal size */ - size?: 'sm' | 'md' | 'lg' | 'full'; - /** Close on overlay click */ - closeOnOverlayClick?: boolean; - /** Close on Escape key */ - closeOnEscape?: boolean; - /** Initial focus element */ - initialFocusRef?: React.RefObject; - /** Custom className */ - className?: string; -} - -/** - * Accessible modal dialog following WCAG 2.1 AA standards - * - * Features: - * - Focus trap (keeps focus inside modal) - * - Focus restoration (returns to trigger on close) - * - Escape key handling - * - Overlay click handling - * - ARIA labels and roles - * - Screen reader announcements - * - Scroll locking - * - * @example - * setIsOpen(false)} - * title="Delete Account" - * > - *

Are you sure?

- *
- */ -export function Modal({ - isOpen, - onClose, - title, - children, - size = 'md', - closeOnOverlayClick = true, - closeOnEscape = true, - initialFocusRef, - className = '', -}: ModalProps) { - const titleId = useId(); - const contentId = useId(); - const overlayRef = useRef(null); - const modalRef = useRef(null); - const previousFocusRef = useRef(null); - const [mounted, setMounted] = useState(false); - - // Portal mounting - useEffect(() => { - setMounted(true); - return () => setMounted(false); - }, []); - - // Focus management - useEffect(() => { - if (!isOpen) return; - - // Store current focus - previousFocusRef.current = document.activeElement as HTMLElement; - - // Focus initial element or first focusable element - const focusElement = initialFocusRef?.current || getFirstFocusable(modalRef.current); - focusElement?.focus(); - - // Restore focus on unmount - return () => { - previousFocusRef.current?.focus(); - }; - }, [isOpen, initialFocusRef]); - - // Escape key handler - useEffect(() => { - if (!isOpen || !closeOnEscape) return; - - const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - - document.addEventListener('keydown', handleEscape); - return () => document.removeEventListener('keydown', handleEscape); - }, [isOpen, closeOnEscape, onClose]); - - // Focus trap - useEffect(() => { - if (!isOpen) return; - - const handleTab = (e: KeyboardEvent) => { - if (e.key !== 'Tab' || !modalRef.current) return; - - const focusableElements = getFocusableElements(modalRef.current); - if (focusableElements.length === 0) return; - - const firstElement = focusableElements[0]; - const lastElement = focusableElements[focusableElements.length - 1]; - - if (e.shiftKey) { - // Shift + Tab - if (document.activeElement === firstElement) { - e.preventDefault(); - lastElement.focus(); - } - } else { - // Tab - if (document.activeElement === lastElement) { - e.preventDefault(); - firstElement.focus(); - } - } - }; - - document.addEventListener('keydown', handleTab); - return () => document.removeEventListener('keydown', handleTab); - }, [isOpen]); - - // Scroll lock - useEffect(() => { - if (!isOpen) return; - - const originalOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - - return () => { - document.body.style.overflow = originalOverflow; - }; - }, [isOpen]); - - // Overlay click handler - const handleOverlayClick = (e: React.MouseEvent) => { - if (closeOnOverlayClick && e.target === overlayRef.current) { - onClose(); - } - }; - - if (!isOpen || !mounted) return null; - - const modal = ( -
-
-
-

- {title} -

- -
-
- {children} -
-
-
- ); - - return createPortal(modal, document.body); -} - -// Helper functions -function getFocusableElements(container: HTMLElement | null): HTMLElement[] { - if (!container) return []; - - const selector = [ - 'button:not([disabled])', - '[href]', - 'input:not([disabled])', - 'select:not([disabled])', - 'textarea:not([disabled])', - '[tabindex]:not([tabindex="-1"])', - ].join(','); - - return Array.from(container.querySelectorAll(selector)); -} - -function getFirstFocusable(container: HTMLElement | null): HTMLElement | null { - const elements = getFocusableElements(container); - return elements[0] || null; -} diff --git a/tools/stale/packages/stale/components/react/src/index.ts b/tools/stale/packages/stale/components/react/src/index.ts deleted file mode 100644 index 76a0a7e..0000000 --- a/tools/stale/packages/stale/components/react/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Core Components -export { Button } from './Button/Button'; -export type { ButtonProps } from './Button/Button'; - -export { Modal } from './Modal/Modal'; -export type { ModalProps } from './Modal/Modal'; - -// Component documentation and version -export const version = '1.0.0'; -export const wcagLevel = 'AA';