fingerprintjs
119 строк · 3.5 Кб
1/*
2* See docs/content_blockers.md
3*/
4
5import * as path from 'path'
6import { promises as fsAsync } from 'fs'
7import * as rollup from 'rollup'
8import rollupConfig from '../../rollup.config'
9import filterConfig, { FilterList } from './filters'
10import { fetchFilter } from './utils'
11
12const inputScript = path.join(__dirname, 'selectors_tester.ts')
13const outputFile = path.join(__dirname, 'selectors_tester.html')
14
15run()
16
17async function run() {
18const uniqueSelectors = await fetchUniqueSelectors(filterConfig)
19const testerHtml = await makeTesterHtml(uniqueSelectors)
20await fsAsync.writeFile(outputFile, testerHtml)
21}
22
23async function fetchUniqueSelectors(filterConfig: FilterList) {
24const filters = Object.values(filterConfig)
25const uniqueSelectors = new Set<string>()
26let fetchedFiltersCount = 0
27
28const clearProgress = () => {
29process.stdout.clearLine(0)
30process.stdout.cursorTo(0)
31}
32const printProgress = () => {
33clearProgress()
34process.stdout.write(`Fetching filters: ${fetchedFiltersCount} of ${filters.length}`)
35}
36
37printProgress()
38
39let abort: (() => void) | undefined
40const abortPromise = new Promise<void>((resolve) => (abort = resolve))
41try {
42await Promise.all(
43filters.map(async (filter) => {
44let filterLines: string[]
45try {
46filterLines = await fetchFilter(filter.file, abortPromise)
47} catch (error) {
48throw new Error(`Failed to fetch filter "${filter.title}" (${filter.file}): ${error}`)
49}
50for (const line of filterLines) {
51const selector = getSelectorFromFilterRule(line)
52if (selector) {
53uniqueSelectors.add(selector)
54}
55}
56++fetchedFiltersCount
57printProgress()
58}),
59)
60} finally {
61abort?.()
62clearProgress()
63}
64
65return uniqueSelectors
66}
67
68function getSelectorFromFilterRule(rule: string): string | undefined {
69const selectorMatch = /^##(.+)$/.exec(rule)
70if (!selectorMatch) {
71return
72}
73const selector = selectorMatch[1]
74// Leaves only selectors suitable for `parseSimpleCssSelector` and `offsetParent` usage
75if (/(^embed([^\w-]|$)|\\|\[src.*=|\[style\W?=[^[]*\bposition:\s*fixed\b|\[[^\]]*\[)/i.test(selector)) {
76return
77}
78// Exclude iframes because they produce unwanted side effects
79if (/^iframe([^\w-]|$)/i.test(selector)) {
80return
81}
82const selectorWithoutAttributes = selector.trim().replace(/\[.*?\]/g, '[]')
83if (/[\s:]/.test(selectorWithoutAttributes)) {
84return
85}
86return selector
87}
88
89async function makeTesterHtml(selectors: { forEach: (callback: (selector: string) => void) => void }) {
90const selectorsList: string[] = []
91selectors.forEach((selector) => selectorsList.push(selector))
92const jsCode = await getJsToDetectBlockedSelectors(selectorsList)
93return `<!DOCTYPE html>
94<html>
95<head>
96<meta charset="utf-8" />
97<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
98<meta http-equiv="X-UA-Compatible" content="IE=edge" />
99<title>Selector blockers tester</title>
100</head>
101<body>
102<script>
103${jsCode}
104</script>
105</body>
106</html>`
107}
108
109async function getJsToDetectBlockedSelectors(selectors: readonly string[]) {
110// The first configuration from rollup.config.ts is supposed to make a JS file with dependencies included
111const bundle = await rollup.rollup({
112input: inputScript,
113plugins: rollupConfig[0].plugins,
114})
115const { output } = await bundle.generate({
116format: 'iife',
117})
118return output[0].code.replace(/\[\s*\/\*\s*selectors\s*\*\/\s*]/g, JSON.stringify(selectors))
119}
120