@adguard/dnr-converter
v2.0.0
Published
A converter that transforms adblock-style filtering rules into rules compatible with the Declarative Net Request (DNR) API.
Readme
DNR Converter
A TypeScript library that converts adblock-style filtering rules into rules compatible with Chrome's Declarative Net Request (DNR) API. It is designed for developers building Manifest V3 browser extensions that need to translate existing AdGuard or other adblock filter lists into the DNR format required by modern Chrome extensions.
Key concepts
- Filter — an adblock filter list represented as an object with an
idand textcontent. Each filter list (e.g. AdGuard Base, EasyList) becomes oneFilterinput. - Declarative rule — a
chrome.declarativeNetRequest.Ruleobject that the browser evaluates natively. The converter produces these from filter text. - Source map — a mapping from each generated declarative rule back to its originating filter and rule index, enabling reverse lookup when a declarative rule fires.
- Rule safety — Chrome classifies DNR rules as safe (block, allow, allowAllRequests, upgradeScheme) or unsafe (redirect, modifyHeaders). The library exposes a helper to check safety.
- Converter options — limits and paths that control conversion output (maximum rule counts, resource paths for redirects).
Supported rule types
The converter supports virtually all adblock network rule modifiers. The known MV3 limitations (inherent to the Declarative Net Request API) are:
Supported modifiers (with MV3 notes):
- Basic blocking/allowing rules,
$third-party,$domain(no regexps /.*TLDs), all content types ($script,$image,$stylesheet, etc.) $important,$match-case,$method,$to,$denyallow,$header(no regex values),$all$redirect— allowlist rules not supported$csp,$removeparam,$removeheader,$permissions— allowlist rules not supported; rules with identical conditions are combined only within the same filter, not across filters$urltransform— limited; complex transforms produce multiple declarative rules via pipeline stages$cookie— limited; only supported without parameters (bare$cookie)$badfilter— partial: does not handle$domainintersections correctly
Not yet supported / not convertible to DNR:
$popup,$redirect-rule,$referrerpolicy— not yet implemented$replace,$jsonprune,$xmlprune,$hls,$network, DNS modifiers ($client,$dnsrewrite,$dnstype,$ctag) — not expressible in DNR; produce conversion errors and are skipped- Exception modifiers
$jsinject,$stealth,$urlblock,$genericblock— not yet implemented $webrtc— deprecated and not supported
For detailed conversion examples with output, see Conversion Examples.
Installation
# pnpm
pnpm install @adguard/dnr-converter
# yarn
yarn add @adguard/dnr-converter
# npm
npm install @adguard/dnr-converterPeer dependencies
The package requires @adguard/re2-wasm
(1.2.0) as a peer dependency for regex validation.
API overview
The library uses a single FilterConverter class with two conversion modes
selected by the withSourceMap option:
| | Simple mode (default) | Advanced mode (withSourceMap: true) |
| --- | --- | --- |
| Converter | FilterConverter | FilterConverter |
| Ruleset | Ruleset (sync, in-memory) | RulesetWithSourceMap (lazy-load, source map) |
| Input filter | IFilter | IFilter |
| Use case | When you only need DeclarativeRule[] output from plain filter text | When you need source maps, $badfilter cross-filter application, and serialization |
IFilter
import type { IFilter } from '@adguard/dnr-converter';Single interface used by both conversion flows:
| Method | Type | Description |
| --- | --- | --- |
| getId() | number | Unique filter identifier |
| getContent() | Promise<string> | Returns the full text of the filter list |
| getRuleByIndex(index) | Promise<string> | Returns original rule text by character offset |
| unloadContent() | void | Releases loaded content from memory |
Filter
The single concrete IFilter implementation shipped by the library supports two
construction modes:
Pre-loaded (
new Filter(id, content: string)) — accepts the filter text directly as a string.getContent()resolves immediately;unloadContent()is a no-op. Use for simple conversion or when content is already in memory.Lazy-loaded (
new Filter(id, source: () => Promise<string>)) — accepts an async callback that fetches the content on demand. Supports promise deduplication across concurrent callers, andunloadContent()releases the cached content. Use withwithSourceMap: true(advanced mode).
import { Filter } from '@adguard/dnr-converter';
// Pre-loaded (simple mode)
const filter = new Filter(1, '||example.com^');
// Lazy-loaded (advanced mode with withSourceMap: true)
const lazyFilter = new Filter(1, async () => fetchFilterText());ConverterOptions
import type { ConverterOptions } from '@adguard/dnr-converter';Configuration for the conversion process:
| Property | Type | Description |
| --- | --- | --- |
| resourcesPath | string? | Path to web-accessible resources relative to the extension root (starts with /, no trailing /). Required for $redirect rules. |
| maxNumberOfRules | number? | Maximum total declarative rules to produce. Excess rules are trimmed. |
| maxNumberOfUnsafeRules | number? | Maximum unsafe (dynamic) rules allowed. |
| maxNumberOfRegexpRules | number? | Maximum rules using regexFilter. |
| combine | boolean? | Merge all input filters into a single combined rule set. |
| withSourceMap | boolean? | When true, returns RulesetWithSourceMap instead of Ruleset. Enables source maps, $badfilter cross-filter support, and lazy loading. |
| badFilterRules | Rule[]? | Static $badfilter rules to apply at scan time (withSourceMap: true only). |
Simple flow: FilterConverter + Ruleset
Use this flow when you only need DeclarativeRule[] output from plain filter
text, without source maps or lazy loading.
import { FilterConverter, Filter } from '@adguard/dnr-converter';
const filter = new Filter(1, '||example.com^\n@@||example.com/path^');
const converter = new FilterConverter();
const [{ ruleset, errors, limitations }] = await converter.convert([filter]);
console.log(ruleset.getDeclarativeRules()); // DeclarativeRule[]
console.log(ruleset.getSafeRulesCount()); // 2
console.log(errors.length); // 0Combine multiple filters into one ruleset:
const [{ ruleset }] = await converter.convert(
[filter1, filter2],
{ combine: true },
);
// ruleset.getId() === FilterConverter.COMBINED_RULESET_IDSerialize and restore:
const json = ruleset.serialize(); // JSON string of DeclarativeRule[]
const { Ruleset } = await import('@adguard/dnr-converter');
const restored = Ruleset.deserialize(ruleset.getId(), json);Ruleset (returned by default, without withSourceMap) implements IRuleset:
| Method | Returns | Description |
| --- | --- | --- |
| getId() | string | Rule set identifier (e.g. "ruleset_1") |
| getSafeRulesCount() | number | Count of safe declarative rules (excludes unsafe) |
| getUnsafeRulesCount() | number | Count of unsafe rules |
| getRegexpRulesCount() | number | Count of regexp-based rules |
| getDeclarativeRules() | DeclarativeRule[] | All converted DNR rules (synchronous) |
| serialize() | string | JSON serialization of declarative rules |
| Ruleset.deserialize(id, json) | Ruleset | Static: reconstruct from serialized JSON |
Advanced flow: FilterConverter with withSourceMap: true + RulesetWithSourceMap
Use this mode in browser extension internals when you need source maps,
$badfilter cross-filter application, serialization with hash maps, and lazy
content loading. Pass withSourceMap: true in the options to get
RulesetWithSourceMap results.
import { Filter, FilterConverter } from '@adguard/dnr-converter';
const filter = new Filter(1, async () => '||example.com^\n@@||example.com/path^');
const converter = new FilterConverter();
const [{ ruleset, errors }] = await converter.convert([filter], { withSourceMap: true });
const declarativeRules = await ruleset.getDeclarativeRules(); // Promise<DeclarativeRule[]>
const sources = await ruleset.getRulesById(declarativeRules[0].id);
console.log(sources[0].sourceRule); // '||example.com^'Apply $badfilter from dynamic filters against static rule sets:
const [{ ruleset: dynamicRuleset }] = await converter.convert(
[dynamicFilter],
{ withSourceMap: true },
);
const rulesToDisable = await converter.computeRulesToDisable(
[dynamicRuleset],
[staticRuleset],
);
// rulesToDisable: UpdateStaticRulesOptions[]RulesetWithSourceMap (returned when withSourceMap: true) implements
IRulesetWithSourceMap:
| Method | Returns | Description |
| --- | --- | --- |
| getId() | string | Rule set identifier |
| getSafeRulesCount() | number | Count of safe declarative rules (excludes unsafe) |
| getUnsafeRulesCount() | number | Count of unsafe rules |
| getRegexpRulesCount() | number | Count of regexp-based rules |
| getDeclarativeRules() | Promise<DeclarativeRule[]> | All converted DNR rules (lazy) |
| getUnsafeRules() | Promise<DeclarativeRule[]> | Unsafe rules subset (lazy) |
| getRulesById(id) | Promise<SourceRuleAndFilterId[]> | Source rules for a DNR rule |
| getBadFilterRules() | NetworkRule[] | $badfilter rules in this set |
| getRulesHashMap() | IRulesHashMap | Hash map for fast $badfilter matching |
| getMetadataRulesCount() | number | Metadata rules in the ruleset file the rule set was read from (0 if not read from a file) |
| serializeCompact(unsafeRules, prettyPrint?) | Promise<string> | Ruleset file in the compact ruleset file format |
| unloadContent() | void | Release lazy-loaded content |
| RulesetWithSourceMap.fromCompact(id, content, filters) | RulesetWithSourceMap | Build a rule set from parseCompactRuleset() output |
| RulesetWithSourceMap.fromDeserialized(deserialized, metadataRulesCount) | RulesetWithSourceMap | Build a rule set from RulesetWithSourceMap.deserialize() output |
import type { ConversionResult } from '@adguard/dnr-converter';Result returned by converter methods:
| Property | Type | Description |
| --- | --- | --- |
| ruleset | IRuleset / IRulesetWithSourceMap | The converted rule set |
| errors | (ConversionError \| Error)[] | Rules that could not be converted |
| limitations | LimitationError[] | Warnings about exceeded limits |
| declarativeRulesToCancel | UpdateStaticRulesOptions[]? | Static rule IDs to disable (from computeRulesToDisable) |
Compact ruleset file format
serializeCompact() and MetadataRuleset.serialize() write a JSON array of
DNR rules. The array starts with one or more metadata rules: block rules
whose condition.urlFilter is dummy.rule.adguard.com (they never match a
request) and whose metadata value is { "chunk": "<fragment>" }. Joining
the fragments in array order gives the serialized metadata. Every metadata
value, including JSON escaping and the wrapper, is at most 65,536 UTF-8
bytes: Edge Add-ons rejects packages with larger values. The ordinary
declarative rules follow the metadata rules.
A metadata rule is recognized by the marker urlFilter together with the
metadata key. A converted rule for the host dummy.rule.adguard.com
without a metadata key is an ordinary rule.
Metadata rules are static DNR rules: they count towards the static rule quota and get the smallest IDs not used by any other rule of the ruleset. Ordinary rule IDs are never changed.
[
{ "id": 1, "action": { "type": "block" }, "condition": { "urlFilter": "dummy.rule.adguard.com", "resourceTypes": ["xmlhttprequest"] }, "metadata": { "chunk": "{\"metadata\":{...},\"lazyMetadata\":{...},\"filterCon" } },
{ "id": 2, "action": { "type": "block" }, "condition": { "urlFilter": "dummy.rule.adguard.com", "resourceTypes": ["xmlhttprequest"] }, "metadata": { "chunk": "tent\":\"||example.com^\"}" } },
{ "id": 1234567, "action": { "type": "block" }, "condition": { "urlFilter": "||example.com^" } }
]Read a filter ruleset file with parseCompactRuleset():
import { Filter, parseCompactRuleset, RulesetWithSourceMap } from '@adguard/dnr-converter';
const content = parseCompactRuleset('ruleset_1', JSON.parse(fileText));
content.metadata; // SerializedRulesetData
content.lazyMetadata; // SerializedRulesetLazyData
content.filterContent; // original filter text
content.declarativeRules; // ordinary DNR rules, without the metadata rules
content.metadataRulesCount; // number of metadata rules, for static rule quota accounting
// Build the rule set directly from the parsed content.
const ruleset = RulesetWithSourceMap.fromCompact('ruleset_1', content, [new Filter(1, content.filterContent)]);fromCompact() validates metadata at once and the lazy metadata and
ordinary rules on first load, with the same schemas as
RulesetWithSourceMap.deserialize(). The rule set keeps the parsed content
in memory for its whole lifetime; unloadContent() does not release it.
When the parts are stored as strings, for example in IndexedDB, load them
lazily with deserialize() and build the rule set with fromDeserialized(),
passing the stored metadataRulesCount:
const ruleset = RulesetWithSourceMap.fromDeserialized(
await RulesetWithSourceMap.deserialize(id, rawData, loadLazyData, loadDeclarativeRules, filters),
metadataRulesCount,
);The reader takes only the leading run of metadata rules, joins them and
parses the result once. It throws InvalidMetadataChunksError (with a
rulesetId property) when the input is not an array, has no leading
metadata rule, has a metadata rule without a string chunk, or when the
joined fragments are not valid JSON.
MetadataRuleset
import { MetadataRuleset, METADATA_RULESET_ID } from '@adguard/dnr-converter';A specialized ruleset that stores checksums and additional properties for a
collection of DNR rule sets. It serializes in the
compact ruleset file format: a JSON array
that contains only metadata rules carrying { checksums, additionalProperties }
in metadata.chunk fragments.
METADATA_RULESET_ID is the constant 0; the ruleset's string ID is always
"ruleset_0".
Checksum methods:
| Method | Returns | Description |
| --- | --- | --- |
| getId() | string | Always "ruleset_0" |
| setChecksum(rulesetId, checksum) | void | Store checksum for a rule set |
| getChecksum(rulesetId) | string \| undefined | Retrieve checksum, or undefined if not set |
| getRulesetIds() | string[] | All rule set IDs that have checksums |
Additional-property methods:
| Method | Returns | Description |
| --- | --- | --- |
| setAdditionalProperty(key, value) | void | Store an arbitrary JSON-serializable property |
| getAdditionalProperty(key) | unknown \| undefined | Retrieve a property value |
| hasAdditionalProperty(key) | boolean | Check whether a property exists |
| removeAdditionalProperty(key) | void | Remove a property (no-op if missing) |
Serialization:
| Method | Returns | Description |
| --- | --- | --- |
| serialize(pretty?) | string | JSON string; pretty=true for human-readable output |
| MetadataRuleset.deserialize(json) | MetadataRuleset | Reconstruct from a serialized string; throws SyntaxError on invalid JSON, InvalidMetadataChunksError on malformed metadata rules, a validation error on a bad payload |
const meta = new MetadataRuleset();
meta.setChecksum('ruleset_1', 'abc123');
meta.setAdditionalProperty('version', '2.0');
const json = meta.serialize();
const restored = MetadataRuleset.deserialize(json);
console.log(restored.getChecksum('ruleset_1')); // "abc123"
console.log(restored.getAdditionalProperty('version')); // "2.0"isSafeRule(rule)
import { isSafeRule } from '@adguard/dnr-converter';
// DeclarativeRule from the DNR API
const rule = {
id: 2,
priority: 1,
action: { type: 'block' },
condition: { urlFilter: 'example.com' },
};
isSafeRule(rule); // true — "block" is a safe actionReturns true if the declarative rule's action is one of the
safe rule actions (block, allow,
allowAllRequests, upgradeScheme). Useful for separating safe static rules
from unsafe dynamic rules that require additional review.
DNR_CONVERTER_VERSION
import { DNR_CONVERTER_VERSION } from '@adguard/dnr-converter';
console.log(DNR_CONVERTER_VERSION); // e.g. "0.0.1"A string constant with the current library version.
Network rule types
The package exports types related to network rule parsing and validation:
import {
Rule,
HttpHeaderMatcher,
RuleDeclarativeValidator,
} from '@adguard/dnr-converter';Rule— parsed network rule with accessors for domains, resource types, methods, advanced modifier values, priority, and option flags. Create instances viaRule.createFromText(filterListId, index, text).HttpHeaderMatcher— type describing the parsed$headermodifier value.RuleDeclarativeValidator— static helper that checks whether aRulecan be converted to a DNR rule. CallRuleDeclarativeValidator.shouldConvertRule(rule)— returnstrueif the rule is convertible,falseif it should be silently skipped, or throwsUnsupportedModifierErrorif the rule uses an unsupported modifier.
CLI
The package ships a dnr-converter command-line tool for converting filter
lists to DNR rulesets and extracting filter content back from compiled rulesets.
Commands
convert
Converts AdGuard filter lists from a metadata directory into DNR rulesets.
dnr-converter convert \
./filters \
./resources \
./dist/rulesetsArguments:
| Argument | Description | Default |
| --- | --- | --- |
| <filters_and_metadata_dir> | Directory containing filters.json and filter list files | (required) |
| <resources_dir> | Directory with redirect resources | (required) |
| [dest_rule_sets_dir] | Output directory for generated rulesets | ./build/rulesets |
Options:
| Option | Description | Default |
| --- | --- | --- |
| --debug | Enable debug logging | false |
| --prettify-json <bool> | Pretty-print JSON output files | true |
| --additional-properties <json> | Additional properties to include in the metadata ruleset as JSON string | {} |
The filters.json metadata file must be an array of filter metadata objects
each with at minimum filterId and name fields.
extract-filters
Extracts original filter list content that was embedded in compiled DNR rulesets
by the convert command.
dnr-converter extract-filters \
./dist/rulesets \
./extracted-filtersArguments:
| Argument | Description |
| --- | --- |
| <path-to-rulesets> | Directory containing compiled ruleset files |
| <path-to-output> | Directory to write extracted filter files |
Programmatic API
The CLI logic is also available as a programmatic API via the
@adguard/dnr-converter/cli subpath export:
import { convertFilters, generateMD5Hash, type ConvertFiltersOptions } from '@adguard/dnr-converter/cli';
await convertFilters(
'./filters',
'./resources',
'./dist/rulesets',
{ debug: true, prettifyJson: true },
);