regex-wand
Version:
Typed regular expressions without giving up readable regex authoring.
362 lines (269 loc) • 11.4 kB
Markdown
# regex-wand
Typed regular expressions without giving up readable regex authoring.
[](https://www.npmjs.com/package/regex-wand)
[](https://github.com/andrew-bierman/regex-wand/actions/workflows/ci.yml)
[](https://andrew-bierman.github.io/regex-wand/)
[](LICENSE)
`regex-wand` lets you write patterns with the composable `magic-regexp` API and
get ArkRegex-powered TypeScript inference on the final `RegExp`.
At runtime, the result is still a native `RegExp`. The extra value is the typed
surface: `.infer`, `.inferCaptures`, `.inferNamedCaptures`, typed `exec()`
groups, literal flags, and `test()` narrowing. ArkRegex is type-only in the
built JavaScript.
## Try It
- [npm package](https://www.npmjs.com/package/regex-wand)
- [Playground](https://andrew-bierman.github.io/regex-wand/)
- [Monorepo README](https://github.com/andrew-bierman/regex-wand#readme)
- [Type-safety guide](docs/type-safety.md)
- [ArkType interop](docs/arktype-interop.md)
- [Support matrix](docs/support.md)
- [Roadmap](docs/roadmap.md)
- [Testing strategy](docs/testing.md)
- [Changelog](CHANGELOG.md)
- [GitHub releases](https://github.com/andrew-bierman/regex-wand/releases)
## Install
```sh
bun add regex-wand
```
```sh
npm install regex-wand
```
`magic-regexp` and `arkregex` are installed with `regex-wand`. They remain part
of the public type surface because Magic Regex primitives are re-exported and
ArkRegex powers the inferred `RegExp` types.
## Quick Start
```ts
import { defineRegex, digit } from "regex-wand"
const route = defineRegex({
match: "exact",
inputs: ["/users/", digit.times.atLeast(1).as("userId")],
})
route.infer satisfies `/users/${number}`
route.inferNamedCaptures.userId satisfies `${number}`
const match = route.exec("/users/42")
match?.groups.userId satisfies string | undefined
declare const value: string
if (route.test(value)) {
value satisfies `/users/${number}`
}
```
## Common Patterns
Exact semver-style match:
```ts
import { defineRegex, digit } from "regex-wand"
const semver = defineRegex({
match: "exact",
inputs: [
digit.times.any().grouped(),
".",
digit.times.any().grouped(),
".",
digit.times.any().grouped(),
],
})
semver.infer satisfies `${number}.${number}.${number}`
semver.inferCaptures satisfies [`${number}`, `${number}`, `${number}`]
```
Contains-style text search:
```ts
import { defineRegex, digit } from "regex-wand"
const ticketId = defineRegex({
inputs: ["id:", digit.times.atLeast(1).grouped()],
})
ticketId.infer satisfies `${string}id:${number}${string}`
ticketId.test("ticket id:8042 is ready")
```
Flags:
```ts
import {
caseInsensitive,
defineRegex,
global,
} from "regex-wand"
const accepted = defineRegex({
flags: [global, caseInsensitive],
match: "exact",
inputs: ["ok"],
})
accepted.flags satisfies "gi"
accepted.infer satisfies "ok" | "oK" | "Ok" | "OK"
```
## Why Not Just Magic Regex Or ArkRegex?
`regex-wand` exists because Magic Regex and ArkRegex solve different parts of
the problem.
Raw regex strings are compact but hard to safely compose:
```ts
const route = /^\/users\/(?<userId>\d{1,})$/
```
Raw Magic Regex gives you readable, composable authoring:
```ts
import { createRegExp, digit } from "magic-regexp"
const route = createRegExp("/users/", digit.times.atLeast(1).as("userId"))
route.test("/users/42")
```
Magic Regex also ships a build-time transform that can compile those
`createRegExp(...)` calls to plain `RegExp` literals.
Raw ArkRegex gives you strong result types from a raw regex string:
```ts
import { regex } from "arkregex"
const route = regex("^/users/(?<userId>\\d{1,})$")
route.infer satisfies `/users/${number}`
route.inferNamedCaptures.userId satisfies `${number}`
```
`regex-wand` keeps the Magic Regex authoring model, adds readable object params,
and returns the ArkRegex-type surface:
```ts
import { defineRegex, digit } from "regex-wand"
const route = defineRegex({
match: "exact",
inputs: ["/users/", digit.times.atLeast(1).as("userId")],
})
route.infer satisfies `/users/${number}`
route.inferNamedCaptures.userId satisfies `${number}`
route.test("/users/42")
```
Use raw `magic-regexp` when you only need composable regex construction. Use raw
`arkregex` when you already have a regex string and want type inference for it.
Use `regex-wand` when you want both authoring ergonomics and result inference.
For new code, prefer `defineRegex({ inputs, match, flags })`. It is more
readable than ordered arguments once definitions need matching mode or flags.
For migration or Magic Regex-style authoring, `createRegExp(...inputs)` mirrors
Magic Regex's positional input API and returns the richer `regex-wand` result.
### Build-Time Transform Note
Magic Regex's transform only recognizes imports from `magic-regexp` and
`magic-regexp/further-magic`. If you do not enable `regex-wand/transform`, direct
`regex-wand` builders are small runtime adapters:
```ts
import { defineRegex, digit } from "regex-wand"
const route = defineRegex({
match: "exact",
inputs: ["/users/", digit.times.atLeast(1).as("userId")],
})
```
If you want Magic Regex's build-time transform in an app, compose with raw Magic
Regex and adapt at the boundary:
```ts
import { createRegExp, digit, exactly } from "magic-regexp"
import { fromMagic } from "regex-wand"
const magicRoute = createRegExp(
exactly("/users/", digit.times.atLeast(1).as("userId"))
.at.lineStart()
.at.lineEnd(),
)
const route = fromMagic(magicRoute)
route.inferNamedCaptures.userId satisfies `${number}`
```
With Magic Regex's transform enabled, the raw `createRegExp(...)` call can
compile away, leaving only the `regex-wand` boundary adapter. ArkRegex remains
type-only in `regex-wand`'s built JavaScript.
If you want direct `regex-wand` builder calls to compile, use
`regex-wand/transform`:
```ts
// vite.config.ts
import { defineConfig } from "vite"
import { RegexWandTransformPlugin } from "regex-wand/transform"
export default defineConfig({
plugins: [RegexWandTransformPlugin.vite()],
})
```
The transform recognizes static `defineRegex`, `createRegExp`,
`createExactRegExp`, `createRegExpWithFlags`, and
`createExactRegExpWithFlags` calls imported from `regex-wand`. Dynamic
expressions are left unchanged. The emitted code preserves the adapter shape, so
`.magic`, `.ark`, and `.toRegExp()` keep working.
For expressions that are valid at runtime but too complex or dynamic for
ArkRegex to infer, use `fromMagicAs` to provide the result type manually:
```ts
import { createRegExp, digit } from "magic-regexp"
import { fromMagicAs } from "regex-wand"
const magicRoute = createRegExp("/users/", digit.times.atLeast(1).as("userId"))
const route = fromMagicAs<
`${string}/users/${number}${string}`,
{ names: { userId: `${number}` } }
>(magicRoute)
```
## API
- `defineRegex({ inputs, match, flags })` is the recommended object-shaped API.
`inputs` is a Magic Regex-compatible tuple, `match` is `"contains"` by
default or `"exact"` for start/end anchoring, and `flags` accepts helper
values, arrays, strings, or Sets. `pattern` is also accepted as an alias for
teams that prefer domain wording.
- `createRegExp(...inputs)` is the Magic Regex-compatible positional builder for
contains-style regexes.
- `createExactRegExp(...inputs)` is a positional helper for start/end anchored
regexes.
- `createRegExpWithFlags(inputs, ...flags)` is a positional helper for Magic
Regex flag helpers.
- `createRegExpWithFlags(inputs, flags)` also accepts flag arrays, flag strings,
and flag Sets.
- `createExactRegExpWithFlags(inputs, ...flags)` combines anchoring and flags.
- `fromMagic(magic)` adapts an existing `MagicRegExp`.
- `fromMagicAs<Pattern, Context>(magic)` adapts an existing `MagicRegExp` with
manually supplied ArkRegex result types.
- `RegexWandTransformPlugin` from `regex-wand/transform` compiles static
`regex-wand` builder calls at build time.
- `WandCompatibilityError` marks strict type-level conversion failures.
All Magic Regex primitives are re-exported from the package.
## Runtime Contract
The returned value is a native `RegExp` with extra typed properties attached:
- `magic` is the original Magic Regex `RegExp`.
- `ark` is an alias to the adapted value for explicit interop.
- `toRegExp()` returns a new plain `RegExp` with the same source and flags.
Plain string inputs are escaped by Magic Regex. Magic Regex fragments keep their
composition behavior. Native `RegExp` rules still apply at runtime, including
duplicate flag errors and `lastIndex` behavior for global or sticky regexes.
## Type Safety Contract
`regex-wand` is strict by default. If the adapter cannot preserve the ArkRegex
type benefit for a Magic Regex value, the return type carries a compatibility
error instead of silently degrading to a plain `RegExp`.
See [docs/type-safety.md](docs/type-safety.md) for the longer version.
See [docs/arktype-interop.md](docs/arktype-interop.md) for when to use ArkType
schema regexes directly.
See [docs/support.md](docs/support.md) for the explicit support matrix and known
technical gaps.
See [docs/roadmap.md](docs/roadmap.md) for the upstream compatibility audit and
future support plan.
## Verification
The package test suite covers both runtime behavior and compile-time inference:
- Vitest runtime tests for builders, exact/contains matching, escaped strings,
flag helpers/arrays/strings/Sets, indices, named groups, optional captures,
lookarounds, backreferences, manual typing, string `RegExp` protocols, and
`lastIndex`.
- Vitest transform tests for direct builder calls, namespaced builder calls, and
dynamic expressions that must be left untouched.
- `tsd` tests for inferred strings, captures, named groups, flags, narrowing,
manual typing, `RegexParts`, and compatibility errors.
- A runtime import guard to ensure built browser code does not import ArkRegex.
- A packed-consumer test that installs the generated tarball into a temporary
project and verifies TypeScript, runtime behavior, and the
`regex-wand/transform` Vite subpath.
- TanStack Intent skill validation.
See [docs/testing.md](docs/testing.md) for the test matrix and what each layer
proves.
Run everything from the monorepo root:
```sh
bun run release:check
```
## Agent Skill
This package ships a TanStack Intent skill under `skills/core/SKILL.md` so coding
agents can load version-aligned usage guidance from the installed npm package.
## Development And Publishing
```sh
bun install
bun run check
bun run test:coverage
```
Useful monorepo commands:
```sh
bun run release:check
bun run publish:dry-run
bun run publish:regex-wand
bun run registry:check
```
Automated publishing lives in the monorepo release workflow:
`.github/workflows/release.yml`.
The workflow publishes public GitHub releases to npm with provenance through npm
trusted publishing. Local publishing remains a fallback for emergencies.
`publish:regex-wand` runs `bun publish --access public` in the package workspace
and should only be used as a local fallback. Bump `packages/regex-wand/package.json`
before publishing because npm does not allow republishing the same version.