Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/abort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

/**
* Create a composite AbortSignal from multiple signals.
*
* @example
* ```typescript
* const ac1 = new AbortController()
* const ac2 = new AbortController()
* const signal = createCompositeAbortSignal(ac1.signal, ac2.signal)
* ```
*/
export function createCompositeAbortSignal(
...signals: Array<AbortSignal | null | undefined>
Expand Down Expand Up @@ -33,6 +40,12 @@ export function createCompositeAbortSignal(

/**
* Create an AbortSignal that triggers after a timeout.
*
* @example
* ```typescript
* const signal = createTimeoutSignal(5000) // aborts after 5 seconds
* fetch('https://example.com', { signal })
* ```
*/
export function createTimeoutSignal(ms: number): AbortSignal {
if (typeof ms !== 'number' || Number.isNaN(ms)) {
Expand Down
13 changes: 13 additions & 0 deletions src/ansi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ const ANSI_REGEX = /\x1b\[[0-9;]*m/g
* https://socket.dev/npm/package/ansi-regexp/overview/6.2.2
* MIT License
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
*
* @example
* ```typescript
* const regex = ansiRegex()
* '\u001b[31mHello\u001b[0m'.match(regex) // ['\u001b[31m', '\u001b[0m']
* ansiRegex({ onlyFirst: true }) // matches only the first code
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function ansiRegex(options?: { onlyFirst?: boolean }): RegExp {
Expand All @@ -40,6 +47,12 @@ export function ansiRegex(options?: { onlyFirst?: boolean }): RegExp {
/**
* Strip ANSI escape codes from text.
* Uses the inlined ansi-regex for matching.
*
* @example
* ```typescript
* stripAnsi('\u001b[31mError\u001b[0m') // 'Error'
* stripAnsi('\u001b[1mBold\u001b[0m') // 'Bold'
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function stripAnsi(text: string): string {
Expand Down
7 changes: 7 additions & 0 deletions src/cache-with-ttl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ const DEFAULT_PREFIX = 'ttl-cache'

/**
* Create a TTL-based cache instance.
*
* @example
* ```typescript
* const cache = createTtlCache({ ttl: 60_000, prefix: 'my-app' })
* await cache.set('key', { value: 42 })
* const data = await cache.get('key') // { value: 42 }
* ```
*/
export function createTtlCache(options?: TtlCacheOptions): TtlCache {
const opts = {
Expand Down
12 changes: 12 additions & 0 deletions src/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ const colorToRgb: Record<ColorName, ColorRgb> = {
* Type guard to check if a color value is an RGB tuple.
* @param value - Color value to check
* @returns `true` if value is an RGB tuple, `false` if it's a color name
*
* @example
* ```typescript
* isRgbTuple([255, 0, 0]) // true
* isRgbTuple('red') // false
* ```
*/
export function isRgbTuple(value: ColorValue): value is ColorRgb {
return Array.isArray(value)
Expand All @@ -78,6 +84,12 @@ export function isRgbTuple(value: ColorValue): value is ColorRgb {
* Named colors are looked up in the `colorToRgb` map, RGB tuples are returned as-is.
* @param color - Color name or RGB tuple
* @returns RGB tuple with values 0-255
*
* @example
* ```typescript
* toRgb('red') // [255, 0, 0]
* toRgb([0, 128, 0]) // [0, 128, 0]
* ```
*/
export function toRgb(color: ColorValue): ColorRgb {
if (isRgbTuple(color)) {
Expand Down
30 changes: 30 additions & 0 deletions src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,25 @@ export type AnyFunction = (...args: unknown[]) => unknown

/**
* A no-op function that does nothing.
*
* @example
* ```typescript
* const callback = noop
* callback() // does nothing
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function noop(): void {}

/**
* Create a function that only executes once.
*
* @example
* ```typescript
* const init = once(() => Math.random())
* init() // 0.456 (random value)
* init() // 0.456 (same value, not recalculated)
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function once<T extends AnyFunction>(fn: T): T {
Expand All @@ -36,6 +49,15 @@ export function once<T extends AnyFunction>(fn: T): T {

/**
* Wrap an async function to silently catch and ignore errors.
*
* @example
* ```typescript
* const safeFetch = silentWrapAsync(async (url: string) => {
* const res = await fetch(url)
* return res.json()
* })
* await safeFetch('https://example.com') // result or undefined on error
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function silentWrapAsync<TArgs extends unknown[], TResult>(
Expand All @@ -52,6 +74,14 @@ export function silentWrapAsync<TArgs extends unknown[], TResult>(

/**
* Execute a function with tail call optimization via trampoline.
*
* @example
* ```typescript
* const factorial = trampoline((n: number, acc = 1): any =>
* n <= 1 ? acc : () => factorial(n - 1, n * acc)
* )
* factorial(5) // 120
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function trampoline<T extends AnyFunction>(fn: T): T {
Expand Down
68 changes: 63 additions & 5 deletions src/paths/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,62 @@ export function isRelative(pathLike: string | Buffer | URL): boolean {
return !isAbsolute(filepath)
}

/**
* Convert Unix-style POSIX paths (MSYS/Git Bash format) back to native Windows paths.
*
* This is the inverse of {@link toUnixPath}. MSYS-style paths use `/c/` notation
* for drive letters, which PowerShell and cmd.exe cannot resolve. This function
* converts them back to native Windows format.
*
* Conversion rules:
* - On Windows: Converts Unix drive notation to Windows drive letters
* - `/c/path/to/file` becomes `C:/path/to/file`
* - `/d/projects/app` becomes `D:/projects/app`
* - Drive letters are always uppercase in the output
* - On Unix: Returns the path unchanged (passes through normalization)
*
* This is particularly important for:
* - GitHub Actions runners where `command -v` returns MSYS paths
* - Tools like sfw that need to resolve real binary paths on Windows
* - Scripts that receive paths from Git Bash but need to pass them to native Windows tools
*
* @param {string | Buffer | URL} pathLike - The MSYS/Unix-style path to convert
* @returns {string} Native Windows path (e.g., `C:/path/to/file`) or normalized Unix path
*
* @example
* ```typescript
* // MSYS drive letter paths
* fromUnixPath('/c/projects/app/file.txt') // 'C:/projects/app/file.txt'
* fromUnixPath('/d/projects/foo/bar') // 'D:/projects/foo/bar'
*
* // Non-drive Unix paths (unchanged)
* fromUnixPath('/tmp/build/output') // '/tmp/build/output'
* fromUnixPath('/usr/local/bin') // '/usr/local/bin'
*
* // Already Windows paths (unchanged)
* fromUnixPath('C:/Windows/System32') // 'C:/Windows/System32'
*
* // Edge cases
* fromUnixPath('/c') // 'C:/'
* fromUnixPath('') // '.'
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function fromUnixPath(pathLike: string | Buffer | URL): string {
const normalized = normalizePath(pathLike)

// On Windows, convert MSYS drive notation back to native: /c/path → C:/path
if (WIN32) {
return normalized.replace(
/^\/([a-zA-Z])(\/|$)/,
(_, letter, sep) => `${letter.toUpperCase()}:${sep || '/'}`,
)
}

// On Unix, just return the normalized path
return normalized
}

/**
* Normalize a path by converting backslashes to forward slashes and collapsing segments.
*
Expand Down Expand Up @@ -1114,21 +1170,23 @@ export function relativeResolve(from: string, to: string): string {
}

/**
* Convert Windows paths to Unix-style POSIX paths for Git Bash tools.
* Convert Windows paths to MSYS/Unix-style POSIX paths for Git Bash tools.
*
* Git for Windows tools (like tar, git, etc.) expect POSIX-style paths with
* forward slashes and Unix drive letter notation (/c/ instead of C:\).
* Git for Windows and MSYS2 tools (like tar, git, etc.) expect POSIX-style
* paths with forward slashes and Unix drive letter notation (/c/ instead of C:\).
* This function handles the conversion for cross-platform compatibility.
*
* This is the inverse of {@link fromUnixPath}.
*
* Conversion rules:
* - On Windows: Normalizes separators and converts drive letters
* - `C:\path\to\file` becomes `/c/path/to/file`
* - `D:/Users/name` becomes `/d/Users/name`
* - `D:/projects/app` becomes `/d/projects/app`
* - Drive letters are always lowercase in the output
* - On Unix: Returns the path unchanged (passes through normalization)
*
* This is particularly important for:
* - Git Bash tools that interpret `D:\` as a remote hostname
* - MSYS2/Git Bash tools that interpret `D:\` as a remote hostname
* - Cross-platform build scripts using tar, git archive, etc.
* - CI/CD environments where Git for Windows is used
*
Expand Down
7 changes: 7 additions & 0 deletions src/regexps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@

/**
* Escape special characters in a string for use in a regular expression.
*
* @example
* ```typescript
* escapeRegExp('foo.bar') // 'foo\\.bar'
* escapeRegExp('a+b*c?') // 'a\\+b\\*c\\?'
* new RegExp(escapeRegExp('[test]')) // /\[test\]/
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function escapeRegExp(str: string): string {
Expand Down
33 changes: 33 additions & 0 deletions src/sorts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ function getFastSort() {

/**
* Compare semantic versions.
*
* @example
* ```typescript
* compareSemver('1.0.0', '2.0.0') // -1
* compareSemver('2.0.0', '1.0.0') // 1
* compareSemver('1.0.0', '1.0.0') // 0
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function compareSemver(a: string, b: string): number {
Expand All @@ -47,6 +54,13 @@ export function compareSemver(a: string, b: string): number {

/**
* Simple string comparison.
*
* @example
* ```typescript
* compareStr('a', 'b') // -1
* compareStr('b', 'a') // 1
* compareStr('a', 'a') // 0
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function compareStr(a: string, b: string): number {
Expand All @@ -56,6 +70,13 @@ export function compareStr(a: string, b: string): number {
let _localeCompare: ((x: string, y: string) => number) | undefined
/**
* Compare two strings using locale-aware comparison.
*
* @example
* ```typescript
* localeCompare('a', 'b') // -1
* localeCompare('b', 'a') // 1
* localeCompare('a', 'a') // 0
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function localeCompare(x: string, y: string): number {
Expand All @@ -69,6 +90,12 @@ export function localeCompare(x: string, y: string): number {
let _naturalCompare: ((x: string, y: string) => number) | undefined
/**
* Compare two strings using natural sorting (numeric-aware, case-insensitive).
*
* @example
* ```typescript
* naturalCompare('file2', 'file10') // negative (file2 before file10)
* naturalCompare('img10', 'img2') // positive (img10 after img2)
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function naturalCompare(x: string, y: string): number {
Expand Down Expand Up @@ -100,6 +127,12 @@ type FastSortFunction = ReturnType<
let _naturalSorter: FastSortFunction | undefined
/**
* Sort an array using natural comparison.
*
* @example
* ```typescript
* naturalSorter(['file10', 'file2', 'file1']).asc()
* // ['file1', 'file2', 'file10']
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function naturalSorter<T>(
Expand Down
Loading