mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
add comments to everything potentially consumer facing (#3127)
* add comments to everything potentially consumer facing * rework smtp --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* Converts an unknown thrown value into an Error instance.
|
||||
* If `e` is already an Error, wraps it; if a string, uses it as the message;
|
||||
* otherwise JSON-serializes it as the error message.
|
||||
*
|
||||
* @param e - The unknown value to convert
|
||||
* @returns An Error instance
|
||||
*/
|
||||
export const asError = (e: unknown) => {
|
||||
if (e instanceof Error) {
|
||||
return new Error(e as any)
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/**
|
||||
* Performs a deep structural equality check across all provided arguments.
|
||||
* Returns true only if every argument is deeply equal to every other argument.
|
||||
* Handles primitives, arrays, and plain objects recursively.
|
||||
*
|
||||
* @param args - Two or more values to compare for deep equality
|
||||
* @returns True if all arguments are deeply equal
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* deepEqual({ a: 1 }, { a: 1 }) // true
|
||||
* deepEqual([1, 2], [1, 2], [1, 2]) // true
|
||||
* deepEqual({ a: 1 }, { a: 2 }) // false
|
||||
* ```
|
||||
*/
|
||||
export function deepEqual(...args: unknown[]) {
|
||||
const objects = args.filter(
|
||||
(x): x is object => typeof x === 'object' && x !== null,
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* Computes the partial difference between two values.
|
||||
* Returns `undefined` if the values are equal, or `{ diff }` containing only the changed parts.
|
||||
* For arrays, the diff contains only items in `next` that have no deep-equal counterpart in `prev`.
|
||||
* For objects, the diff contains only keys whose values changed.
|
||||
*
|
||||
* @param prev - The original value
|
||||
* @param next - The updated value
|
||||
* @returns An object containing the diff, or `undefined` if the values are equal
|
||||
*/
|
||||
export function partialDiff<T>(
|
||||
prev: T,
|
||||
next: T,
|
||||
@@ -46,6 +56,14 @@ export function partialDiff<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deeply merges multiple values together. Objects are merged key-by-key recursively.
|
||||
* Arrays are merged by appending items that are not already present (by deep equality).
|
||||
* Primitives are resolved by taking the last argument.
|
||||
*
|
||||
* @param args - The values to merge, applied left to right
|
||||
* @returns The merged result
|
||||
*/
|
||||
export function deepMerge(...args: unknown[]): unknown {
|
||||
const lastItem = (args as any)[args.length - 1]
|
||||
if (typeof lastItem !== 'object' || !lastItem) return lastItem
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { DefaultString } from '../actions/input/inputSpecTypes'
|
||||
import { getRandomString } from './getRandomString'
|
||||
|
||||
/**
|
||||
* Resolves a DefaultString spec into a concrete string value.
|
||||
* If the spec is a plain string, returns it directly.
|
||||
* If it is a random-string specification, generates a random string accordingly.
|
||||
*
|
||||
* @param defaultSpec - A string literal or a random-string generation spec
|
||||
* @returns The resolved default string value
|
||||
*/
|
||||
export function getDefaultString(defaultSpec: DefaultString): string {
|
||||
if (typeof defaultSpec === 'string') {
|
||||
return defaultSpec
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
import { ExtendedVersion } from '../exver'
|
||||
|
||||
/**
|
||||
* A vertex (node) in a directed graph, holding metadata and a list of connected edges.
|
||||
* @typeParam VMetadata - The type of metadata stored on vertices
|
||||
* @typeParam EMetadata - The type of metadata stored on edges
|
||||
*/
|
||||
export type Vertex<VMetadata = null, EMetadata = null> = {
|
||||
metadata: VMetadata
|
||||
edges: Array<Edge<EMetadata, VMetadata>>
|
||||
}
|
||||
|
||||
/**
|
||||
* A directed edge connecting two vertices, with its own metadata.
|
||||
* @typeParam EMetadata - The type of metadata stored on edges
|
||||
* @typeParam VMetadata - The type of metadata stored on the connected vertices
|
||||
*/
|
||||
export type Edge<EMetadata = null, VMetadata = null> = {
|
||||
metadata: EMetadata
|
||||
from: Vertex<VMetadata, EMetadata>
|
||||
to: Vertex<VMetadata, EMetadata>
|
||||
}
|
||||
|
||||
/**
|
||||
* A directed graph data structure supporting vertex/edge management and graph traversal algorithms
|
||||
* including breadth-first search, reverse BFS, and shortest path computation.
|
||||
*
|
||||
* @typeParam VMetadata - The type of metadata stored on vertices
|
||||
* @typeParam EMetadata - The type of metadata stored on edges
|
||||
*/
|
||||
export class Graph<VMetadata = null, EMetadata = null> {
|
||||
private readonly vertices: Array<Vertex<VMetadata, EMetadata>> = []
|
||||
constructor() {}
|
||||
/**
|
||||
* Serializes the graph to a JSON string for debugging.
|
||||
* @param metadataRepr - Optional function to transform metadata values before serialization
|
||||
* @returns A pretty-printed JSON string of the graph structure
|
||||
*/
|
||||
dump(
|
||||
metadataRepr: (metadata: VMetadata | EMetadata) => any = (a) => a,
|
||||
): string {
|
||||
@@ -30,6 +52,13 @@ export class Graph<VMetadata = null, EMetadata = null> {
|
||||
2,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Adds a new vertex to the graph, optionally connecting it to existing vertices via edges.
|
||||
* @param metadata - The metadata to attach to the new vertex
|
||||
* @param fromEdges - Edges pointing from existing vertices to this new vertex
|
||||
* @param toEdges - Edges pointing from this new vertex to existing vertices
|
||||
* @returns The newly created vertex
|
||||
*/
|
||||
addVertex(
|
||||
metadata: VMetadata,
|
||||
fromEdges: Array<Omit<Edge<EMetadata, VMetadata>, 'to'>>,
|
||||
@@ -60,6 +89,11 @@ export class Graph<VMetadata = null, EMetadata = null> {
|
||||
this.vertices.push(vertex)
|
||||
return vertex
|
||||
}
|
||||
/**
|
||||
* Returns a generator that yields all vertices matching the predicate.
|
||||
* @param predicate - A function to test each vertex
|
||||
* @returns A generator of matching vertices
|
||||
*/
|
||||
findVertex(
|
||||
predicate: (vertex: Vertex<VMetadata, EMetadata>) => boolean,
|
||||
): Generator<Vertex<VMetadata, EMetadata>, null> {
|
||||
@@ -74,6 +108,13 @@ export class Graph<VMetadata = null, EMetadata = null> {
|
||||
}
|
||||
return gen()
|
||||
}
|
||||
/**
|
||||
* Adds a directed edge between two existing vertices.
|
||||
* @param metadata - The metadata to attach to the edge
|
||||
* @param from - The source vertex
|
||||
* @param to - The destination vertex
|
||||
* @returns The newly created edge
|
||||
*/
|
||||
addEdge(
|
||||
metadata: EMetadata,
|
||||
from: Vertex<VMetadata, EMetadata>,
|
||||
@@ -88,6 +129,11 @@ export class Graph<VMetadata = null, EMetadata = null> {
|
||||
edge.to.edges.push(edge)
|
||||
return edge
|
||||
}
|
||||
/**
|
||||
* Performs a breadth-first traversal following outgoing edges from the starting vertex or vertices.
|
||||
* @param from - A starting vertex, or a predicate to select multiple starting vertices
|
||||
* @returns A generator yielding vertices in BFS order
|
||||
*/
|
||||
breadthFirstSearch(
|
||||
from:
|
||||
| Vertex<VMetadata, EMetadata>
|
||||
@@ -139,6 +185,11 @@ export class Graph<VMetadata = null, EMetadata = null> {
|
||||
return rec(from)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Performs a reverse breadth-first traversal following incoming edges from the starting vertex or vertices.
|
||||
* @param to - A starting vertex, or a predicate to select multiple starting vertices
|
||||
* @returns A generator yielding vertices in reverse BFS order
|
||||
*/
|
||||
reverseBreadthFirstSearch(
|
||||
to:
|
||||
| Vertex<VMetadata, EMetadata>
|
||||
@@ -190,6 +241,12 @@ export class Graph<VMetadata = null, EMetadata = null> {
|
||||
return rec(to)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Finds the shortest path (by edge count) between two vertices using BFS.
|
||||
* @param from - The starting vertex, or a predicate to select starting vertices
|
||||
* @param to - The target vertex, or a predicate to identify target vertices
|
||||
* @returns An array of edges forming the shortest path, or `null` if no path exists
|
||||
*/
|
||||
shortestPath(
|
||||
from:
|
||||
| Vertex<VMetadata, EMetadata>
|
||||
|
||||
@@ -15,6 +15,21 @@ const digitsMs = (digits: string | null, multiplier: number) => {
|
||||
const divideBy = multiplier / Math.pow(10, digits.length - 1)
|
||||
return Math.round(value * divideBy)
|
||||
}
|
||||
/**
|
||||
* Converts a human-readable time string to milliseconds.
|
||||
* Supports units: `ms`, `s`, `m`, `h`, `d`. If a number is passed, it is returned as-is.
|
||||
*
|
||||
* @param time - A time string (e.g. `"500ms"`, `"1.5s"`, `"2h"`) or a numeric millisecond value
|
||||
* @returns The time in milliseconds, or `undefined` if `time` is falsy
|
||||
* @throws Error if the string format is invalid
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* inMs("2s") // 2000
|
||||
* inMs("1.5h") // 5400000
|
||||
* inMs(500) // 500
|
||||
* ```
|
||||
*/
|
||||
export const inMs = (time?: string | number) => {
|
||||
if (typeof time === 'number') return time
|
||||
if (!time) return undefined
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* Represents an IPv4 or IPv6 address as raw octets with arithmetic and comparison operations.
|
||||
*
|
||||
* IPv4 addresses have 4 octets, IPv6 addresses have 16 octets.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const ip = IpAddress.parse("192.168.1.1")
|
||||
* const next = ip.add(1) // 192.168.1.2
|
||||
* ```
|
||||
*/
|
||||
export class IpAddress {
|
||||
private renderedOctets: number[]
|
||||
protected constructor(
|
||||
@@ -6,6 +17,13 @@ export class IpAddress {
|
||||
) {
|
||||
this.renderedOctets = [...octets]
|
||||
}
|
||||
/**
|
||||
* Parses an IP address string into an IpAddress instance.
|
||||
* Supports both IPv4 dotted-decimal and IPv6 colon-hex notation (including `::` shorthand).
|
||||
* @param address - The IP address string to parse
|
||||
* @returns A new IpAddress instance
|
||||
* @throws Error if the address format is invalid
|
||||
*/
|
||||
static parse(address: string): IpAddress {
|
||||
let octets
|
||||
if (address.includes(':')) {
|
||||
@@ -39,6 +57,12 @@ export class IpAddress {
|
||||
}
|
||||
return new IpAddress(octets, address)
|
||||
}
|
||||
/**
|
||||
* Creates an IpAddress from a raw octet array.
|
||||
* @param octets - Array of 4 octets (IPv4) or 16 octets (IPv6), each 0-255
|
||||
* @returns A new IpAddress instance
|
||||
* @throws Error if the octet array length is not 4 or 16, or any octet exceeds 255
|
||||
*/
|
||||
static fromOctets(octets: number[]) {
|
||||
if (octets.length == 4) {
|
||||
if (octets.some((o) => o > 255)) {
|
||||
@@ -66,15 +90,24 @@ export class IpAddress {
|
||||
throw new Error('invalid ip address')
|
||||
}
|
||||
}
|
||||
/** Returns true if this is an IPv4 address (4 octets). */
|
||||
isIpv4(): boolean {
|
||||
return this.octets.length === 4
|
||||
}
|
||||
/** Returns true if this is an IPv6 address (16 octets). */
|
||||
isIpv6(): boolean {
|
||||
return this.octets.length === 16
|
||||
}
|
||||
/** Returns true if this is a public IPv4 address (not in any private range). */
|
||||
isPublic(): boolean {
|
||||
return this.isIpv4() && !PRIVATE_IPV4_RANGES.some((r) => r.contains(this))
|
||||
}
|
||||
/**
|
||||
* Returns a new IpAddress incremented by `n`.
|
||||
* @param n - The integer amount to add (fractional part is truncated)
|
||||
* @returns A new IpAddress with the result
|
||||
* @throws Error on overflow
|
||||
*/
|
||||
add(n: number): IpAddress {
|
||||
let octets = [...this.octets]
|
||||
n = Math.floor(n)
|
||||
@@ -92,6 +125,12 @@ export class IpAddress {
|
||||
}
|
||||
return IpAddress.fromOctets(octets)
|
||||
}
|
||||
/**
|
||||
* Returns a new IpAddress decremented by `n`.
|
||||
* @param n - The integer amount to subtract (fractional part is truncated)
|
||||
* @returns A new IpAddress with the result
|
||||
* @throws Error on underflow
|
||||
*/
|
||||
sub(n: number): IpAddress {
|
||||
let octets = [...this.octets]
|
||||
n = Math.floor(n)
|
||||
@@ -109,6 +148,11 @@ export class IpAddress {
|
||||
}
|
||||
return IpAddress.fromOctets(octets)
|
||||
}
|
||||
/**
|
||||
* Compares this address to another, returning -1, 0, or 1.
|
||||
* @param other - An IpAddress instance or string to compare against
|
||||
* @returns -1 if this < other, 0 if equal, 1 if this > other
|
||||
*/
|
||||
cmp(other: string | IpAddress): -1 | 0 | 1 {
|
||||
if (typeof other === 'string') other = IpAddress.parse(other)
|
||||
const len = Math.max(this.octets.length, other.octets.length)
|
||||
@@ -123,6 +167,7 @@ export class IpAddress {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
/** The string representation of this IP address (e.g. `"192.168.1.1"` or `"::1"`). Cached and recomputed only when octets change. */
|
||||
get address(): string {
|
||||
if (
|
||||
this.renderedOctets.length === this.octets.length &&
|
||||
@@ -160,6 +205,17 @@ export class IpAddress {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an IP network (CIDR notation) combining an IP address with a prefix length.
|
||||
* Extends IpAddress with network-specific operations like containment checks and broadcast calculation.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const net = IpNet.parse("192.168.1.0/24")
|
||||
* net.contains("192.168.1.100") // true
|
||||
* net.broadcast() // 192.168.1.255
|
||||
* ```
|
||||
*/
|
||||
export class IpNet extends IpAddress {
|
||||
private constructor(
|
||||
octets: number[],
|
||||
@@ -168,18 +224,35 @@ export class IpNet extends IpAddress {
|
||||
) {
|
||||
super(octets, address)
|
||||
}
|
||||
/**
|
||||
* Creates an IpNet from an IpAddress and prefix length.
|
||||
* @param ip - The base IP address
|
||||
* @param prefix - The CIDR prefix length (0-32 for IPv4, 0-128 for IPv6)
|
||||
* @returns A new IpNet instance
|
||||
* @throws Error if prefix exceeds the address bit length
|
||||
*/
|
||||
static fromIpPrefix(ip: IpAddress, prefix: number): IpNet {
|
||||
if (prefix > ip.octets.length * 8) {
|
||||
throw new Error('invalid prefix')
|
||||
}
|
||||
return new IpNet(ip.octets, prefix, ip.address)
|
||||
}
|
||||
/**
|
||||
* Parses a CIDR notation string (e.g. `"192.168.1.0/24"`) into an IpNet.
|
||||
* @param ipnet - The CIDR string to parse
|
||||
* @returns A new IpNet instance
|
||||
*/
|
||||
static parse(ipnet: string): IpNet {
|
||||
const [address, prefixStr] = ipnet.split('/', 2)
|
||||
const ip = IpAddress.parse(address)
|
||||
const prefix = Number(prefixStr)
|
||||
return IpNet.fromIpPrefix(ip, prefix)
|
||||
}
|
||||
/**
|
||||
* Checks whether this network contains the given address or subnet.
|
||||
* @param address - An IP address or subnet (string, IpAddress, or IpNet)
|
||||
* @returns True if the address falls within this network's range
|
||||
*/
|
||||
contains(address: string | IpAddress | IpNet): boolean {
|
||||
if (typeof address === 'string') address = IpAddress.parse(address)
|
||||
if (address instanceof IpNet && address.prefix < this.prefix) return false
|
||||
@@ -197,6 +270,7 @@ export class IpNet extends IpAddress {
|
||||
const mask = 255 ^ (255 >> prefix)
|
||||
return (this.octets[idx] & mask) === (address.octets[idx] & mask)
|
||||
}
|
||||
/** Returns the network address (all host bits zeroed) for this subnet. */
|
||||
zero(): IpAddress {
|
||||
let octets: number[] = []
|
||||
let prefix = this.prefix
|
||||
@@ -213,6 +287,7 @@ export class IpNet extends IpAddress {
|
||||
|
||||
return IpAddress.fromOctets(octets)
|
||||
}
|
||||
/** Returns the broadcast address (all host bits set to 1) for this subnet. */
|
||||
broadcast(): IpAddress {
|
||||
let octets: number[] = []
|
||||
let prefix = this.prefix
|
||||
@@ -229,11 +304,13 @@ export class IpNet extends IpAddress {
|
||||
|
||||
return IpAddress.fromOctets(octets)
|
||||
}
|
||||
/** The CIDR notation string for this network (e.g. `"192.168.1.0/24"`). */
|
||||
get ipnet() {
|
||||
return `${this.address}/${this.prefix}`
|
||||
}
|
||||
}
|
||||
|
||||
/** All private IPv4 ranges: loopback (127.0.0.0/8), Class A (10.0.0.0/8), Class B (172.16.0.0/12), Class C (192.168.0.0/16). */
|
||||
export const PRIVATE_IPV4_RANGES = [
|
||||
IpNet.parse('127.0.0.0/8'),
|
||||
IpNet.parse('10.0.0.0/8'),
|
||||
@@ -241,8 +318,12 @@ export const PRIVATE_IPV4_RANGES = [
|
||||
IpNet.parse('192.168.0.0/16'),
|
||||
]
|
||||
|
||||
/** IPv4 loopback network (127.0.0.0/8). */
|
||||
export const IPV4_LOOPBACK = IpNet.parse('127.0.0.0/8')
|
||||
/** IPv6 loopback address (::1/128). */
|
||||
export const IPV6_LOOPBACK = IpNet.parse('::1/128')
|
||||
/** IPv6 link-local network (fe80::/10). */
|
||||
export const IPV6_LINK_LOCAL = IpNet.parse('fe80::/10')
|
||||
|
||||
/** Carrier-Grade NAT (CGNAT) address range (100.64.0.0/10), per RFC 6598. */
|
||||
export const CGNAT = IpNet.parse('100.64.0.0/10')
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/**
|
||||
* Wraps a function so it is only executed once. Subsequent calls return the cached result.
|
||||
*
|
||||
* @param fn - The function to execute at most once
|
||||
* @returns A wrapper that lazily evaluates `fn` on first call and caches the result
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const getConfig = once(() => loadExpensiveConfig())
|
||||
* getConfig() // loads config
|
||||
* getConfig() // returns cached result
|
||||
* ```
|
||||
*/
|
||||
export function once<B>(fn: () => B): () => B {
|
||||
let result: [B] | [] = []
|
||||
return () => {
|
||||
|
||||
@@ -1,57 +1,68 @@
|
||||
import { Pattern } from '../actions/input/inputSpecTypes'
|
||||
import * as regexes from './regexes'
|
||||
|
||||
/** Pattern for validating IPv6 addresses. */
|
||||
export const ipv6: Pattern = {
|
||||
regex: regexes.ipv6.matches(),
|
||||
description: 'Must be a valid IPv6 address',
|
||||
}
|
||||
|
||||
/** Pattern for validating IPv4 addresses. */
|
||||
export const ipv4: Pattern = {
|
||||
regex: regexes.ipv4.matches(),
|
||||
description: 'Must be a valid IPv4 address',
|
||||
}
|
||||
|
||||
/** Pattern for validating hostnames (RFC-compliant). */
|
||||
export const hostname: Pattern = {
|
||||
regex: regexes.hostname.matches(),
|
||||
description: 'Must be a valid hostname',
|
||||
}
|
||||
|
||||
/** Pattern for validating `.local` mDNS hostnames. */
|
||||
export const localHostname: Pattern = {
|
||||
regex: regexes.localHostname.matches(),
|
||||
description: 'Must be a valid ".local" hostname',
|
||||
}
|
||||
|
||||
/** Pattern for validating HTTP/HTTPS URLs. */
|
||||
export const url: Pattern = {
|
||||
regex: regexes.url.matches(),
|
||||
description: 'Must be a valid URL',
|
||||
}
|
||||
|
||||
/** Pattern for validating `.local` URLs (mDNS/LAN). */
|
||||
export const localUrl: Pattern = {
|
||||
regex: regexes.localUrl.matches(),
|
||||
description: 'Must be a valid ".local" URL',
|
||||
}
|
||||
|
||||
/** Pattern for validating ASCII-only strings (printable characters). */
|
||||
export const ascii: Pattern = {
|
||||
regex: regexes.ascii.matches(),
|
||||
description:
|
||||
'May only contain ASCII characters. See https://www.w3schools.com/charsets/ref_html_ascii.asp',
|
||||
}
|
||||
|
||||
/** Pattern for validating fully qualified domain names (FQDNs). */
|
||||
export const domain: Pattern = {
|
||||
regex: regexes.domain.matches(),
|
||||
description: 'Must be a valid Fully Qualified Domain Name',
|
||||
}
|
||||
|
||||
/** Pattern for validating email addresses. */
|
||||
export const email: Pattern = {
|
||||
regex: regexes.email.matches(),
|
||||
description: 'Must be a valid email address',
|
||||
}
|
||||
|
||||
/** Pattern for validating email addresses, optionally with a display name (e.g. `"John Doe <john@example.com>"`). */
|
||||
export const emailWithName: Pattern = {
|
||||
regex: regexes.emailWithName.matches(),
|
||||
description: 'Must be a valid email address, optionally with a name',
|
||||
}
|
||||
|
||||
/** Pattern for validating base64-encoded strings. */
|
||||
export const base64: Pattern = {
|
||||
regex: regexes.base64.matches(),
|
||||
description:
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/**
|
||||
* A wrapper around RegExp that supports composition into larger patterns.
|
||||
* Provides helpers to produce anchored (full-match), grouped (sub-expression),
|
||||
* and unanchored (contains) regex source strings.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const digit = new ComposableRegex(/\d+/)
|
||||
* digit.matches() // "^\\d+$"
|
||||
* digit.contains() // "\\d+"
|
||||
* digit.asExpr() // "(\\d+)"
|
||||
* ```
|
||||
*/
|
||||
export class ComposableRegex {
|
||||
readonly regex: RegExp
|
||||
constructor(regex: RegExp | string) {
|
||||
@@ -7,69 +20,94 @@ export class ComposableRegex {
|
||||
this.regex = new RegExp(regex)
|
||||
}
|
||||
}
|
||||
/** Returns the regex source wrapped in a capturing group, suitable for embedding in a larger expression. */
|
||||
asExpr(): string {
|
||||
return `(${this.regex.source})`
|
||||
}
|
||||
/** Returns the regex source anchored with `^...$` for full-string matching. */
|
||||
matches(): string {
|
||||
return `^${this.regex.source}$`
|
||||
}
|
||||
/** Returns the raw regex source string for substring/containment matching. */
|
||||
contains(): string {
|
||||
return this.regex.source
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes all regex special characters in a string so it can be used as a literal in a RegExp.
|
||||
* @param str - The string to escape
|
||||
* @returns The escaped string safe for regex interpolation
|
||||
*/
|
||||
export const escapeLiteral = (str: string) =>
|
||||
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
/** Composable regex for matching IPv6 addresses (all standard forms including `::` shorthand). */
|
||||
// https://ihateregex.io/expr/ipv6/
|
||||
export const ipv6 = new ComposableRegex(
|
||||
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
|
||||
)
|
||||
|
||||
/** Composable regex for matching IPv4 addresses in dotted-decimal notation. */
|
||||
// https://ihateregex.io/expr/ipv4/
|
||||
export const ipv4 = new ComposableRegex(
|
||||
/(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
|
||||
)
|
||||
|
||||
/** Composable regex for matching RFC-compliant hostnames. */
|
||||
export const hostname = new ComposableRegex(
|
||||
/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])/,
|
||||
)
|
||||
|
||||
/** Composable regex for matching `.local` mDNS hostnames. */
|
||||
export const localHostname = new ComposableRegex(
|
||||
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/,
|
||||
)
|
||||
|
||||
/** Composable regex for matching HTTP/HTTPS URLs. */
|
||||
// https://ihateregex.io/expr/url/
|
||||
export const url = new ComposableRegex(
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
||||
)
|
||||
|
||||
/** Composable regex for matching `.local` URLs (mDNS/LAN). */
|
||||
export const localUrl = new ComposableRegex(
|
||||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
|
||||
)
|
||||
|
||||
/** Composable regex for matching printable ASCII characters (space through tilde). */
|
||||
// https://ihateregex.io/expr/ascii/
|
||||
export const ascii = new ComposableRegex(/[ -~]*/)
|
||||
|
||||
/** Composable regex for matching fully qualified domain names. */
|
||||
export const domain = new ComposableRegex(/[A-Za-z0-9.-]+\.[A-Za-z]{2,}/)
|
||||
|
||||
/** Composable regex for matching email addresses. */
|
||||
// https://www.regular-expressions.info/email.html
|
||||
export const email = new ComposableRegex(`[A-Za-z0-9._%+-]+@${domain.asExpr()}`)
|
||||
|
||||
/** Composable regex for matching email addresses optionally preceded by a display name (e.g. `"Name <email>"`). */
|
||||
export const emailWithName = new ComposableRegex(
|
||||
`${email.asExpr()}|([^<]*<${email.asExpr()}>)`,
|
||||
)
|
||||
|
||||
/** Composable regex for matching base64-encoded strings (no whitespace). */
|
||||
//https://rgxdb.com/r/1NUN74O6
|
||||
export const base64 = new ComposableRegex(
|
||||
/(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))/,
|
||||
)
|
||||
|
||||
/** Composable regex for matching base64-encoded strings that may contain interspersed whitespace. */
|
||||
//https://rgxdb.com/r/1NUN74O6
|
||||
export const base64Whitespace = new ComposableRegex(
|
||||
/(?:([a-zA-Z0-9+\/]\s*){4})*(?:|(?:([a-zA-Z0-9+\/]\s*){3}=)|(?:([a-zA-Z0-9+\/]\s*){2}==)|(?:([a-zA-Z0-9+\/]\s*){1}===))/,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a composable regex for matching PEM-encoded blocks with the given label.
|
||||
* @param label - The PEM label (e.g. `"CERTIFICATE"`, `"RSA PRIVATE KEY"`)
|
||||
* @returns A ComposableRegex matching `-----BEGIN <label>-----...-----END <label>-----`
|
||||
*/
|
||||
export const pem = (label: string) =>
|
||||
new ComposableRegex(
|
||||
`-----BEGIN ${escapeLiteral(label)}-----\r?\n[a-zA-Z0-9+/\n\r=]*?\r?\n-----END ${escapeLiteral(label)}-----`,
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/**
|
||||
* Normalizes a command into an argv-style string array.
|
||||
* If given a string, wraps it as `["sh", "-c", command]`.
|
||||
* If given a tuple, returns it as-is.
|
||||
*
|
||||
* @param command - A shell command string or a pre-split argv tuple
|
||||
* @returns An argv-style string array suitable for process execution
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* splitCommand("echo hello") // ["sh", "-c", "echo hello"]
|
||||
* splitCommand(["node", "index.js"]) // ["node", "index.js"]
|
||||
* ```
|
||||
*/
|
||||
export const splitCommand = (
|
||||
command: string | [string, ...string[]],
|
||||
): string[] => {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Extracts a string result from a stdout/stderr pair.
|
||||
* Returns `stdout` on success; rejects with `stderr` if it is non-empty.
|
||||
*
|
||||
* @param x - An object containing `stdout` and `stderr` strings
|
||||
* @returns A promise resolving to `stdout`, or rejecting with `stderr`
|
||||
*/
|
||||
export async function stringFromStdErrOut(x: {
|
||||
stdout: string
|
||||
stderr: string
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
import * as T from '../types'
|
||||
|
||||
/**
|
||||
* Flattens an intersection type into a single object type for improved readability in IDE tooltips.
|
||||
* Arrays pass through unchanged; objects are remapped to a single flat type.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Merged = FlattenIntersection<{ a: 1 } & { b: 2 }>
|
||||
* // Result: { a: 1; b: 2 }
|
||||
* ```
|
||||
*/
|
||||
// prettier-ignore
|
||||
export type FlattenIntersection<T> =
|
||||
export type FlattenIntersection<T> =
|
||||
T extends ArrayLike<any> ? T :
|
||||
T extends object ? {} & {[P in keyof T]: T[P]} :
|
||||
T;
|
||||
|
||||
/** Shorthand alias for {@link FlattenIntersection}. */
|
||||
export type _<T> = FlattenIntersection<T>
|
||||
|
||||
/**
|
||||
* Type guard that checks whether a value is a {@link T.KnownError}.
|
||||
* Returns true if the value is an object containing an `error` or `error-code` property.
|
||||
*
|
||||
* @param e - The value to check
|
||||
* @returns True if `e` is a KnownError
|
||||
*/
|
||||
export const isKnownError = (e: unknown): e is T.KnownError =>
|
||||
e instanceof Object && ('error' in e || 'error-code' in e)
|
||||
|
||||
declare const affine: unique symbol
|
||||
|
||||
/**
|
||||
* A branded/nominal type wrapper using a unique symbol to make structurally identical types incompatible.
|
||||
* Useful for creating distinct type identities at the type level.
|
||||
*/
|
||||
export type Affine<A> = { [affine]: A }
|
||||
|
||||
type NeverPossible = { [affine]: string }
|
||||
/**
|
||||
* Evaluates to `never` if `A` is `any`, otherwise resolves to `A`.
|
||||
* Useful for preventing `any` from silently propagating through generic constraints.
|
||||
*/
|
||||
export type NoAny<A> = NeverPossible extends A
|
||||
? keyof NeverPossible extends keyof A
|
||||
? never
|
||||
@@ -54,6 +80,14 @@ type Numbers = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
|
||||
|
||||
type CapitalChars = CapitalLetters | Numbers
|
||||
|
||||
/**
|
||||
* Converts a PascalCase or camelCase string type to kebab-case at the type level.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Result = ToKebab<"FooBar"> // "foo-bar"
|
||||
* ```
|
||||
*/
|
||||
export type ToKebab<S extends string> = S extends string
|
||||
? S extends `${infer Head}${CapitalChars}${infer Tail}` // string has a capital char somewhere
|
||||
? Head extends '' // there is a capital char in the first position
|
||||
@@ -101,6 +135,7 @@ export type ToKebab<S extends string> = S extends string
|
||||
: S /* 'abc' */
|
||||
: never
|
||||
|
||||
/** A generic object type with string keys and unknown values. */
|
||||
export type StringObject = Record<string, unknown>
|
||||
|
||||
function test() {
|
||||
|
||||
Reference in New Issue
Block a user