npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@feojs/regex

v0.0.1-alpha.0

Published

A safe, fast regex engine for JavaScript hotspots, powered by Rust and WebAssembly.

Readme

@feojs/regex

A safe, fast regex engine for JavaScript hotspots, powered by Rust and WebAssembly.

@feojs/regex is designed for user-controlled or performance-sensitive matching paths where JavaScript's backtracking RegExp engine can stall the event loop. It uses Rust's regex crate through WebAssembly, which means unsupported high-risk features such as look-around and backreferences fail at compile time instead of falling back to unsafe behavior.

Install

pnpm add @feojs/regex

This package is experimental and published under the alpha dist-tag while the first API is being validated.

Requirements:

  • Node.js >=18 for the Node.js entry.
  • A bundler with top-level await support for @feojs/regex/browser.
  • A server that can serve .wasm assets. application/wasm is preferred for faster streaming initialization, but the browser entry falls back when the MIME type is not set.

Usage

Node.js:

import { Regex, RegexSet, isMatch } from "@feojs/regex";

const regex = new Regex("^(a+)+$");
const secrets = new RegexSet([
  { name: "password", pattern: "password\\s*=" },
  { name: "apiKey", pattern: "api[_-]?key\\s*=" },
  { name: "token", pattern: "token\\s*=" },
], "i");

regex.isMatch("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!");
secrets.matches("api-key=abc");
isMatch("\\bfeo\\w+", "Ship FeoJS today", "i");

Browser/bundler:

import { Regex } from "@feojs/regex/browser";

const email = new Regex("[\\w.+-]+@[\\w.-]+\\.\\w+");

email.isMatch("[email protected]");

Compiled regexes are the preferred API for hot paths:

import { Regex } from "@feojs/regex";

const email = new Regex("[\\w.+-]+@[\\w.-]+\\.\\w+");

for (const line of lines) {
  if (email.isMatch(line)) {
    // handle match
  }
}

API

const regex = new Regex(pattern, flags?);
const set = new RegexSet(patterns, flags?);

regex.isMatch(input);
regex.isFullMatch(input);
regex.fullMatch(input);
regex.test(input);
regex.find(input);
regex.findAll(input, options?);
regex.scan(input, options?);
regex.extract(input, options?);
regex.filter(inputs);
regex.reject(inputs);
regex.captures(input);
regex.capturesAll(input, options?);
regex.groups(input);
regex.groupsAll(input, options?);
regex.count(input);
regex.replaceAll(input, replacement);
regex.redact(input, replacement?);
regex.split(input, options?);

set.isMatch(input);
set.test(input);
set.matches(input);
set.classify(inputs, options?);
set.scanLines(input, options?);
set.scanFiles(files, options?);
set.filter(inputs);
set.reject(inputs);
set.redact(input, replacement?);
set.firstMatch(input);
set.matchedPatterns(input);
set.firstPattern(input);
set.matchedRules(input);
set.firstRule(input);
set.matchedIds(input);
set.firstId(input);
set.matchedNames(input);
set.firstName(input);

Convenience helpers are available for one-off calls:

compile(pattern, flags?);
compileSet(patterns, flags?);
compileRules(config, flags?);
auditPatterns(patterns, flags?);
isMatch(pattern, input, flags?);
isFullMatch(pattern, input, flags?);
find(pattern, input, flags?);
findAll(pattern, input, flagsOrOptions?, options?);
scan(pattern, input, flagsOrOptions?, options?);
extract(pattern, input, flagsOrOptions?, options?);
filter(pattern, inputs, flags?);
reject(pattern, inputs, flags?);
captures(pattern, input, flags?);
capturesAll(pattern, input, flagsOrOptions?, options?);
groups(pattern, input, flags?);
groupsAll(pattern, input, flagsOrOptions?, options?);
count(pattern, input, flags?);
replaceAll(pattern, input, replacement, flags?);
redact(pattern, input, replacement?, flags?);
split(pattern, input, flagsOrOptions?, options?);
matchesAny(patterns, input, flags?);
matchesSet(patterns, input, flags?);
scanLines(patterns, input, flagsOrOptions?, options?);
scanFiles(patterns, files, flagsOrOptions?, options?);
summarizeScan(rows, options?);
toGitHubAnnotations(rows, options?);
toTextReport(rows, options?);
toSarif(rows, options?);
escape(input);

Migration helpers:

isSupported(pattern, flags?);
tryCompile(pattern, flags?);
tryCompileSet(patterns, flags?);
tryCompileRules(config, flags?);
auditPatterns(patterns, flags?);

tryCompile() returns { ok: true, regex } or { ok: false, error }. The error is a RegexSyntaxError with pattern, flags, and cause fields. tryCompileSet() returns { ok: true, regexSet } or { ok: false, error }. compileRules() accepts either an array of rules or { flags, rules }, matching the reusable JSON shape supported by the CLI. tryCompileRules() returns the same shape as tryCompileSet(). auditPatterns() returns { total, supported, unsupported, results } for migration reports.

Common Scenarios

Validation:

const slug = new Regex("[a-z0-9-]{1,80}");

slug.isFullMatch("feojs-regex");
slug.isFullMatch("prefix/feojs-regex");
// false

Use isFullMatch() for validation when the whole input must match. It avoids forcing every caller to remember anchors.

Batch validation:

const slug = new Regex("^[a-z0-9-]{1,80}$");
const candidates = ["feojs-regex", "invalid/slug", "also invalid", 2026];

slug.filter(candidates);
// ["feojs-regex", "2026"]

slug.reject(candidates);
// ["invalid/slug", "also invalid"]

Use filter() and reject() when you already have an array of candidate strings to keep or drop, such as imported IDs, route params, log lines, and form values. They use isMatch() semantics, so anchor validation patterns when the whole input must match.

Migration checks:

const report = auditPatterns([
  { name: "slug", pattern: "[a-z0-9-]{1,80}" },
  { name: "legacy backreference", pattern: "(\\w+)\\1" },
]);

report.unsupported;
// 1

For auditing many existing patterns, see docs/migration-guide.md and examples/audit-patterns.mjs.

Extraction with named groups:

const email = new Regex("(?<name>[a-z]+)@(?<host>[\\w.]+)");

email.groups("[email protected]");
// {
//   name: { text: "carbon", start: 0, end: 6 },
//   host: { text: "feojs.dev", start: 7, end: 16 }
// }

Repeated extraction:

const email = new Regex("(?<name>[a-z]+)@(?<host>[\\w.]+)");

email.groupsAll("[email protected] [email protected]");
// [
//   { name: { text: "carbon", start: 0, end: 6 }, host: { text: "feojs.dev", start: 7, end: 16 } },
//   { name: { text: "ada", start: 21, end: 24 }, host: { text: "example.com", start: 25, end: 36 } }
// ]

email.groupsAll("[email protected] [email protected]", { limit: 1 });

Scanning:

const token = new Regex("\\d+");

token.scan("v1 v22 v333");
// [
//   { text: "1", start: 1, end: 2 },
//   { text: "22", start: 4, end: 6 },
//   { text: "333", start: 8, end: 11 }
// ]

token.scan("v1 v22 v333", { limit: 2 });
// [
//   { text: "1", start: 1, end: 2 },
//   { text: "22", start: 4, end: 6 }
// ]

token.extract("v1 v22 v333");
// ["1", "22", "333"]

Use findAll() or scan() when you need offsets. Use extract() when you only need matched text. { limit } is useful when you only need a preview or the first few findings from large input. limit must be a non-negative integer.

Redaction:

const email = new Regex("[\\w.+-]+@[\\w.-]+\\.\\w+");

email.redact("email [email protected]");
// "email [REDACTED]"

Delimiter splitting:

const delimiter = new Regex("[,;]\\s*");

delimiter.split("alpha, beta;gamma");
// ["alpha", "beta", "gamma"]

delimiter.split("alpha, beta;gamma", { limit: 2 });
// ["alpha", "beta;gamma"]

Use split() for regex delimiters when you already chose this engine for predictable syntax and latency. limit means "return at most this many fields"; when the input has more delimiters, the last returned field keeps the remaining input.

Multi-rule scanning:

const secrets = new RegexSet(["password\\s*=", "api[_-]?key\\s*=", "token\\s*="], "i");

secrets.matches("token=abc password=def");
// [0, 2]

secrets.matchedPatterns("api-key=abc");
// ["api[_-]?key\\s*="]

Named rules:

import { compileRules, summarizeScan, toGitHubAnnotations, toSarif, toTextReport } from "@feojs/regex";

const secrets = compileRules({
  flags: "i",
  rules: [
    { id: "secret.password", name: "password", pattern: "password\\s*=", severity: "high" },
    {
      id: "secret.api_key",
      name: "apiKey",
      pattern: "api[_-]?key\\s*=",
      severity: "high",
      message: "Potential API key assignment.",
      tags: ["secret", "credential"]
    },
    { id: "secret.token", name: "token", pattern: "token\\s*=", severity: "medium" },
  ],
});

secrets.matchedIds("api-key=abc token=def");
// ["secret.api_key", "secret.token"]

secrets.matchedNames("api-key=abc token=def");
// ["apiKey", "token"]

secrets.firstId("api-key=abc token=def");
// "secret.api_key"

secrets.firstName("api-key=abc token=def");
// "apiKey"

secrets.matchedRules("api-key=abc");
// [{
//   index: 1,
//   id: "secret.api_key",
//   name: "apiKey",
//   pattern: "api[_-]?key\\s*=",
//   severity: "high",
//   message: "Potential API key assignment.",
//   tags: ["secret", "credential"]
// }]

secrets.filter(["ok", "api-key=abc", "token=def"]);
// ["api-key=abc", "token=def"]

secrets.classify(["ok", "api-key=abc token=def"], { matchedOnly: true });
// [
//   {
//     input: "api-key=abc token=def",
//     matched: true,
//     matches: [1, 2],
//     ids: ["secret.api_key", "secret.token"],
//     names: ["apiKey", "token"],
//     firstId: "secret.api_key",
//     firstName: "apiKey",
//     ...
//   }
// ]

secrets.scanLines("ok\napi-key=abc token=def", { limit: 1 });
// [
//   {
//     line: "api-key=abc token=def",
//     lineNumber: 2,
//     start: 3,
//     end: 24,
//     names: ["apiKey", "token"],
//     ids: ["secret.api_key", "secret.token"],
//     firstId: "secret.api_key",
//     firstName: "apiKey",
//     ...
//   }
// ]

Multi-file scans:

const rows = secrets.scanFiles([
  { uri: "src/app.log", input: appLog },
  { uri: "dist/app.log", input: builtLog },
  { uri: "src/worker.txt", input: workerText },
  { uri: "src/worker.log", input: workerLog },
], {
  include: "*.log",
  exclude: "dist/**",
  limit: 100,
});

rows.map((row) => row.uri);
// ["src/app.log", "src/worker.log"]

Precise findings:

const rows = secrets.scanLines("prefix api-key=abc token=def", { withMatches: true });

rows[0].findings;
// [
//   { rule: { id: "secret.api_key", ... }, match: { text: "api-key=", start: 7, end: 15 } },
//   { rule: { id: "secret.token", ... }, match: { text: "token=", start: 19, end: 25 } }
// ]

Context lines:

const rows = secrets.scanLines("boot\nok\napi-key=abc\nnext", {
  context: { before: 1, after: 1 },
});

rows[0].context;
// {
//   before: [{ line: "ok", lineNumber: 2, start: 5, end: 7 }],
//   after: [{ line: "next", lineNumber: 4, start: 20, end: 24 }]
// }

CI/code scanning reports:

const rows = secrets.scanLines(logText);
const summary = summarizeScan(rows, { failOn: "warning" });
const annotations = toGitHubAnnotations(rows);
const text = toTextReport(rows);
const sarif = toSarif(rows, { uri: "app.log", toolName: "feojs-regex" });

summary.shouldFail;
// true when the scan contains warning-or-higher findings

sarif.version;
// "2.1.0"

annotations.split("\n")[0];
// "::error file=stdin,line=2,..."

text.split("\n")[0];
// "2 findings across 2 matched rows"

RegexSet returns matches in rule order. Use firstRule(), firstId(), or firstName() when your rules are ordered by priority. Rule objects may include id, name, severity, message, tags, and metadata; these fields are preserved in matchedRules(), classify(), scanLines(), scanFiles(), summarizeScan(), SARIF reports, GitHub annotations, and text reports. A rule may also provide a string replacement for redaction; RegexSet#redact() applies rules in declaration order, with a rule-level replacement overriding the method fallback. Use filter() or reject() when you need to keep or drop log lines, events, routes, or imported records that match any rule. Use classify() when you need a per-input report with rule indexes, patterns, rule metadata, ids, names, and first-match fields. Pass withMatches: true to classify(), scanLines(), or scanFiles() when reports need exact matched spans. Pass context: n or context: { before, after } to scanLines() or scanFiles() when terminal output, review UIs, or audit screens need surrounding lines for triage. Use scanLines() when the input is already a log, config file, or multi-line report and callers need line numbers plus offsets. Use scanFiles() when a CLI, bundler plugin, or CI wrapper already has { uri, input } pairs and needs one row stream with optional include/exclude filtering. Use summarizeScan() when a CI job, audit screen, or CLI wrapper needs counts by severity, rule, and file plus a shouldFail decision. Use toGitHubAnnotations() when a GitHub Actions job should emit inline workflow annotations without SARIF upload setup. Use toTextReport() when a local CLI or script should print grep-like findings for humans. Use Regex when you need match text, offsets, captures, groups, or replacement.

Use compileRules() when the same rule config should be shared between app code, browser tooling, and the CLI. Passing an explicit second flags argument overrides config.flags.

CLI

The package also ships a small Node.js CLI for log and text scans:

feojs-regex scan-lines --rule 'apiKey=api[_-]?key\s*=' --flags i app.log worker.log

It prints JSON reports from RegexSet#scanLines() and adds a uri field for each input file. Omit files or pass - to read stdin:

cat app.log | feojs-regex scan-lines --rule 'apiKey=api[_-]?key\s*=' --jsonl

For reusable scans, keep rules in JSON:

{
  "flags": "i",
  "rules": [
    {
      "id": "secret.api_key",
      "name": "apiKey",
      "pattern": "api[_-]?key\\s*=",
      "severity": "high",
      "message": "Potential API key assignment.",
      "tags": ["secret", "credential"]
    },
    { "id": "secret.bearer", "name": "bearer", "pattern": "bearer\\s+[A-Za-z0-9._-]+", "severity": "high" }
  ]
}
feojs-regex scan-lines --rules rules.json app.log worker.log
feojs-regex scan-lines --rules rules.json --include '*.log' --exclude 'dist/**' app.log dist/app.log worker.log
feojs-regex scan-lines --rules rules.json --context 2 --text app.log worker.log
feojs-regex scan-lines --rules rules.json --text app.log worker.log
feojs-regex scan-lines --rules rules.json --sarif app.log worker.log > results.sarif
feojs-regex scan-lines --rules rules.json --github --fail-on warning app.log worker.log
feojs-regex redact --rules rules.json app.log > redacted.log
cat app.log | feojs-regex redact --rules rules.json --replacement '[MASKED]'

Useful options:

  • --rule name=pattern: add a named rule
  • --pattern pattern: add an unnamed pattern
  • --rules file: load a JSON array of rules or { "flags", "rules" }
  • --flags flags: shared flags for all rules
  • --all: include unmatched lines
  • --limit n: return at most n rows
  • --include glob: scan only file paths matching glob; repeatable
  • --exclude glob: skip file paths matching glob; repeatable
  • --matches: include precise finding spans in JSON or JSONL rows
  • --context n, -C n: include n lines before and after each matched line
  • --before-context n, -B n: include n lines before each matched line
  • --after-context n, -A n: include n lines after each matched line
  • --jsonl: print one JSON object per line
  • --text: print a human-readable text report
  • --summary: print a JSON summary instead of row-level results
  • --sarif: print a SARIF 2.1.0 report
  • --github: print GitHub Actions workflow annotations
  • --fail-on level: exit 1 when findings meet error, warning, note, or none

When multiple files are provided, --limit applies to the combined output in argument order. --include and --exclude use *, ?, and **; patterns without / match the basename, so *.log matches src/app.log. SARIF, GitHub annotation, and text output automatically use precise finding spans. Use --matches when JSON or JSONL output should include the same span data. Context options add structured context to JSON/JSONL rows and grep-like context lines to text output; SARIF and GitHub annotations keep their standard output shape.

For CI gates, combine --summary and --fail-on:

feojs-regex scan-lines --rules rules.json --summary --fail-on warning app.log worker.log

Supported Flags

Supported flags:

  • i: case-insensitive
  • m: multi-line
  • s: dot matches newline
  • u: accepted because Rust regex is Unicode-aware by default
  • g, y: accepted for migration, but treated as iteration semantics rather than pattern semantics

Unsupported syntax throws during compilation. This is intentional. The package should not silently fall back to JavaScript RegExp, because predictable matching behavior is the product promise.

Benchmarks

pnpm install
pnpm bench

pnpm bench also writes a machine-readable report to tmp/benchmarks/latest.json.

For JSON on stdout:

pnpm bench:json

To verify the npm package contents before publishing:

pnpm pack:check

To verify the packed tarball in a clean consumer project:

pnpm smoke:pack

For a slower headline stress case:

REDOS_SIZE=30 pnpm bench

The benchmark suite includes:

  • a catastrophic-backtracking style miss: ^(a+)+$ against many a characters plus !
  • an email-like match
  • a supported pattern audit batch
  • a rules config compile pass
  • a numeric token count over large input
  • a full token scan over large input
  • a text-only token extraction over large input
  • a limited token scan over large input
  • a full-match slug validation batch
  • a batch slug filtering pass
  • a named capture extraction batch
  • a delimiter normalization split
  • a 130-rule secret scan using RegexSet
  • a priority rule classification using RegexSet#firstName
  • a multi-rule line filtering pass using RegexSet#filter
  • a multi-rule line classification pass using RegexSet#classify
  • a multi-rule log scan using RegexSet#scanLines
  • a multi-rule precise log scan using RegexSet#scanLines with withMatches
  • a multi-rule contextual log scan using RegexSet#scanLines with context
  • a multi-rule redaction pass using RegexSet#redact
  • a multi-file filtered scan using RegexSet#scanFiles
  • a multi-rule SARIF report using RegexSet#scanLines and toSarif
  • a multi-rule scan summary using RegexSet#scanLines and summarizeScan
  • a multi-rule GitHub annotation report using RegexSet#scanLines and toGitHubAnnotations
  • a multi-rule text report using RegexSet#scanLines and toTextReport

The ReDoS-style benchmark is the headline case, but ordinary scan cases are kept in the suite so API and WASM boundary costs stay visible.

Runtime Targets

Current package targets:

  • Node.js: @feojs/regex
  • Browser/bundler: @feojs/regex/browser

Bundler support is verified with a Vite fixture. The browser entry uses the wasm-pack --target web output and initializes the WASM module with top-level await.

The release smoke test installs the packed tarball into a temporary consumer project and verifies:

  • Node.js ESM import from @feojs/regex
  • Node.js CommonJS require from @feojs/regex
  • TypeScript NodeNext type checking for @feojs/regex and @feojs/regex/browser
  • Vite browser build from @feojs/regex/browser

See docs/release-readiness.md for the current release checklist and non-goals.