Skip to content

Commit 169eaf3

Browse files
Redesign repo structure and get domains list from multiple sources (#44)
* refactor: design monorepos * ci: fix ESLint chain * ci: expand ESLint checking to all Typescript files * refactor: split IAB sellers downloader * refactor: update pacakge.json in `builder` * fix: `baseUrl` in `tsconfig.json` leads fatal of `tsx` execution * chore: add http-server utils * chore: update .gitignore and package.json for new monorepo structure * Potential fix for code scanning alert no. 4: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * chore: follow PascalCase * Potential fix for code scanning alert no. 9: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * feat: add `IndexAdShieldDomainsFromAG` func to refer AG Base filters list * feat: add checking func to discard all CDN domains owned by Ad-Shield * feat: add `IndexAdShieldDomainsFromUBO` func to refer uBO filters list * feat: add `FetchAdShieldDomains` func to refer AG Base and uBO filters list * chore: remove unnecessary import * ci: run ESLint workflow for all branches * chore: rename `FetchAdShieldDomainsFromFiltersLists` * feat: add `FetchAdShieldDomains` func to refer all references * feat: provide `CustomDefinedMatches` Set * feat: add building cache for caching remote domains * feat: create `CreateBanner` func * fix: path is not resolved correctly if `npm run` is used * feat: use new builder * chore: add `SubscriptionUrl` customization for debugging * fix: chokidar does not built-in glob support * fix: typo --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 76c476e commit 169eaf3

29 files changed

Lines changed: 540 additions & 172 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Check building and linting
22

33
on:
44
push:
5-
branches-ignore: [ "main" ]
5+
branches: [ "**" ]
66
pull_request:
77
# The branches below must be a subset of the branches above
88
branches: [ "**" ]

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
dist
3-
sources/src/#generated-*.ts
3+
sources/src/#generated-*.ts
4+
.buildcache

builder.ts

Lines changed: 0 additions & 117 deletions
This file was deleted.

builder/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@filteringdev/tinyshield-builder",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"lint": "tsc --noEmit && eslint **/*.ts",
7+
"build": "tsx source/buildci.ts",
8+
"debug": "tsx source/debug.ts",
9+
"clean": "rm -rf dist && rm -rf .buildcache"
10+
},
11+
"dependencies": {
12+
"@types/node": "^24.10.9"
13+
},
14+
"devDependencies": {
15+
"@adguard/agtree": "^3.4.3",
16+
"@npmcli/package-json": "^7.0.4",
17+
"@types/npmcli__package-json": "^4.0.4",
18+
"@types/semver": "^7.7.1",
19+
"@typescriptprime/parsing": "^1.0.4",
20+
"@typescriptprime/securereq": "^1.1.0",
21+
"chokidar": "^5.0.0",
22+
"esbuild": "^0.27.2",
23+
"eslint": "^9.39.2",
24+
"semver": "^7.7.3",
25+
"tldts": "^7.0.19",
26+
"tsx": "^4.21.0",
27+
"typescript": "^5.9.3",
28+
"typescript-eslint": "^8.53.0",
29+
"zod": "^4.3.5"
30+
}
31+
}

builder/source/banner/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export interface BannerOptions {
2+
Version: string
3+
BuildType: 'production' | 'development'
4+
Domains: Set<string>
5+
Author: string
6+
Name: string
7+
Namespace: string
8+
HomepageURL: URL
9+
SupportURL: URL
10+
UpdateURL: URL
11+
DownloadURL: URL
12+
License: string
13+
Description: Record<'en' | 'ko' | 'ja' | string, string>
14+
}
15+
16+
export function CreateBanner(Options: BannerOptions): string {
17+
let BannerString: string = '// ==UserScript==\n'
18+
BannerString += `// @name ${Options.BuildType === 'production' ? Options.Name : Options.Name + ' (Development)'}\n`
19+
BannerString += '//\n'
20+
BannerString += `// @namespace ${Options.Namespace}\n`
21+
BannerString += `// @homepageURL ${Options.HomepageURL.href}\n`
22+
BannerString += `// @supportURL ${Options.SupportURL.href}\n`
23+
BannerString += `// @updateURL ${Options.UpdateURL.href}\n`
24+
BannerString += `// @downloadURL ${Options.DownloadURL.href}\n`
25+
BannerString += `// @license ${Options.License}\n`
26+
BannerString += '//\n'
27+
BannerString += `// @version ${Options.Version}\n`
28+
BannerString += `// @author ${Options.Author}\n`
29+
BannerString += '//\n'
30+
BannerString += '// @grant unsafeWindow\n'
31+
BannerString += '// @run-at document-start\n'
32+
BannerString += '//\n'
33+
BannerString += `// @description ${Options.Description['en']}\n`
34+
35+
for (const Key of Object.keys(Options.Description)) {
36+
if (Key === 'en') continue
37+
BannerString += `// @description:${Key} ${Options.Description[Key]}\n`
38+
}
39+
BannerString += '//\n'
40+
41+
for (const Domain of Options.Domains) {
42+
BannerString += `// @match *://${Domain}/*\n`
43+
BannerString += `// @match *://*.${Domain}/*\n`
44+
}
45+
BannerString += '// ==/UserScript==\n\n'
46+
return BannerString
47+
}

builder/source/build.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as ESBuild from 'esbuild'
2+
import * as Zod from 'zod'
3+
import * as Process from 'node:process'
4+
import * as TLDTS from 'tldts'
5+
import PackageJson from '@npmcli/package-json'
6+
import { LoadDomainsFromCache } from './cache.js'
7+
import { FetchAdShieldDomains } from './references/index.js'
8+
import { CustomDefinedMatches } from './references/custom-defined.js'
9+
import { ConvertWildcardSuffixToRegexPattern } from './utils/wildcard-suffix-converter.js'
10+
import { CreateBanner } from './banner/index.js'
11+
12+
export type BuildOptions = {
13+
Minify: boolean
14+
UseCache: boolean
15+
BuildType: 'production' | 'development',
16+
SubscriptionUrl: string,
17+
Version?: string
18+
}
19+
20+
export async function Build(OptionsParam?: BuildOptions): Promise<void> {
21+
const Options = await Zod.strictObject({
22+
Minify: Zod.boolean(),
23+
UseCache: Zod.boolean(),
24+
BuildType: Zod.enum(['production', 'development']),
25+
SubscriptionUrl: Zod.string().transform(Value => new URL(Value)).default(new URL('https://cdn.jsdelivr.net/npm/@filteringdev/tinyshield@latest/dist/tinyShield.user.js')),
26+
Version: Zod.string().optional()
27+
}).parseAsync(OptionsParam)
28+
29+
let MatchingDomains: Set<string> = new Set<string>()
30+
if (Options.UseCache) {
31+
MatchingDomains = await LoadDomainsFromCache()
32+
} else {
33+
MatchingDomains = await FetchAdShieldDomains()
34+
}
35+
CustomDefinedMatches.forEach(Domain => MatchingDomains.add(Domain))
36+
37+
MatchingDomains = new Set<string>([...MatchingDomains].map(Domain => TLDTS.parse(Domain).domain ?? Domain).filter((D): D is string => D !== null))
38+
for (const Domain of MatchingDomains) {
39+
if (Domain.endsWith('.*')) {
40+
MatchingDomains.delete(Domain)
41+
ConvertWildcardSuffixToRegexPattern(Domain).forEach(GeneratedPattern => MatchingDomains.add(GeneratedPattern))
42+
}
43+
}
44+
45+
let ProjectRoot = Process.cwd()
46+
if (Process.cwd().endsWith('/builder')) {
47+
ProjectRoot = Process.cwd() + '/..'
48+
}
49+
50+
const Banner = CreateBanner({
51+
Version: Options.Version ?? (await PackageJson.load(ProjectRoot)).content.version ?? '0.0.0',
52+
BuildType: Options.BuildType ?? 'production',
53+
Domains: MatchingDomains,
54+
Name: 'tinyShield',
55+
Namespace: 'https://github.com/FilteringDev/tinyShield',
56+
DownloadURL: Options.SubscriptionUrl,
57+
UpdateURL: Options.SubscriptionUrl,
58+
HomepageURL: new URL('https://github.com/FilteringDev/tinyShield'),
59+
SupportURL: new URL('https://github.com/FilteringDev/tinyShield/issues'),
60+
License: 'MPL-2.0',
61+
Author: 'PiQuark6046 and contributors',
62+
Description: {
63+
en: 'tinyShield allows AdGuard, uBlock Origin, Brave and ABP to resist against Ad-Shield quickly.',
64+
ko: 'tinyShield는 AdGuard, uBlock Origin, Brave 와 ABP가 애드쉴드에 빠르게 저항할 수 있도록 합니다.',
65+
ja: 'tinyShieldを使うと、AdGuard, uBlock Origin, Brave, およびABPがAd-Shieldに素早く対抗できます。'
66+
}
67+
})
68+
69+
await ESBuild.build({
70+
entryPoints: [ProjectRoot + '/userscript/source/index.ts'],
71+
bundle: true,
72+
minify: Options.Minify,
73+
outfile: `${ProjectRoot}/dist/tinyShield${Options.BuildType === 'development' ? '.dev' : ''}.user.js`,
74+
banner: {
75+
js: Banner
76+
},
77+
target: ['es2024', 'chrome119', 'firefox142', 'safari26']
78+
})
79+
}

builder/source/buildci.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as Zod from 'zod'
2+
import * as Process from 'node:process'
3+
import { PreProcessing, PostProcessing } from '@typescriptprime/parsing'
4+
import { Build, BuildOptions } from './build.js'
5+
6+
let ParsedArgv = (await PostProcessing<BuildOptions>(PreProcessing(Process.argv))).Options
7+
let Options = await Zod.strictObject({
8+
Minify: Zod.string().pipe(Zod.enum(['true', 'false'])).transform(Value => Value === 'true').default(true),
9+
UseCache: Zod.string().pipe(Zod.enum(['true', 'false'])).transform(Value => Value === 'true').default(true),
10+
BuildType: Zod.enum(['production', 'development']),
11+
SubscriptionUrl: Zod.string()
12+
}).parseAsync(ParsedArgv)
13+
14+
await Build(Options)

builder/source/cache.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as Zod from 'zod'
2+
import * as Fs from 'node:fs'
3+
import * as Process from 'node:process'
4+
import { FetchAdShieldDomains } from './references/index.js'
5+
6+
const CachePath = Process.cwd() + '/.buildcache'
7+
const CacheDomainsPath = CachePath + '/domains.json'
8+
9+
export function CreateCache(Domains: Set<string>) {
10+
if (!Fs.existsSync(CachePath)) {
11+
Fs.mkdirSync(CachePath)
12+
} else if (!Fs.statSync(CachePath).isDirectory()) {
13+
throw new Error('.buildcache exists and is not a directory!')
14+
}
15+
if (Fs.existsSync(CacheDomainsPath)) {
16+
throw new Error('Cache already exists!')
17+
}
18+
Fs.writeFileSync(CacheDomainsPath, JSON.stringify([...Domains], null, 2), { encoding: 'utf-8' })
19+
}
20+
21+
export async function LoadCache(): Promise<Set<string>> {
22+
if (!Fs.existsSync(CacheDomainsPath)) {
23+
throw new Error('Cache does not exist!')
24+
}
25+
const DomainsRaw = Fs.readFileSync(CacheDomainsPath, { encoding: 'utf-8' })
26+
const DomainsArray: string[] = JSON.parse(DomainsRaw)
27+
await Zod.array(Zod.string().refine((Value) => {
28+
try {
29+
new URLPattern(`https://${Value}/`)
30+
return true
31+
} catch {
32+
return false
33+
}
34+
})).parseAsync(DomainsArray)
35+
return new Set(DomainsArray)
36+
}
37+
38+
export async function LoadDomainsFromCache(): Promise<Set<string>> {
39+
if (!Fs.existsSync(CacheDomainsPath)) {
40+
const Domains = await FetchAdShieldDomains()
41+
CreateCache(Domains)
42+
return Domains
43+
} else {
44+
return await LoadCache()
45+
}
46+
}

0 commit comments

Comments
 (0)