diff --git a/README.md b/README.md index b113a81c..45d5d146 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,19 @@ -Incredibly fast and precise web accessibility engine. +Incredibly fast and precise web accessibility engine with 0 dependencies. ```sh npm install kayle --save ``` -Playwright 🎭 or Puppeteer 🤖 - ```ts import { kayle } from "kayle"; +// Playwright 🎭 or Puppeteer 🤖 const page = await browser.newPage(); -const results = await kayle({ page, browser, origin: "https://mywebsite.com" }); +const results = await kayle({ page, browser, origin: "https://a11ywatch.com" }); ``` If you need to run a full site-wide crawl import `autoKayle`. @@ -36,9 +35,9 @@ const results = await autoKayle({ runners: ["htmlcs", "axe"], includeWarnings: true, origin: "https://a11ywatch.com", - cb: function callback(result) { - console.log(result) - } + cb: function callback(result) { + console.log(result); + }, // store: `${process.cwd()}/_data/`, // _data folder must exist first }); @@ -159,11 +158,9 @@ the best aspects of testing without worrying about a `name`. 1. zh-CN ("Chinese-Simplified") 1. zh-TW ("Chinese-Traditional") -## Performance - -This project is the fastest web accessibility runner OSS. The `htmlcs` and `axe-core` handling of the runners runs faster due to bug fixes and improved optimizations. This library optimizes the scripts to take advtage of v8 and pre-compiles locales in separate scripts for blazing fast speeds. +## Features -- Playwright runs 100% faster than puppeteer. Most of it is due to more fine grain control of events, ws connections, and timers. +You can enable a high performance adblock detection by brave by installing `npm i adblock-rs` to the project. This module needs to be manually installed and the env variable `KAYLE_ADBLOCK` needs to be set to `true`. ## Testing diff --git a/kayle/README.md b/kayle/README.md index ca30df71..f0122d82 100644 --- a/kayle/README.md +++ b/kayle/README.md @@ -86,14 +86,13 @@ kayle supports multiple test runners which return different results. The built-i - `htmlcs` (default): run tests using [HTML CodeSniffer](./lib/runners/htmlcs.ts) - `custom`: custom runners. -## Benchmarks - -## playwright +## Playwright/Puppeteer `Fast_htmlcs`: expect runs to finish between 10-40ms with static html and around 30-90ms without. `Fast_axecore`: expect runs to finish between 40-350ms with static html and around 30-90ms without. We are working on making fast_axecore fast so it can run relatively near htmlcs. +If you are using puppeteer expect around 2x slower results. ## Utils diff --git a/kayle/lib/auto.ts b/kayle/lib/auto.ts new file mode 100644 index 00000000..f4897681 --- /dev/null +++ b/kayle/lib/auto.ts @@ -0,0 +1,85 @@ +import { _log } from "./config"; +import { Audit, kayle, RunnerConf } from "./kayle"; + +let write; +let extractLinks; + +// on autoKayle link find callback +declare function callback(audit: Audit): Audit; +declare function callback(audit: Audit): Promise; + +/** + * Run accessibility tests for page auto running until all pages complete. + * @param {Object} [config={}] config - Options to change the way tests run. + * @returns {Promise} Returns a promise which resolves with array of results. + */ +export async function autoKayle( + o: RunnerConf & { log?: boolean; store?: string; cb?: typeof callback } = {}, + ignoreSet?: Set, + _results?: Audit[] +): Promise { + if (!write) { + const { writeFile } = await import("fs/promises"); + write = writeFile; + } + // pre init list + if (!_results) { + _results = []; + } + + const result = await kayle(o, true); + + _results.push(result); + + if (o.cb && typeof o.cb === "function") { + await o.cb(result); + } + + // auto run links until finished. + if (!extractLinks) { + extractLinks = (await import("./wasm/extract")).extractLinks; + } + + if (!ignoreSet) { + ignoreSet = new Set(); + } + + const links: string[] = await extractLinks(o); + + // persist html file to disk + if (o.store) { + await write( + `${o.store}/${encodeURIComponent(o.page.url())}`, + await o.page.content() + ); + } + + await o.page.close(); + + await Promise.all( + links.map(async (link) => { + if (ignoreSet.has(link)) { + return await Promise.resolve(); + } + + if (_log.enabled) { + console.log(`Running: ${link}`); + } + + ignoreSet.add(link); + + return await autoKayle( + { + ...o, + page: await o.browser.newPage(), + html: null, + origin: link, + }, + ignoreSet, + _results + ); + }) + ); + + return _results; +} diff --git a/kayle/lib/config.ts b/kayle/lib/config.ts index 34c54ef8..d8cbad23 100644 --- a/kayle/lib/config.ts +++ b/kayle/lib/config.ts @@ -114,3 +114,15 @@ export type RunnerConfig = { // prevent auto intercept request to prevent fetching resources. noIntercept?: boolean; }; + +// log singleton +export const _log = { enabled: false }; + +/** + * Enable or disable logging. + * @param {Object} [enabled] enabled - Enable console logging. + * @returns {void} Returns void. + */ +export function setLogging(enabled?: boolean): void { + _log.enabled = enabled; +} diff --git a/kayle/lib/data/README.md b/kayle/lib/data/README.md new file mode 100644 index 00000000..98e932cd --- /dev/null +++ b/kayle/lib/data/README.md @@ -0,0 +1,5 @@ +# data + +Things that help make kayle more efficient. + +1. [adblock](https://github.com/brave/adblock-rust) - Enabled by using the `process.env.KAYLE_ADBLOCK=true` or setting the env variable `KAYLE_ADBLOCK` to true. In order to use adblock you also need to run `npm i adblock-rs` to install the wasm module locally. diff --git a/kayle/lib/data/list.ts b/kayle/lib/data/list.ts new file mode 100644 index 00000000..4c22a442 --- /dev/null +++ b/kayle/lib/data/list.ts @@ -0,0 +1,2 @@ +// get a list of valid things that help with data +export const adblock = Symbol("adblock engine to ignore resources"); diff --git a/kayle/lib/index.ts b/kayle/lib/index.ts index 94e52d69..d217ac21 100644 --- a/kayle/lib/index.ts +++ b/kayle/lib/index.ts @@ -1,15 +1,9 @@ -export { - kayle, - setLogging, - autoKayle, - Issue, - Audit, - MetaInfo, - Automatable, -} from "./kayle"; +export { kayle, Issue, Audit, MetaInfo, Automatable } from "./kayle"; +export { autoKayle } from "./auto"; export { runnersJavascript } from "./runner-js"; export { goToPage, setNetworkInterception, networkBlock, } from "./utils/go-to-page"; +export { setLogging } from "./config"; diff --git a/kayle/lib/kayle.ts b/kayle/lib/kayle.ts index 34c5c16d..b228517a 100644 --- a/kayle/lib/kayle.ts +++ b/kayle/lib/kayle.ts @@ -1,10 +1,9 @@ import { extractArgs } from "./option"; import { runAction } from "./action"; -import { RunnerConfig } from "./config"; +import { RunnerConfig, _log } from "./config"; import { runnersJavascript, getRunner } from "./runner-js"; import { goToPage, setNetworkInterception } from "./utils/go-to-page"; import { Watcher } from "./watcher"; -import { writeFile } from "fs/promises"; export type MetaInfo = { errorCount: number; @@ -41,137 +40,7 @@ export type Audit = { pageUrl: string; }; -type RunnerConf = Partial; - -let _log = false; - -/** - * Enable or disable logging. - * @param {Object} [enabled] enabled - Enable console logging. - * @returns {void} Returns void. - */ -export function setLogging(enabled?: boolean): void { - _log = enabled; -} - -/** - * Run accessibility tests for page. - * @param {Object} [config={}] config - Options to change the way tests run. - * @param {Boolean} [preventClose=false] preventClose - Prevent page page from closing on finish. - * @returns {Promise} Returns a promise which resolves with results. - */ -export async function kayle( - o: RunnerConf = {}, - preventClose?: boolean -): Promise { - const navigate = - typeof o.page.url === "function" && - o.page.url() === "about:blank" && - (o.origin || o.html); - - // navigate to a clean page - if (navigate) { - await goToPage( - { page: o.page, html: o.html, timeout: o.timeout }, - o.origin - ); - } else if (!o.noIntercept) { - await setNetworkInterception(o.page); - } - - const config = extractArgs(o); - - const watcher = new Watcher(); - - const results = await Promise.race([ - watcher.watch(config.timeout), - auditPage(config), - ]); - - clearTimeout(watcher.timer); - - !preventClose && navigate && (await o.page.close()); - - return results; -} - -let extractLinks; - -// on autoKayle link find callback -declare function callback(audit: Audit): Audit; -declare function callback(audit: Audit): Promise; - -/** - * Run accessibility tests for page auto running until all pages complete. - * @param {Object} [config={}] config - Options to change the way tests run. - * @returns {Promise} Returns a promise which resolves with array of results. - */ -export async function autoKayle( - o: RunnerConf & { log?: boolean; store?: string; cb?: typeof callback } = {}, - ignoreSet?: Set, - _results?: Audit[] -): Promise { - // pre init list - if (!_results) { - _results = []; - } - - const result = await kayle(o, true); - - _results.push(result); - - if (o.cb && typeof o.cb === "function") { - await o.cb(result); - } - - // auto run links until finished. - if (!extractLinks) { - extractLinks = (await import("./wasm/extract")).extractLinks; - } - - if (!ignoreSet) { - ignoreSet = new Set(); - } - - const links: string[] = await extractLinks(o); - - // persist html file to disk - if (o.store) { - await writeFile( - `${o.store}/${encodeURIComponent(o.page.url())}`, - await o.page.content() - ); - } - - await o.page.close(); - - await Promise.all( - links.map(async (link) => { - if (ignoreSet.has(link)) { - return await Promise.resolve(); - } - - if (_log) { - console.log(`Running: ${link}`); - } - - ignoreSet.add(link); - - return await autoKayle( - { - ...o, - page: await o.browser.newPage(), - html: null, - origin: link, - }, - ignoreSet, - _results - ); - }) - ); - - return _results; -} +export type RunnerConf = Partial; // run accessibility audit async function auditPage(config: RunnerConfig) { @@ -222,3 +91,44 @@ async function audit(config: RunnerConfig): Promise { } ); } + +/** + * Run accessibility tests for page. + * @param {Object} [config={}] config - Options to change the way tests run. + * @param {Boolean} [preventClose=false] preventClose - Prevent page page from closing on finish. + * @returns {Promise} Returns a promise which resolves with results. + */ +export async function kayle( + o: RunnerConf = {}, + preventClose?: boolean +): Promise { + const navigate = + typeof o.page.url === "function" && + o.page.url() === "about:blank" && + (o.origin || o.html); + + // navigate to a clean page + if (navigate) { + await goToPage( + { page: o.page, html: o.html, timeout: o.timeout }, + o.origin + ); + } else if (!o.noIntercept) { + await setNetworkInterception(o.page); + } + + const config = extractArgs(o); + + const watcher = new Watcher(); + + const results = await Promise.race([ + watcher.watch(config.timeout), + auditPage(config), + ]); + + clearTimeout(watcher.timer); + + !preventClose && navigate && (await o.page.close()); + + return results; +} diff --git a/kayle/lib/utils/adblock.ts b/kayle/lib/utils/adblock.ts new file mode 100644 index 00000000..41a2d099 --- /dev/null +++ b/kayle/lib/utils/adblock.ts @@ -0,0 +1,76 @@ +type AdCheck = { + check(url: string, domain: string, resource: string, bool: boolean); +}; + +const engine: unknown | AdCheck = + process.env.KAYLE_ADBLOCK === "true" + ? (async () => { + try { + // @ts-ignore module is not installed by default. + const adblockRust = await import("adblock-rs"); + const { promises } = await import("fs"); + const { join } = await import("path"); + + const filterSet = new adblockRust.FilterSet(false); + + // fetch list of resources and store inside data directory + const resourceList = [ + "https://github.com/brave/adblock-rust/blob/master/data/brave/brave-unbreak.txt", + "https://github.com/brave/adblock-rust/blob/master/data/brave/coin-miners.txt", + "https://github.com/brave/adblock-rust/blob/master/data/uBlockOrigin/unbreak.txt", + ]; + + for (const adlist of resourceList) { + const u = new URL(adlist); + const filePath = u.pathname.split("/"); + const fileName = filePath[filePath.length - 1]; + const file = join(__dirname, "../data/", fileName); + + const fileExists = !!(await promises + .stat(file) + .catch((e) => false)); + + if (!fileExists) { + try { + const req = await fetch(adlist); + const adFilter = await req.text(); + + if (adFilter) { + await promises.writeFile(file, adFilter); + filterSet.addFilters(adFilter.split("\n")); + } + } catch (e) { + console.error(e); + } + return; + } + const adFilter = await promises.readFile(file, { + encoding: "utf-8", + }); + + filterSet.addFilters(adFilter.split("\n")); + } + + const engine = new adblockRust.Engine(filterSet, true); + const serializedArrayBuffer = engine.serializeRaw(); + + console.log( + `Adblock Engine size: ${( + serializedArrayBuffer.byteLength / + 1024 / + 1024 + ).toFixed(2)} MB` + ); + + return engine; + } catch (_) { + // error for now without exiting since feature is opt in + console.error( + "Error: adblock-rs installation missing! Run `npm i adblock-rs` or `yarn add adblock-rs` to start the adblock engine." + ); + } + })() + : null; + +// engine to prevent ads and bad stuff +export const adEngine = engine as AdCheck; diff --git a/kayle/lib/utils/go-to-page.ts b/kayle/lib/utils/go-to-page.ts index edd3a2ab..7670ff80 100644 --- a/kayle/lib/utils/go-to-page.ts +++ b/kayle/lib/utils/go-to-page.ts @@ -1,4 +1,5 @@ import { blockedResourceTypes, skippedResources } from "./resource-ignore"; +import { adEngine } from "./adblock"; import type { RunnerConfig } from "../config"; type Request = { @@ -11,6 +12,7 @@ type NetworkResource = { resourceType: string; request: Request; url: string; + domain?: string; }; /** @@ -20,7 +22,7 @@ type NetworkResource = { * @returns {Promise} Returns a promise void. */ const blocknet = async ( - { resourceType, request, url }: NetworkResource, + { resourceType, request, url, domain }: NetworkResource, allowImage?: boolean ) => { // ignore intercepted request @@ -51,6 +53,16 @@ const blocknet = async ( } } + if ( + // if engine is loaded + adEngine && + typeof adEngine.check === "function" && + (url.startsWith("https://") || url.startsWith("http://")) && + adEngine.check(url, new URL(url).origin, resourceType, true) + ) { + return await request.abort(); + } + return await request.continue(); }; diff --git a/kayle/package.json b/kayle/package.json index f209e23b..c83cde91 100644 --- a/kayle/package.json +++ b/kayle/package.json @@ -1,6 +1,6 @@ { "name": "kayle", - "version": "0.4.20", + "version": "0.4.22", "description": "Extremely fast accessibility evaluation for nodejs", "main": "./build/index.js", "keywords": [ @@ -17,7 +17,7 @@ "scripts": { "prepare": "tsc", "build": "tsc", - "compile:test": "tsc && tsc --project tsconfig.test.json", + "compile:test": "yarn build && tsc --project tsconfig.test.json", "lint": "eslint .", "fix": "prettier --write '**/*.{js,jsx,ts,tsx}'", "bench:playwright:htmlcs": "node _tests/bench/fast_htmlcs-playwright.js",