Quick-and-dirty semver

import { createRegExp, exactly, maybe, oneOrMore, digit, char } from 'magic-regexp'

createRegExp(
  oneOrMore(digit).groupedAs('major'),
  '.',
  oneOrMore(digit).groupedAs('minor'),
  maybe('.', oneOrMore(char).groupedAs('patch'))
)
// /(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>.+))?/

References to previously captured groups using the group name

import assert from 'node:assert'
import { createRegExp, wordChar, char, oneOrMore } from 'magic-regexp'

const TENET_RE = createRegExp(
  wordChar
    .groupedAs('firstChar')
    .and(wordChar.groupedAs('secondChar'))
    .and(oneOrMore(char))
    .and.referenceTo('secondChar')
    .and.referenceTo('firstChar')
)
// /(?<firstChar>\w)(?<secondChar>\w).+\k<secondChar>\k<firstChar>/

assert.equal(TENET_RE.test('TEN<==O==>NET'), true)

Type-level RegExp match and replace result (experimental)

This feature is still experimental, to try it please import createRegExp and all Input helpers from magic-regexp/further-magic instead of magic-regexp.

When matching or replacing with literal string such as magic-regexp v3.2.5.beta.1 just release!

import { 
  createRegExp, 
  oneOrMore, 
  exactly, 
  anyOf, 
  digit, 
  wordChar 
} from 'magic-regexp/further-magic'

const literalString = 'magic-regexp 3.2.5.beta.1 just release!'

const semverRegExp = createRegExp(
  oneOrMore(digit)
    .as('major')
    .and('.')
    .and(oneOrMore(digit).as('minor'))
    .and(
      exactly('.')
        .and(oneOrMore(anyOf(wordChar, '.')).groupedAs('patch'))
        .optionally()
    )
)

// `String.match()` example
const matchResult = literalString.match(semverRegExp)
matchResult[0] // "3.2.5.beta.1"
matchResult[3] // "5.beta.1"
matchResult.length // 4
matchResult.index // 14
matchResult.groups
//   groups: {
//     major: "3";
//     minor: "2";
//     patch: "5.beta.1";
//  }

// `String.replace()` example
const replaceResult = literalString.replace(
    semverRegExp,
    `minor version "$2" brings many great DX improvements, while patch "$<patch>" fix some bugs and it's`
  )

replaceResult // "magic-regexp minor version \"2\" brings many great DX improvements, while patch \"5.beta.1\" fix some bugs and it's just release!"

When matching dynamic string, the result will be union of possible matches

let myString = 'dynamic'

const RegExp = createRegExp(exactly('foo').or('bar').groupedAs('g1'))
const matchAllResult = myString.match(RegExp)

matchAllResult
// null | RegExpMatchResult<{
//     matched: ["bar", "bar"] | ["foo", "foo"];
//     namedCaptures: ["g1", "bar"] | ["g1", "foo"];
//     input: string;
//     restInput: undefined;
// }> 
matchAllResult?.[0] // ['foo', 'foo'] | ['bar', 'bar']
matchAllResult?.length // 2 | undefined
matchAllResult?.groups
// groups: {
//     g1: "foo" | "bar";
// } | undefined