Skip to main content

magic-regexp vs regexp-tree vs safe-regex: Regular Expression Utilities in JavaScript (2026)

·PkgPulse Team

TL;DR

magic-regexp is the UnJS composable regex builder — write regex with TypeScript-first chainable API, get compile-time type safety for capture groups, no regex syntax needed. regexp-tree is the regex AST toolkit — parse, transform, optimize, and generate regular expressions using an abstract syntax tree. safe-regex detects vulnerable regex patterns — prevents ReDoS (Regular Expression Denial of Service) attacks by detecting exponential backtracking. In 2026: magic-regexp for building readable regex, regexp-tree for regex tooling, safe-regex for security auditing.

Key Takeaways

  • magic-regexp: ~500K weekly downloads — UnJS, composable regex, typed capture groups
  • regexp-tree: ~15M weekly downloads — regex parser/transformer/optimizer, used by ESLint
  • safe-regex: ~10M weekly downloads — ReDoS detection, prevents catastrophic backtracking
  • Completely different tools: build regex, transform regex, secure regex
  • magic-regexp makes regex readable and type-safe
  • safe-regex is critical for validating user-supplied patterns

magic-regexp

magic-regexp — composable regex:

Building regex

import { createRegExp, exactly, oneOrMore, digit, letter, anyOf } from "magic-regexp"

// Instead of: /^\d{3}-\d{3}-\d{4}$/
const phoneRegex = createRegExp(
  exactly(digit.times(3))
    .and("-")
    .and(digit.times(3))
    .and("-")
    .and(digit.times(4)),
  ["g"]
)

// Instead of: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
const emailRegex = createRegExp(
  oneOrMore(anyOf(letter, digit, exactly("._%+-")))
    .and("@")
    .and(oneOrMore(anyOf(letter, digit, exactly(".-"))))
    .and(".")
    .and(letter.times.atLeast(2))
)

Named capture groups (type-safe)

import { createRegExp, exactly, oneOrMore, digit, anyOf } from "magic-regexp"

// Create regex with named groups:
const dateRegex = createRegExp(
  exactly(digit.times(4)).as("year")
    .and("-")
    .and(digit.times(2)).as("month")
    .and("-")
    .and(digit.times(2)).as("day")
)

// TypeScript knows the capture groups:
const match = "2026-03-09".match(dateRegex)
if (match) {
  match.groups.year   // string — TypeScript infers this
  match.groups.month  // string
  match.groups.day    // string
}

Common patterns

import {
  createRegExp, exactly, maybe, oneOrMore, digit, word,
  charIn, charNotIn, anyOf, linefeed, tab,
} from "magic-regexp"

// URL slug: /^[a-z0-9]+(-[a-z0-9]+)*$/
const slugRegex = createRegExp(
  oneOrMore(charIn("a-z0-9"))
    .and(
      exactly("-").and(oneOrMore(charIn("a-z0-9"))).times.any()
    )
)

// Semver: /^(\d+)\.(\d+)\.(\d+)(-[\w.]+)?$/
const semverRegex = createRegExp(
  oneOrMore(digit).as("major")
    .and(".")
    .and(oneOrMore(digit)).as("minor")
    .and(".")
    .and(oneOrMore(digit)).as("patch")
    .and(maybe(exactly("-").and(oneOrMore(anyOf(word, exactly("."))))))
)

const match = "1.2.3-beta.1".match(semverRegex)
// match.groups.major → "1"
// match.groups.minor → "2"
// match.groups.patch → "3"

Why it matters

Traditional regex:
  /^(?:https?:\/\/)?(?:www\.)?([a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+)(?:\/.*)?$/

magic-regexp:
  createRegExp(
    maybe(anyOf("http://", "https://"))
      .and(maybe("www."))
      .and(oneOrMore(anyOf(letter, digit, exactly("-")))
        .and(oneOrMore(exactly(".").and(letter.times.atLeast(2)))))
      .as("domain")
      .and(maybe(exactly("/").and(oneOrMore(charNotIn(" ")))))
  )

Benefits:
  ✅ Readable — no cryptic symbols
  ✅ Type-safe — capture groups are typed
  ✅ Composable — build from smaller parts
  ✅ Maintainable — easy to modify

regexp-tree

regexp-tree — regex AST toolkit:

Parse regex

import regexpTree from "regexp-tree"

// Parse a regex into an AST:
const ast = regexpTree.parse("/^[a-z]+\\d{2,4}$/i")
console.log(JSON.stringify(ast, null, 2))
// → {
//   type: "RegExp",
//   body: {
//     type: "Alternative",
//     expressions: [
//       { type: "Assertion", kind: "^" },
//       { type: "Repetition", expression: { type: "CharacterClass", ... }, quantifier: { type: "+", ... } },
//       { type: "Repetition", expression: { type: "Char", value: "\\d" }, quantifier: { type: "{2,4}", ... } },
//       { type: "Assertion", kind: "$" },
//     ],
//   },
//   flags: "i",
// }

Optimize regex

import regexpTree from "regexp-tree"

// Optimize a regex:
const optimized = regexpTree.optimize("/[a-zA-Z0-9_]|[0-9]/")
console.log(optimized.toString())
// → /[\w]/  (simplified character class)

regexpTree.optimize("/(?:a|b|c)/").toString()
// → /[abc]/

regexpTree.optimize("/(a)(b)\\1\\2/").toString()
// → /(a)(b)\1\2/  (already optimal)

Transform regex

import regexpTree from "regexp-tree"

// Transform: convert named groups to numbered groups
const result = regexpTree.transform("/(?<year>\\d{4})-(?<month>\\d{2})/", {
  NamedGroup(path) {
    // Replace named groups with regular groups:
    path.replace({
      type: "Group",
      capturing: true,
      expression: path.node.expression,
    })
  },
})

console.log(result.toString())
// → /(\\d{4})-(\\d{2})/

Generate regex

import regexpTree from "regexp-tree"

// Build regex from AST:
const ast = {
  type: "RegExp",
  body: {
    type: "Alternative",
    expressions: [
      { type: "Char", value: "a" },
      { type: "Repetition",
        expression: { type: "Char", value: "b" },
        quantifier: { type: "+", greedy: true },
      },
    ],
  },
  flags: "g",
}

const regex = regexpTree.generate(ast)
// → /ab+/g

How ESLint uses regexp-tree

// eslint-plugin-regexp uses regexp-tree internally:

// It parses regex literals in your code,
// analyzes them for issues, and suggests fixes:

// Before: /[0-9]/
// Suggestion: Use \d instead → /\d/

// Before: /[a-zA-Z0-9_]/
// Suggestion: Use \w instead → /\w/

// Before: /a{1}/
// Suggestion: Unnecessary quantifier → /a/

safe-regex

safe-regex — ReDoS detection:

Basic usage

import safe from "safe-regex"

// Check if a regex is safe from ReDoS:
safe(/^(a+)+$/)         // false — exponential backtracking!
safe(/^([a-zA-Z0-9]+)$/)  // true — safe
safe(/^(a|b|c)+$/)      // true — safe
safe(/(a+){10}/)         // false — nested quantifiers
safe(/\d+/)              // true — safe

// With string:
safe("^(a+)+$")          // false

Why ReDoS matters

// ReDoS — Regular Expression Denial of Service:

// Vulnerable regex:
const emailRegex = /^([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+)\.([a-zA-Z]{2,})$/

// Malicious input that causes exponential backtracking:
const evil = "a".repeat(30) + "@" + "b".repeat(30) + "!"
// This could take MINUTES to process with a vulnerable regex

// The pattern /^(a+)+$/ with input "aaaaaaaaaaaaaaaaX":
// → Tries every possible way to divide "a"s between groups
// → 2^n combinations — exponential time
// → With 30 "a"s, could take hours

Validating user input patterns

import safe from "safe-regex"

// In a search API that accepts regex:
app.get("/api/search", (req, res) => {
  const { pattern } = req.query

  if (!safe(pattern)) {
    return res.status(400).json({
      error: "Regex pattern is potentially unsafe (ReDoS risk)",
    })
  }

  const regex = new RegExp(pattern, "i")
  const results = packages.filter((pkg) => regex.test(pkg.name))
  res.json(results)
})

Configuration

import safe from "safe-regex"

// Adjust the star height limit:
safe(/^(a+)+$/, { limit: 25 })  // Default limit is 25

// Star height = nested quantifier depth
// Higher limit = more permissive but less safe

Common vulnerable patterns

Vulnerable (exponential backtracking):
  /^(a+)+$/              — nested quantifiers
  /^(a|aa)+$/            — overlapping alternation
  /^([a-zA-Z]+)*$/       — quantified group with quantified content
  /^(a+b?)*$/            — optional element in quantified group
  /(.*a){x}/             — .* before specific char in group

Safe alternatives:
  /^a+$/                 — single quantifier (no nesting)
  /^[a-zA-Z]+$/          — character class (no group needed)
  /^(?:a|b)+$/           — non-overlapping alternation
  /^\w+@\w+\.\w+$/       — simple character classes

Feature Comparison

Featuremagic-regexpregexp-treesafe-regex
PurposeBuild regexParse/transform regexDetect unsafe regex
APIComposable builderAST manipulationSafety checker
TypeScript types✅ (capture groups)
Parse regex
Build regex✅ (from AST)
Optimize regex
Transform regex
ReDoS detection
Named groups✅ (type-safe)✅ (AST)N/A
Used byUnJS projectsESLint, BabelSecurity tools
Weekly downloads~500K~15M~10M

When to Use Each

Use magic-regexp if:

  • Want readable, composable regex without cryptic syntax
  • Need type-safe capture groups in TypeScript
  • Building regex that will be maintained by a team
  • In the UnJS ecosystem

Use regexp-tree if:

  • Building regex tooling (linters, codemods, analyzers)
  • Need to parse, optimize, or transform regex programmatically
  • Building an ESLint plugin that works with regex
  • Need AST-level regex manipulation

Use safe-regex if:

  • Accepting user-supplied regex patterns
  • Auditing your codebase for ReDoS vulnerabilities
  • Building a WAF or input validation layer
  • Need to prevent catastrophic backtracking

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on magic-regexp v0.8.x, regexp-tree v0.1.x, and safe-regex v2.x.

Compare regex utilities and developer tooling on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.