diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0cb716a..8781b98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Check building and linting on: push: - branches-ignore: [ "main" ] + branches: [ "**" ] pull_request: # The branches below must be a subset of the branches above branches: [ "**" ] diff --git a/.gitignore b/.gitignore index c2d28d0..c7a2ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist -sources/src/#generated-*.ts \ No newline at end of file +sources/src/#generated-*.ts +.buildcache \ No newline at end of file diff --git a/builder.ts b/builder.ts deleted file mode 100644 index 478665b..0000000 --- a/builder.ts +++ /dev/null @@ -1,117 +0,0 @@ -// This is a build script temporarily. -// If development of https://github.com/TypescriptPrime/userscript-build-toolkit is completed, -// this script will be replaced by that package. - -import * as ESBuild from 'esbuild' -import * as Zod from 'zod' -import PackageJson from '@npmcli/package-json' -import * as Semver from 'semver' -import * as Fs from 'node:fs' -import * as NodeHttps from 'node:https' -import * as Process from 'node:process' - -let BuildType: 'production' | 'development' = Zod.string().refine(BT => BT === 'production' || BT === 'development').default('production').parse(Process.argv[3] ?? 'production') -console.log('Build type set to:', BuildType) - -let Version: string = (await PackageJson.load('./')).content.version -Version = await Zod.string().refine(V => Semver.valid(V) !== null).parseAsync(Version) -console.log('Applying version value:', Version) - -let DomainsList: Set = new Set() - -const IABSellersJsonURL = 'https://info.ad-shield.io/sellers.json' -const IABSellersJsonResponse: { StatusCode: number, Headers: Record, Body: string } = await new Promise((Resolve, Reject) => { - const IABSellersJsonReq = NodeHttps.get({ - hostname: new URL(IABSellersJsonURL).hostname, - path: new URL(IABSellersJsonURL).pathname, - headers: { - 'user-agent': 'node/v24.12.0 linux x64 workspaces/true' - }, - ciphers: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256', - ecdhCurve: 'X25519MLKEM768', - minVersion: 'TLSv1.3' - }, (Res) => { - const Chunks: Buffer[] = [] - Res.on('data', (Chunk: Buffer) => Chunks.push(Chunk)) - Res.on('end', () => { - Resolve({ - StatusCode: Res.statusCode, - Headers: Res.headers, - Body: new TextDecoder().decode(Buffer.concat(Chunks)) - }) - }) - IABSellersJsonReq.on('error', (Err) => Reject(Err)) - }) -}) -console.log('Fetched IAB Sellers.json with status code:', IABSellersJsonResponse.StatusCode) -let IABSellersJsonData = JSON.parse(IABSellersJsonResponse.Body) as { - // eslint-disable-next-line @typescript-eslint/naming-convention - sellers: Array<{ - // eslint-disable-next-line @typescript-eslint/naming-convention - seller_id: number, - // eslint-disable-next-line @typescript-eslint/naming-convention - seller_type: string, - // eslint-disable-next-line @typescript-eslint/naming-convention - name: string, - // eslint-disable-next-line @typescript-eslint/naming-convention - domain: string - }> -} -IABSellersJsonData = await Zod.object({ - sellers: Zod.array(Zod.object({ - seller_id: Zod.number(), - seller_type: Zod.string(), - name: Zod.string(), - domain: Zod.string().refine(D => { - try { - new URL(`https://${D}`) - } catch { - return false - } - return true - }) - })) -}).parseAsync(IABSellersJsonData) -console.log('Validated IAB Sellers.json data') -for (const SellerEntry of IABSellersJsonData.sellers) { - DomainsList.add(SellerEntry.domain) -} -console.log('Collected', DomainsList.size, 'unique domains from IAB Sellers.json') - -const HeaderLocation = './sources/banner.txt' -let ConvertedHeader: string = '' -for (const Line of Fs.readFileSync(HeaderLocation, 'utf-8').split('\n')) { - if (Line.includes('%%VERSION_VALUE%%')) { - ConvertedHeader += Line.replaceAll('%%VERSION_VALUE%%', Version) + '\n' - } else if (Line.includes('%%NAME%%')) { - ConvertedHeader += Line.replaceAll('%%NAME%%', BuildType === 'production' ? 'tinyShield' : 'tinyShield (Development)') + '\n' - } else if (Line === '%%DOMAIN_INJECTION%%') { - for (const DomainEntry of DomainsList) { - ConvertedHeader += `// @match *://${DomainEntry}/*\n` - ConvertedHeader += `// @match *://*.${DomainEntry}/*\n` - } - } else { - ConvertedHeader += Line + '\n' - } -} -console.log('Generated header with domain injections and processing') -let AttachHeaderPath = `/tmp/${crypto.randomUUID()}` -Fs.writeFileSync(AttachHeaderPath, ConvertedHeader, { encoding: 'utf-8', mode: 0o700 }) -console.log('Written temporary header file to:', AttachHeaderPath) -await ESBuild.build({ - entryPoints: ['./sources/src/index.ts'], - bundle: true, - minify: BuildType === 'production', - define: { - global: 'window' - }, - inject: ['./sources/esbuild.inject.ts'], - banner: { - js: Fs.readFileSync(AttachHeaderPath, 'utf-8') - }, - target: ['es2024', 'chrome119', 'firefox142', 'safari26'], - outfile: BuildType === 'production' ? './dist/tinyShield.user.js' : './dist/tinyShield.dev.user.js', -}) -console.log('Build completed') -Fs.rmSync(AttachHeaderPath) -console.log('Temporary header file removed') \ No newline at end of file diff --git a/builder/package.json b/builder/package.json new file mode 100644 index 0000000..9e32c7c --- /dev/null +++ b/builder/package.json @@ -0,0 +1,31 @@ +{ + "name": "@filteringdev/tinyshield-builder", + "private": true, + "type": "module", + "scripts": { + "lint": "tsc --noEmit && eslint **/*.ts", + "build": "tsx source/buildci.ts", + "debug": "tsx source/debug.ts", + "clean": "rm -rf dist && rm -rf .buildcache" + }, + "dependencies": { + "@types/node": "^24.10.9" + }, + "devDependencies": { + "@adguard/agtree": "^3.4.3", + "@npmcli/package-json": "^7.0.4", + "@types/npmcli__package-json": "^4.0.4", + "@types/semver": "^7.7.1", + "@typescriptprime/parsing": "^1.0.4", + "@typescriptprime/securereq": "^1.1.0", + "chokidar": "^5.0.0", + "esbuild": "^0.27.2", + "eslint": "^9.39.2", + "semver": "^7.7.3", + "tldts": "^7.0.19", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.0", + "zod": "^4.3.5" + } +} diff --git a/builder/source/banner/index.ts b/builder/source/banner/index.ts new file mode 100644 index 0000000..89cc27a --- /dev/null +++ b/builder/source/banner/index.ts @@ -0,0 +1,47 @@ +export interface BannerOptions { + Version: string + BuildType: 'production' | 'development' + Domains: Set + Author: string + Name: string + Namespace: string + HomepageURL: URL + SupportURL: URL + UpdateURL: URL + DownloadURL: URL + License: string + Description: Record<'en' | 'ko' | 'ja' | string, string> +} + +export function CreateBanner(Options: BannerOptions): string { + let BannerString: string = '// ==UserScript==\n' + BannerString += `// @name ${Options.BuildType === 'production' ? Options.Name : Options.Name + ' (Development)'}\n` + BannerString += '//\n' + BannerString += `// @namespace ${Options.Namespace}\n` + BannerString += `// @homepageURL ${Options.HomepageURL.href}\n` + BannerString += `// @supportURL ${Options.SupportURL.href}\n` + BannerString += `// @updateURL ${Options.UpdateURL.href}\n` + BannerString += `// @downloadURL ${Options.DownloadURL.href}\n` + BannerString += `// @license ${Options.License}\n` + BannerString += '//\n' + BannerString += `// @version ${Options.Version}\n` + BannerString += `// @author ${Options.Author}\n` + BannerString += '//\n' + BannerString += '// @grant unsafeWindow\n' + BannerString += '// @run-at document-start\n' + BannerString += '//\n' + BannerString += `// @description ${Options.Description['en']}\n` + + for (const Key of Object.keys(Options.Description)) { + if (Key === 'en') continue + BannerString += `// @description:${Key} ${Options.Description[Key]}\n` + } + BannerString += '//\n' + + for (const Domain of Options.Domains) { + BannerString += `// @match *://${Domain}/*\n` + BannerString += `// @match *://*.${Domain}/*\n` + } + BannerString += '// ==/UserScript==\n\n' + return BannerString +} \ No newline at end of file diff --git a/builder/source/build.ts b/builder/source/build.ts new file mode 100644 index 0000000..83c0c33 --- /dev/null +++ b/builder/source/build.ts @@ -0,0 +1,79 @@ +import * as ESBuild from 'esbuild' +import * as Zod from 'zod' +import * as Process from 'node:process' +import * as TLDTS from 'tldts' +import PackageJson from '@npmcli/package-json' +import { LoadDomainsFromCache } from './cache.js' +import { FetchAdShieldDomains } from './references/index.js' +import { CustomDefinedMatches } from './references/custom-defined.js' +import { ConvertWildcardSuffixToRegexPattern } from './utils/wildcard-suffix-converter.js' +import { CreateBanner } from './banner/index.js' + +export type BuildOptions = { + Minify: boolean + UseCache: boolean + BuildType: 'production' | 'development', + SubscriptionUrl: string, + Version?: string +} + +export async function Build(OptionsParam?: BuildOptions): Promise { + const Options = await Zod.strictObject({ + Minify: Zod.boolean(), + UseCache: Zod.boolean(), + BuildType: Zod.enum(['production', 'development']), + SubscriptionUrl: Zod.string().transform(Value => new URL(Value)).default(new URL('https://cdn.jsdelivr.net/npm/@filteringdev/tinyshield@latest/dist/tinyShield.user.js')), + Version: Zod.string().optional() + }).parseAsync(OptionsParam) + + let MatchingDomains: Set = new Set() + if (Options.UseCache) { + MatchingDomains = await LoadDomainsFromCache() + } else { + MatchingDomains = await FetchAdShieldDomains() + } + CustomDefinedMatches.forEach(Domain => MatchingDomains.add(Domain)) + + MatchingDomains = new Set([...MatchingDomains].map(Domain => TLDTS.parse(Domain).domain ?? Domain).filter((D): D is string => D !== null)) + for (const Domain of MatchingDomains) { + if (Domain.endsWith('.*')) { + MatchingDomains.delete(Domain) + ConvertWildcardSuffixToRegexPattern(Domain).forEach(GeneratedPattern => MatchingDomains.add(GeneratedPattern)) + } + } + + let ProjectRoot = Process.cwd() + if (Process.cwd().endsWith('/builder')) { + ProjectRoot = Process.cwd() + '/..' + } + + const Banner = CreateBanner({ + Version: Options.Version ?? (await PackageJson.load(ProjectRoot)).content.version ?? '0.0.0', + BuildType: Options.BuildType ?? 'production', + Domains: MatchingDomains, + Name: 'tinyShield', + Namespace: 'https://github.com/FilteringDev/tinyShield', + DownloadURL: Options.SubscriptionUrl, + UpdateURL: Options.SubscriptionUrl, + HomepageURL: new URL('https://github.com/FilteringDev/tinyShield'), + SupportURL: new URL('https://github.com/FilteringDev/tinyShield/issues'), + License: 'MPL-2.0', + Author: 'PiQuark6046 and contributors', + Description: { + en: 'tinyShield allows AdGuard, uBlock Origin, Brave and ABP to resist against Ad-Shield quickly.', + ko: 'tinyShield는 AdGuard, uBlock Origin, Brave 와 ABP가 애드쉴드에 빠르게 저항할 수 있도록 합니다.', + ja: 'tinyShieldを使うと、AdGuard, uBlock Origin, Brave, およびABPがAd-Shieldに素早く対抗できます。' + } + }) + + await ESBuild.build({ + entryPoints: [ProjectRoot + '/userscript/source/index.ts'], + bundle: true, + minify: Options.Minify, + outfile: `${ProjectRoot}/dist/tinyShield${Options.BuildType === 'development' ? '.dev' : ''}.user.js`, + banner: { + js: Banner + }, + target: ['es2024', 'chrome119', 'firefox142', 'safari26'] + }) +} \ No newline at end of file diff --git a/builder/source/buildci.ts b/builder/source/buildci.ts new file mode 100644 index 0000000..07d4cd3 --- /dev/null +++ b/builder/source/buildci.ts @@ -0,0 +1,14 @@ +import * as Zod from 'zod' +import * as Process from 'node:process' +import { PreProcessing, PostProcessing } from '@typescriptprime/parsing' +import { Build, BuildOptions } from './build.js' + +let ParsedArgv = (await PostProcessing(PreProcessing(Process.argv))).Options +let Options = await Zod.strictObject({ + Minify: Zod.string().pipe(Zod.enum(['true', 'false'])).transform(Value => Value === 'true').default(true), + UseCache: Zod.string().pipe(Zod.enum(['true', 'false'])).transform(Value => Value === 'true').default(true), + BuildType: Zod.enum(['production', 'development']), + SubscriptionUrl: Zod.string() +}).parseAsync(ParsedArgv) + +await Build(Options) \ No newline at end of file diff --git a/builder/source/cache.ts b/builder/source/cache.ts new file mode 100644 index 0000000..bfb09d7 --- /dev/null +++ b/builder/source/cache.ts @@ -0,0 +1,46 @@ +import * as Zod from 'zod' +import * as Fs from 'node:fs' +import * as Process from 'node:process' +import { FetchAdShieldDomains } from './references/index.js' + +const CachePath = Process.cwd() + '/.buildcache' +const CacheDomainsPath = CachePath + '/domains.json' + +export function CreateCache(Domains: Set) { + if (!Fs.existsSync(CachePath)) { + Fs.mkdirSync(CachePath) + } else if (!Fs.statSync(CachePath).isDirectory()) { + throw new Error('.buildcache exists and is not a directory!') + } + if (Fs.existsSync(CacheDomainsPath)) { + throw new Error('Cache already exists!') + } + Fs.writeFileSync(CacheDomainsPath, JSON.stringify([...Domains], null, 2), { encoding: 'utf-8' }) +} + +export async function LoadCache(): Promise> { + if (!Fs.existsSync(CacheDomainsPath)) { + throw new Error('Cache does not exist!') + } + const DomainsRaw = Fs.readFileSync(CacheDomainsPath, { encoding: 'utf-8' }) + const DomainsArray: string[] = JSON.parse(DomainsRaw) + await Zod.array(Zod.string().refine((Value) => { + try { + new URLPattern(`https://${Value}/`) + return true + } catch { + return false + } + })).parseAsync(DomainsArray) + return new Set(DomainsArray) +} + +export async function LoadDomainsFromCache(): Promise> { + if (!Fs.existsSync(CacheDomainsPath)) { + const Domains = await FetchAdShieldDomains() + CreateCache(Domains) + return Domains + } else { + return await LoadCache() + } +} \ No newline at end of file diff --git a/builder/source/debug.ts b/builder/source/debug.ts new file mode 100644 index 0000000..494b042 --- /dev/null +++ b/builder/source/debug.ts @@ -0,0 +1,39 @@ + +import * as Chokidar from 'chokidar' +import * as Process from 'node:process' +import * as Crypto from 'node:crypto' +import * as Fs from 'node:fs' +import { RunDebugServer } from './utils/http-server.js' +import { Build } from './build.js' + +let ProjectRoot = Process.cwd() +if (Process.cwd().endsWith('/builder')) { + ProjectRoot = Process.cwd().replaceAll(/\/builder$/g, '') +} +const WatchingGlob = []; +['builder/', 'userscript/', ''].forEach(Dir => { + WatchingGlob.push(...Fs.globSync(`${ProjectRoot}/${Dir}source/**/*.ts`)) + WatchingGlob.push(...Fs.globSync(`${ProjectRoot}/${Dir}source/**/*.json`)) + WatchingGlob.push(...Fs.globSync(`${ProjectRoot}/${Dir}source/**/*.txt`)) +}) +const Watcher = Chokidar.watch([...WatchingGlob], { + ignored: '**/node_modules/**', +}) + +let BuildCooldownTimer: NodeJS.Timeout = null +let ShouldPreventHTTPResponse = false +let Version: number = 0 +Watcher.on('all', async (WatcherEvent, WatcherPath) => { + clearTimeout(BuildCooldownTimer) + BuildCooldownTimer = setTimeout(async () => { + console.log(`Detected file change (${WatcherEvent}):`, WatcherPath) + ShouldPreventHTTPResponse = true + await Build({ Version: `0.0.${Version}`, Minify: false, UseCache: true, BuildType: 'development', SubscriptionUrl: `http://localhost:${RandomPort}/tinyShield.dev.user.js` }) + Version++ + ShouldPreventHTTPResponse = false + }, 1500) +}) + +let RandomPort = Crypto.randomInt(8000, 8999) +RunDebugServer(RandomPort, ['tinyShield.dev.user.js'], ShouldPreventHTTPResponse) +console.log(`Debug HTTP server running on http://localhost:${RandomPort}/tinyShield.dev.user.js`) \ No newline at end of file diff --git a/builder/source/references/custom-defined.ts b/builder/source/references/custom-defined.ts new file mode 100644 index 0000000..1f10b78 --- /dev/null +++ b/builder/source/references/custom-defined.ts @@ -0,0 +1 @@ +export const CustomDefinedMatches: Set = new Set() \ No newline at end of file diff --git a/builder/source/references/filterslists.ts b/builder/source/references/filterslists.ts new file mode 100644 index 0000000..7189599 --- /dev/null +++ b/builder/source/references/filterslists.ts @@ -0,0 +1,12 @@ +import { IndexAdShieldDomainsFromAG } from './filterslists/ADG.js' +import { IndexAdShieldDomainsFromUBO } from './filterslists/uBO.js' + +export async function FetchAdShieldDomainsFromFiltersLists(): Promise> { + const [AGDomains, UBODomains] = await Promise.all([ + IndexAdShieldDomainsFromAG(), + IndexAdShieldDomainsFromUBO() + ]) + + const CombinedDomains = new Set([...AGDomains, ...UBODomains]) + return CombinedDomains +} \ No newline at end of file diff --git a/builder/source/references/filterslists/ADG.ts b/builder/source/references/filterslists/ADG.ts new file mode 100644 index 0000000..3b3bcb9 --- /dev/null +++ b/builder/source/references/filterslists/ADG.ts @@ -0,0 +1,51 @@ +import { HTTPS2Request } from '@typescriptprime/securereq' +import * as AGTree from '@adguard/agtree' +import { AdShieldCDNDomains } from './keywords.js' + +const AGBaseFilterListSpecificURL = 'https://adguardteam.github.io/AdguardFilters/BaseFilter/sections/specific.txt' +const AGBaseFilterListAdShieldKeys = { + Starting: 'START: Ad-Shield ad reinsertion', + Ending: 'END: Ad-Shield ad reinsertion' +} + +export async function IndexAdShieldDomainsFromAG(): Promise> { + const FiltersListContent = await HTTPS2Request(new URL(AGBaseFilterListSpecificURL), { ExpectedAs: 'String' }) + const AGTreeFiltersList = AGTree.FilterListParser.parse(FiltersListContent.Body) + let StartingLine = -1 + let EndingLine = -1 + for (const [Index, Filter] of AGTreeFiltersList.children.entries()) { + if (Filter.category === 'Comment' && typeof Filter.raws.text === 'string' && Filter.raws.text.includes(AGBaseFilterListAdShieldKeys.Starting)) { + StartingLine = Index + } else if (Filter.category === 'Comment' && typeof Filter.raws.text === 'string' && Filter.raws.text.includes(AGBaseFilterListAdShieldKeys.Ending)) { + EndingLine = Index + } else if (StartingLine !== -1 && EndingLine !== -1) { + break + } else if (Index === AGTreeFiltersList.children.length - 1) { + throw new Error('Could not find Ad-Shield ad reinsertion section in ' + AGBaseFilterListSpecificURL) + } + } + const AdShieldFilters = AGTreeFiltersList.children.filter((Filter, Index) => Index > StartingLine && Index < EndingLine) + + const AdShieldDomains = new Set() + for (const Filter of AdShieldFilters) { + if (Filter.category === 'Cosmetic' && Filter.type === 'ScriptletInjectionRule') { + Filter.domains.children.forEach(Domain => AdShieldDomains.add(Domain.value)) + } else if (Filter.category === 'Cosmetic' && Filter.type === 'JsInjectionRule') { + Filter.domains.children.forEach(Domain => AdShieldDomains.add(Domain.value)) + } else if (Filter.category === 'Network' && Filter.type === 'NetworkRule' && typeof Filter.modifiers !== 'undefined' && Filter.modifiers.children.some(M => M.name.value === 'domain')) { + let DomainValue = Filter.modifiers.children.find(M => M.name.value === 'domain').value.value + DomainValue.split('|').forEach(Domain => AdShieldDomains.add(Domain)) + } + } + + let FilteredDomains = [...AdShieldDomains].filter(Domain => { + try { + new URLPattern(`https://${Domain}/`) + } catch { + return false + } + return !AdShieldCDNDomains.has(Domain) + }) + + return new Set(FilteredDomains) +} \ No newline at end of file diff --git a/builder/source/references/filterslists/keywords.ts b/builder/source/references/filterslists/keywords.ts new file mode 100644 index 0000000..d3de966 --- /dev/null +++ b/builder/source/references/filterslists/keywords.ts @@ -0,0 +1,13 @@ +import { HTTPS2Request } from '@typescriptprime/securereq' + +export const AdShieldCDNDomains: Set = new Set([ + 'html-load.com', + 'css-load.com', + 'ads.adthrive.com' +]) + +export async function IsAdShieldCDNDomain(Domain: string): Promise { + const AdShieldCDNCheckResponse = await HTTPS2Request(new URL(`https://${Domain}/`), { ExpectedAs: 'String' }).catch(() => false) + return typeof AdShieldCDNCheckResponse !== 'boolean' && AdShieldCDNCheckResponse.StatusCode === 200 && + AdShieldCDNCheckResponse.Body.includes('This domain is a part of the Ad-Shield (ad-shield.io) platform,') +} \ No newline at end of file diff --git a/builder/source/references/filterslists/uBO.ts b/builder/source/references/filterslists/uBO.ts new file mode 100644 index 0000000..1307063 --- /dev/null +++ b/builder/source/references/filterslists/uBO.ts @@ -0,0 +1,51 @@ +import { HTTPS2Request } from '@typescriptprime/securereq' +import * as AGTree from '@adguard/agtree' +import { AdShieldCDNDomains } from './keywords.js' + +const UBOFilterListSpecificURL = 'https://ublockorigin.github.io/uAssets/filters/filters.min.txt' +const UBOFilterListAdShieldKeys = { + Starting: '! SECTION: Ad-Shield', + Ending: '! !SECTION: Ad-Shield' +} + +export async function IndexAdShieldDomainsFromUBO(): Promise> { + const FiltersListContent = await HTTPS2Request(new URL(UBOFilterListSpecificURL), { ExpectedAs: 'String' }) + const AGTreeFiltersList = AGTree.FilterListParser.parse(FiltersListContent.Body) + let StartingLine = -1 + let EndingLine = -1 + for (const [Index, Filter] of AGTreeFiltersList.children.entries()) { + if (Filter.category === 'Comment' && typeof Filter.raws.text === 'string' && Filter.raws.text.includes(UBOFilterListAdShieldKeys.Starting)) { + StartingLine = Index + } else if (Filter.category === 'Comment' && typeof Filter.raws.text === 'string' && Filter.raws.text.includes(UBOFilterListAdShieldKeys.Ending)) { + EndingLine = Index + } else if (StartingLine !== -1 && EndingLine !== -1) { + break + } else if (Index === AGTreeFiltersList.children.length - 1) { + throw new Error('Could not find Ad-Shield ad reinsertion section in ' + UBOFilterListSpecificURL) + } + } + const AdShieldFilters = AGTreeFiltersList.children.filter((Filter, Index) => Index > StartingLine && Index < EndingLine) + + const AdShieldDomains = new Set() + for (const Filter of AdShieldFilters) { + if (Filter.category === 'Cosmetic' && Filter.type === 'ScriptletInjectionRule') { + Filter.domains.children.forEach(Domain => AdShieldDomains.add(Domain.value)) + } else if (Filter.category === 'Cosmetic' && Filter.type === 'JsInjectionRule') { + Filter.domains.children.forEach(Domain => AdShieldDomains.add(Domain.value)) + } else if (Filter.category === 'Network' && Filter.type === 'NetworkRule' && typeof Filter.modifiers !== 'undefined' && Filter.modifiers.children.some(M => M.name.value === 'domain')) { + let DomainValue = Filter.modifiers.children.find(M => M.name.value === 'domain').value.value + DomainValue.split('|').forEach(Domain => AdShieldDomains.add(Domain)) + } + } + + let FilteredDomains = [...AdShieldDomains].filter(Domain => { + try { + new URLPattern(`https://${Domain}/`) + } catch { + return false + } + return !AdShieldCDNDomains.has(Domain) + }) + + return new Set(FilteredDomains) +} \ No newline at end of file diff --git a/builder/source/references/iabsellers.ts b/builder/source/references/iabsellers.ts new file mode 100644 index 0000000..cbfab0e --- /dev/null +++ b/builder/source/references/iabsellers.ts @@ -0,0 +1,38 @@ +import * as Zod from 'zod' +import { HTTPSRequest } from '@typescriptprime/securereq' + + +const IABSellersJsonURL = 'https://info.ad-shield.io/sellers.json' + +export async function FetchIABSellersJsonData(): Promise { + const IABSellersJsonResponse: { StatusCode: number, Headers: Record, Body: unknown } = await HTTPSRequest(new URL(IABSellersJsonURL), { ExpectedAs: 'JSON' }) + let IABSellersJsonData =IABSellersJsonResponse.Body as { + // eslint-disable-next-line @typescript-eslint/naming-convention + sellers: Array<{ + // eslint-disable-next-line @typescript-eslint/naming-convention + seller_id: number, + // eslint-disable-next-line @typescript-eslint/naming-convention + seller_type: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + name: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + domain: string + }> + } + IABSellersJsonData = await Zod.object({ + sellers: Zod.array(Zod.object({ + seller_id: Zod.number(), + seller_type: Zod.string(), + name: Zod.string(), + domain: Zod.string().refine(D => { + try { + new URL(`https://${D}`) + } catch { + return false + } + return true + }) + })) + }).parseAsync(IABSellersJsonData) + return [...new Set(IABSellersJsonData.sellers.map(S => S.domain))] +} \ No newline at end of file diff --git a/builder/source/references/index.ts b/builder/source/references/index.ts new file mode 100644 index 0000000..c0c47ec --- /dev/null +++ b/builder/source/references/index.ts @@ -0,0 +1,12 @@ +import { FetchAdShieldDomainsFromFiltersLists } from './filterslists.js' +import { FetchIABSellersJsonData } from './iabsellers.js' + +export async function FetchAdShieldDomains(): Promise> { + const [IABSellersDomains, FiltersListsDomains] = await Promise.all([ + FetchIABSellersJsonData(), + FetchAdShieldDomainsFromFiltersLists() + ]) + + const CombinedDomains = new Set([...IABSellersDomains, ...FiltersListsDomains]) + return CombinedDomains +} \ No newline at end of file diff --git a/builder/source/utils/http-server.ts b/builder/source/utils/http-server.ts new file mode 100644 index 0000000..812d86f --- /dev/null +++ b/builder/source/utils/http-server.ts @@ -0,0 +1,50 @@ +import * as HTTP from 'node:http' +import * as Fs from 'node:fs' +import * as Process from 'node:process' +import * as Path from 'node:path' + +function IsLoopBack(IP: string) { + return IP === '127.0.0.1' || IP === '::1' || IP === '::ffff:127.0.0.1' +} + +export function RunDebugServer(Port: number, FileName: string[], ShouldPreventHTTPResponse: boolean) { + const HTTPServer = HTTP.createServer((Req, Res) => { + let ProjectRoot = Process.cwd() + if (Process.cwd().endsWith('/builder')) { + ProjectRoot = Process.cwd() + '/..' + } + const RequestPath = Req.url?.substring(1) || '' + const ResolvedPath = Path.resolve(ProjectRoot + '/dist', RequestPath) + const RelativePath = Path.relative(ProjectRoot + '/dist', ResolvedPath) + + // Ensure the resolved path stays within the dist root to prevent directory traversal + if (RelativePath.startsWith('..') || Path.isAbsolute(RelativePath)) { + Res.writeHead(403) + Res.end() + return + } + + if (!FileName.includes(RequestPath)) { + Res.writeHead(404) + Res.end() + return + } else if (!IsLoopBack(Req.socket.remoteAddress)) { + Res.writeHead(403) + Res.end() + return + } else if (ShouldPreventHTTPResponse || !Fs.existsSync(ResolvedPath)) { + Res.writeHead(503) + Res.end('File not built yet.') + return + } + + const Content = Fs.readFileSync(ResolvedPath, 'utf-8') + Res.writeHead(200, { + 'content-type': 'application/javascript; charset=utf-8', + 'content-length': new TextEncoder().encode(Content).byteLength.toString() + }) + Res.end(Content) + }) + + HTTPServer.listen(Port) +} \ No newline at end of file diff --git a/builder/source/utils/wildcard-suffix-converter.ts b/builder/source/utils/wildcard-suffix-converter.ts new file mode 100644 index 0000000..3ecb00a --- /dev/null +++ b/builder/source/utils/wildcard-suffix-converter.ts @@ -0,0 +1,12 @@ +const PublicSuffixList = [ + 'com', 'org', 'co', 'de', 'ru', 'fr', 'me', 'it', 'nl', 'io', 'cc', 'in', 'pl', 'xyz', 'es', 'se', 'uk', 'tv', 'info', + 'site', 'us', 'online', 'ch', 'at', 'eu', 'top', 'be', 'cz', 'app', 'ca', 'to', 'jp', 'dev', 'kr' +] + +export function ConvertWildcardSuffixToRegexPattern(Domain: string): string[] { + const Result: string[] = [] + PublicSuffixList.forEach(Suffix => { + Result.push(Domain.replaceAll(/\.\*$/g, '.' + Suffix)) + }) + return Result +} \ No newline at end of file diff --git a/builder/tsconfig.json b/builder/tsconfig.json new file mode 100644 index 0000000..bcb6707 --- /dev/null +++ b/builder/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "source/**/*.ts", + "test/**/*.ts" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 170f47a..254eacc 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,11 @@ "description": "", "type": "module", "scripts": { - "build:interface": "esbuild sources/interface.ts --bundle --format=esm --splitting --sourcemap --target=es2024 --external:/node_modules --outdir=dist && tsc sources/interface.ts --outDir dist/types --declaration --emitDeclarationOnly", - "build:userscript": "tsx builder.ts -- production", + "build:interface": "esbuild userscript/source/interface.ts --bundle --format=esm --splitting --sourcemap --target=es2024 --external:/node_modules --outdir=dist && tsc userscript/source/interface.ts --outDir dist/types --declaration --emitDeclarationOnly --skipLibCheck", + "build:userscript": "npm run build -w builder -- --minify true --use-cache false --build-type production --SubscriptionUrl https://cdn.jsdelivr.net/npm/@filteringdev/tinyshield@latest/dist/tinyShield.user.js", "build": "npm run build:interface && npm run build:userscript", - "debug": "tsx builder.ts -- development", - "lint": "tsc --noEmit && eslint sources/**/*.ts" + "debug": "npm run debug -w builder", + "lint": "npm run lint -w builder && npm run lint -w userscript" }, "keywords": [ "Ad-Shield" @@ -28,20 +28,9 @@ "url": "git+https://github.com/FilteringDev/tinyShield.git" }, "license": "MPL-2.0", - "dependencies": { - "@types/node": "^24.9.2" - }, - "devDependencies": { - "@npmcli/package-json": "^7.0.4", - "@types/npmcli__package-json": "^4.0.4", - "@types/semver": "^7.7.1", - "esbuild": "^0.27.0", - "eslint": "^9.38.0", - "semver": "^7.7.3", - "tsx": "^4.21.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.46.2", - "zod": "^4.3.5" - }, - "packageManager": "npm@11.5.1+" + "packageManager": "npm@11.5.1+", + "workspaces": [ + "userscript", + "builder" + ] } diff --git a/sources/banner.txt b/sources/banner.txt deleted file mode 100644 index ebafe48..0000000 --- a/sources/banner.txt +++ /dev/null @@ -1,22 +0,0 @@ -// ==UserScript== -// @name %%NAME%% -// @encoding utf-8 -// @namespace https://github.com/FilteringDev/tinyShield -// @homepageURL https://github.com/FilteringDev/tinyShield -// @supportURL https://github.com/FilteringDev/tinyShield/issues -// @updateURL https://cdn.jsdelivr.net/npm/@filteringdev/tinyshield@latest/dist/tinyShield.user.js -// @downloadURL https://cdn.jsdelivr.net/npm/@filteringdev/tinyshield@latest/dist/tinyShield.user.js -// @license MIT -// -// @version %%VERSION_VALUE%% -// @author PiQuark6046 and contributors -// -%%DOMAIN_INJECTION%% -// -// @description tinyShield allows AdGuard, uBlock Origin, Brave and ABP to resist against Ad-Shield quickly. -// @description:ko tinyShield는 AdGuard, uBlock Origin, Brave 와 ABP가 애드쉴드에 빠르게 저항할 수 있도록 합니다. -// @description:ja tinyShieldを使うと、AdGuard, uBlock Origin, Brave, およびABPがAd-Shieldに素早く対抗できます。 -// -// @grant unsafeWindow -// @run-at document-start -// ==/UserScript== diff --git a/sources/esbuild.inject.ts b/sources/esbuild.inject.ts deleted file mode 100644 index 02b2d13..0000000 --- a/sources/esbuild.inject.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * @license MPL-2.0 - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * Contributors: - * - See Git history at https://github.com/FilteringDev/tinyShield for detailed authorship information. - */ \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 398aab3..ac06520 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,12 @@ "moduleResolution": "NodeNext", "removeComments": false, "alwaysStrict": false, - "skipLibCheck": true + "skipLibCheck": true, + "paths": { + "@builder/*": ["./builder/source/*"], + "@userscript/*": ["./userscript/source/*"], + "@root/*": ["./source/*"], + "@reporoot/*": ["./*"] + } } } \ No newline at end of file diff --git a/userscript/package.json b/userscript/package.json new file mode 100644 index 0000000..d16ae18 --- /dev/null +++ b/userscript/package.json @@ -0,0 +1,11 @@ +{ + "name": "@filteringdev/tinyshield-userscript", + "private": true, + "type": "module", + "scripts": { + "lint": "tsc --noEmit && eslint **/*.ts" + }, + "devDependencies": { + "@types/web": "^0.0.318" + } +} diff --git a/sources/src/as-weakmap.ts b/userscript/source/as-weakmap.ts similarity index 100% rename from sources/src/as-weakmap.ts rename to userscript/source/as-weakmap.ts diff --git a/sources/src/index.ts b/userscript/source/index.ts similarity index 100% rename from sources/src/index.ts rename to userscript/source/index.ts diff --git a/sources/interface.ts b/userscript/source/interface.ts similarity index 86% rename from sources/interface.ts rename to userscript/source/interface.ts index 5621353..659cced 100644 --- a/sources/interface.ts +++ b/userscript/source/interface.ts @@ -8,4 +8,4 @@ * - See Git history at https://github.com/FilteringDev/tinyShield for detailed authorship information. */ -export { RunTinyShieldUserscript } from './src/index.js' \ No newline at end of file +export { RunTinyShieldUserscript } from './index.js' \ No newline at end of file diff --git a/sources/src/utils.ts b/userscript/source/utils.ts similarity index 100% rename from sources/src/utils.ts rename to userscript/source/utils.ts diff --git a/userscript/tsconfig.json b/userscript/tsconfig.json new file mode 100644 index 0000000..ed149fb --- /dev/null +++ b/userscript/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "source/**/*.ts" + ] +} \ No newline at end of file