Skip to content

About

A tool that compiles & validates filters

Topics

Resources

Stars

65 stars

Watchers

22 watching

Forks

Latest commit

 

History

1,215 Commits

Folders and files

Repository files navigation

AdGuard Filters Compiler

A Node.js library that compiles, converts, validates, and optimizes ad-blocking filter lists into platform-specific formats for AdGuard products across all supported platforms.

Note: This package is developed in AdGuardSoftwareLimited/ext-compiler. The AdguardTeam/FiltersCompiler repository is a public mirror.

Description

AdGuard Filters Compiler is a library for developers who maintain ad-blocking filter lists and the build pipelines that ship them. It transforms source filter lists — written in the AdGuard filter rules syntax — into compiled output tailored to each target platform (browser extensions, desktop and mobile apps, and the AdGuard CLI).

AdGuard's filter rules are authored once but must run on many engines with different capabilities: not every platform supports scriptlets, HTML filtering, $redirect modifiers, extended CSS, or regex-based $domain selectors. The compiler closes that gap. It resolves @include and !#include preprocessor directives, converts rules between AdGuard and uBlock Origin formats, validates each rule against the @adguard/tsurlfilter engine, optimizes blocking rules using hit-count statistics, and strips rules that a given platform cannot apply.

The library is consumed by FiltersRegistry to produce production filter builds, but it can be used directly by anyone who needs to compile filter lists for AdGuard platforms. The expected source directory structure and the filters metadata format are documented in the FiltersRegistry README.

Table of Contents


Installation

npm install @adguard/filters-compiler

Requires Node.js 22.21+ (22.x) or 24+.

Also available via pnpm or yarn:

pnpm add @adguard/filters-compiler
# or
yarn add @adguard/filters-compiler

The package ships as a single ES module. Use import, or use require with a supported Node.js version, which loads the same artifact via require(ESM).

Quick Start

import { compile } from '@adguard/filters-compiler';

const filtersDir = './filters';
const logPath = './log.txt';
const reportPath = './report.txt';
const platformsPath = './platforms';

// Compile every filter for every platform, keeping only filters
// with IDs 1 and 3, and excluding filter ID 2.
await compile(filtersDir, logPath, reportPath, platformsPath, [1, 3], [2]);

The compiler reads the source filter lists from filtersDir and writes platform-specific output to subdirectories under platformsPath. A human-readable compilation report is written to reportPath, and a timestamped log to logPath.

API Overview

The library exports four functions:

Function Purpose
compile(...) Compiles filter lists into platform-specific output
validateJSONSchema(...) Validates built platform output against JSON schemas
validateLocales(...) Validates locale translation files for completeness
localOptimizationStatistics Caches optimization statistics for local compilation

compile(...)

function compile(
    path: string,
    logPath: string | undefined,
    reportFile: string | undefined,
    platformsPath: string | null,
    includedFilterIds?: number[] | null,
    excludedFilterIds?: number[] | null,
    customPlatformsConfig?: CustomPlatformsConfig,
): Promise<void>;

Compiles the filter lists in path for all configured platforms and writes the results to platformsPath.

  • includedFilterIds — compile only the filter IDs listed here (empty array compiles all filters).
  • excludedFilterIds — exclude the filter IDs listed here.
  • customPlatformsConfig — overrides or extends the built-in platform definitions (see Custom platforms).

validateJSONSchema(...)

interface ValidateJSONSchemaOptions {
    localesPath: string;
    requiredLocales?: string[];
    logPath?: string;
}

function validateJSONSchema(
    platformsPath: string,
    options: ValidateJSONSchemaOptions,
): boolean;

Recursively validates the built JSON files in platformsPath.

The filters_i18n schema is generated at runtime for every non-legacy platform from its filters.json and the FiltersRegistry locale data in localesPath. The entity IDs come from filters.json. The required locales and the English fallback strings come from the locale directory. The compiler does not keep a copy of filter, group, or tag text. The legacy mac and mac_v2 platforms use their frozen schemas.

  • localesPath — path to the FiltersRegistry locales directory.
  • requiredLocales — locales that must be present for every group, tag, and filter. Defaults to all locale subdirectories of localesPath. The schema validates every locale directory in localesPath, even when this list contains a subset.
  • logPath — optional validation log file. The file is appended to and closed when validation finishes. When omitted, messages go to the console only; a log file opened by an earlier call is not reused.

Missing fields in an existing locale object are filled with the base (en) value, and each fallback is logged as a warning. A locale object that is missing entirely for an entity is not filled: it fails validation when the locale is required. A built entity with no English localization, or a missing required locale, fails validation.

The validator writes the filled filters_i18n.json back to disk, plus a .js copy, even when validation fails.

requiredLocales and the English fallback apply only to the generated schema used for non-legacy platforms. The frozen mac and mac_v2 schemas ignore requiredLocales, fill their own defaults, and log no fallbacks.

The filters.schema.json and the legacy mac/mac_v2 schemas remain static and are bundled with the library.

Returns true when all files are valid. On validation failure it logs the errors and returns false instead of throwing, so callers must check the return value. It throws when options or options.localesPath is missing, when options.logPath cannot be opened, or when a built JSON file cannot be parsed.

validateLocales(...)

function validateLocales(
    localesPath: string,
    requiredLocales: string[],
    logFormat?: 'text' | 'markdown',
): ValidateLocalesResult;

interface ValidateLocalesResult {
    ok: boolean;
    data?: unknown[]; // per-locale warning details, present when warnings exist
    log?: string;     // formatted warnings log, present when warnings exist
}

Validates that the locale files in localesPath are complete — that every filter, group, and tag has a translated name and description for all locales listed in requiredLocales.

Returns { ok: true } when no problems are found. When warnings are found, data and log contain the per-locale details and ok is false only if at least one warning is critical. Throws when the locales directory is missing or empty.

logFormat controls the format of the returned log field: 'text' (the default) produces the plain indented list, 'markdown' produces a markdown document suitable for embedding in a GitHub issue or PR comment.

localOptimizationStatistics

A namespace for caching optimization statistics locally so that compile() reads each filter's stats.json from disk instead of fetching it from the remote server. percent.json (which determines which filters are optimizable) is fetched remotely unless a local percentPath is passed to download or use.

It exposes download, use, and reset methods and throws OptimizationStatsError (an Error subclass carrying filterId, sourcePath, and code fields) when stats for a filter cannot be retrieved or fail structural validation. Stats whose groups array is empty — a filter whose hit data has not been collected yet — are not an error: they are logged as a warning and the filter is built without optimization.

See Local Optimization Statistics for a complete workflow.

Usage Examples

Compiling filter lists

Compile all filters with the default platform configuration:

import { compile } from '@adguard/filters-compiler';

await compile(
    './filters',      // source filter lists
    './log.txt',      // log file (omit to disable logging)
    './report.txt',   // compilation report
    './platforms',    // platform output directory
    [],               // includedFilterIds (empty = compile all)
    [],               // excludedFilterIds (empty = exclude none)
);

Compile a single filter into a custom platform:

import { compile } from '@adguard/filters-compiler';

const customPlatformsConfig = {
    // Here you can redefine some of the platforms from platforms.json
    // or add new platforms if you need it.
    MAC_V3: {
        platform: 'mac',
        path: 'mac_v3',
        configuration: {
            ignoreRuleHints: false,
            removeRulePatterns: [
                '^\\/.*', // drop regex rules
            ],
            replacements: [
                { from: 'regex', to: 'repl' },
            ],
        },
        defines: {
            adguard: true,
            adguard_app_mac: true,
        },
    },
};

await compile(
    './filters',
    undefined,          // no log file
    './report.txt',
    './platforms',
    [1],                // only filter ID 1
    [],
    customPlatformsConfig,
);

Validating built filters

After compiling, validate the built output:

import { validateJSONSchema } from '@adguard/filters-compiler';

const valid = validateJSONSchema('./platforms', {
    localesPath: './locales',
    logPath: './json-validation.log',
});
if (!valid) {
    throw new Error('Schema validation failed, see log for details');
}

Validating locale translations

import { validateLocales } from '@adguard/filters-compiler';

const result = validateLocales('./locales', ['en', 'fr', 'ko'], 'markdown');
if (!result.ok) {
    console.error(result.log);
}

@include directive and its options

The @include directive provides the ability to include content from the specified address. Filter source files use it to pull in content from other files or remote URLs during compilation, letting you compose filter lists from shared fragments.

Syntax

@include <filepath> [<options>]
  • <filepath> — required, URL or same-origin relative file path to include.

  • <options> — optional, space-separated flags:

    • /stripComments — removes AdBlock-style comment lines (starting with !) from the included file.
    • /notOptimized — adds a !+ NOT_OPTIMIZED hint to the rules.
    • /exclude="<filepath>" — excludes rules listed in the exception file at filepath.
    • /addModifiers="<modifiers>" — appends the given modifiers (as is) to every rule in the included file. Works with host-rule files too, converting # host-file comments to ! AdBlock-style comments.
    • /ignoreTrustLevel — skips the trust-level check for the included file. Only allowed for same-origin files.
    • /optimizeDomainBlockingRules — removes redundant domain-blocking rules. Rules with modifiers and rules of other formats are ignored.

Important

Options are applied in the order they appear in the directive, except /ignoreTrustLevel.

Examples

Apply all options to an included file:

@include ../input.txt /addModifiers="script" /exclude="../exclusions.txt" /notOptimized /stripComments /optimizeDomainBlockingRules

Given ../input.txt:

# comment
example.com
example.org

1. /addModifiers="script" appends $script to every rule and converts host-file # comments to AdBlock-style !:

! comment
example.com$script
example.org$script

2. /exclude="../exclusions.txt" removes rules listed in exclusions.txt (for example, example.com$script):

! comment
example.org$script

3. /notOptimized adds the hint:

! comment
!+ NOT_OPTIMIZED
example.org$script

4. /stripComments removes comment lines:

!+ NOT_OPTIMIZED
example.org$script

5. /optimizeDomainBlockingRules removes redundant domain-blocking rules. For example, when ||sub.example.com^ is already covered by ||example.com^, it is dropped, while rules with modifiers such as ||test.com^$script are kept:

||example.com^
||domain.com^
||test.com^$script

Include a remote file with comment stripping:

@include "https://easylist.github.io/easylist/easylist.txt" /stripComments

Skip the trust-level check for a same-origin included file:

@include ./input.txt /ignoreTrustLevel

Configuration

Custom platforms

Pass customPlatformsConfig to compile() to redefine existing platforms or add new ones. Each entry mirrors the structure in src/main/platforms-config.js:

const customPlatformsConfig = {
    MAC_V3: {
        platform: 'mac',           // platform family
        path: 'mac_v3',            // output subdirectory under platformsPath
        expires: '12 hours',       // optional cache TTL written to metadata
        configuration: {
            ignoreRuleHints: false, // honour `!+ HINT` hints when false
            removeRulePatterns: [   // regexes of rules to drop
                '^\\/.*',
            ],
            replacements: [        // regex replacements applied to rules
                { from: 'regex', to: 'repl' },
            ],
        },
        defines: {                  // !#if preprocessor flags for this platform
            adguard: true,
            adguard_app_mac: true,
        },
    },
};

replacements[].from is treated as a regular-expression pattern (it is passed to new RegExp(from, 'g')), not as literal text. Escape regex meta characters in from when a literal match is intended.

Logging

The compiler writes a timestamped log file when logPath is passed to compile(). If logPath is omitted, no log file is written. The parent directory is created automatically when it does not exist.

validateJSONSchema() accepts the same logPath in its options object. It appends to the file and closes it when validation finishes. A later call without logPath does not reuse it.

Log levels:

  • INFO — general information about the process
  • WARN — warnings that do not stop the process
  • ERROR — errors indicating failures

Environment variables

Variable Default Purpose
TLS (system defaults) Set to insecure to make curl skip TLS certificate verification
NODE_USE_ENV_PROXY (unset) Set to 1 to let Node.js fetch use proxy variables for remote !#include downloads

TLS=insecure applies only to curl downloads: @include files and optimization data. Remote !#include downloads use fetch, which follows the Node.js TLS settings. To trust a private CA, set NODE_EXTRA_CA_CERTS to the CA file. To disable certificate verification, set NODE_TLS_REJECT_UNAUTHORIZED=0. This disables verification for the whole process.

NODE_USE_ENV_PROXY=1 makes remote !#include downloads use a proxy from the environment. Node.js reads HTTP_PROXY or http_proxy, HTTPS_PROXY or https_proxy, and NO_PROXY or no_proxy. Without this variable, fetch does not use a proxy. ALL_PROXY is not supported.

curl downloads read the same names, except the uppercase HTTP_PROXY, and also ALL_PROXY as a fallback. http_proxy applies only to http:// URLs. @include sources are usually https://, so set https_proxy or HTTPS_PROXY to proxy them.

Supported platforms

The compiler ships built-in configurations for the following platforms. Each writes its output to a subdirectory under platformsPath.

Platform ID Output directory Target
WINDOWS windows AdGuard for Windows
MAC mac AdGuard for macOS (legacy v1)
MAC_V2 mac_v2 AdGuard for macOS v2
MAC_V3 mac_v3 AdGuard for macOS v3
ANDROID android AdGuard for Android
IOS ios AdGuard for iOS
CLI cli AdGuard CLI / CoreLibs
EXTENSION_CHROMIUM extension/chromium AdGuard Browser Extension (Chromium, MV2)
EXTENSION_CHROMIUM_MV3 extension/chromium-mv3 AdGuard Browser Extension (Chromium, MV3)
EXTENSION_EDGE extension/edge AdGuard Browser Extension (Edge, MV2)
EXTENSION_EDGE_MV3 extension/edge-mv3 AdGuard Browser Extension (Edge, MV3)
EXTENSION_OPERA extension/opera AdGuard Browser Extension (Opera, MV2)
EXTENSION_OPERA_MV3 extension/opera-mv3 AdGuard Browser Extension (Opera, MV3)
EXTENSION_FIREFOX extension/firefox AdGuard Browser Extension (Firefox)
EXTENSION_SAFARI extension/safari AdGuard Browser Extension (Safari)
EXTENSION_ANDROID_CONTENT_BLOCKER extension/android-content-blocker AdGuard content blocker (Android)
EXTENSION_UBLOCK extension/ublock uBlock Origin-compatible output

Local Optimization Statistics

localOptimizationStatistics provides a workflow for caching optimization statistics locally so that compile() reads each filter's stats.json from disk instead of fetching it from the remote server. percent.json (which determines which filters are optimizable) is fetched remotely unless a local percentPath is passed to download or use.

Directory layout

<basePath>/
  filters/
    <filterId>/
      stats.json                 # per-filter hit counts (STATS_JSON)

Workflow

  1. Call download(basePath, includedFilterIds, excludedFilterIds, { percentPath }) to save stats.json for filters listed in percent.json (the remote one, or the local array at the optional percentPath). includedFilterIds limits processing to those IDs (default [] = all). excludedFilterIds skips those IDs (default [] = none). When both are given, a filter is processed only if it is included and not excluded.
  2. Call use(basePath, { percentPath }) before compile() to tell getOptimizationStatistics to read stats from local files under basePath instead of fetching from the remote server. The optional percentPath points to a local JSON file holding an array of { "filterId": 1 } entries, such as the registry's optimization_config.json, instead of the remote percent.json — so the set of optimizable filters is pinned and no network access is needed. Each source (a given percentPath, or the remote percent.json) is read once per process and cached until reset(), so repeated use() and download() calls with the same path don't re-read it. Calling use() without percentPath (or reset()) stops using the path. Pass the same percentPath to download() and use(): a filter listed only in the local file gets no stats.json if download() used the remote list, and compile() then fails on it.
  3. Call reset(basePath) when done to remove the cache directory and clear in-memory state.

If stats for a filter can't be retrieved (missing local file, or a failed remote fetch) or fail structural validation, getOptimizationStatistics throws OptimizationStatsError — an Error subclass carrying filterId and sourcePath fields, and a code (OPTIMIZATION_STATS_UNAVAILABLE for retrieval failures, OPTIMIZATION_STATS_INVALID for validation failures) so callers can branch on the failure kind instead of matching on error.message. Its constructor takes a reason: 'retrieval' | 'validation' argument after sourcePath.

One case is not a failure: stats whose groups array is empty mean that no hit data has been collected for the filter yet. getOptimizationStatistics logs a warning with the filter ID and the stats source and returns null, so the filter is built without optimization. Any other malformed groups value still throws OptimizationStatsError.

import { compile, localOptimizationStatistics } from '@adguard/filters-compiler';

const percentPath = './optimization_config.json';

// 1. Download stats for the optimizable filters listed in percentPath to ./stats-cache
await localOptimizationStatistics.download('./stats-cache', [], [], { percentPath });

// 2. Tell compile() to read stats from the local cache
localOptimizationStatistics.use('./stats-cache', { percentPath });
await compile('./filters', undefined, undefined, './platforms', [], []);

// 3. Clean up when done
await localOptimizationStatistics.reset('./stats-cache');

Documentation

About

A tool that compiles & validates filters

Topics

Resources

Stars

65 stars

Watchers

22 watching

Forks

Releases

Packages

Used by

Contributors

Languages