From cd144e11e8347b897118a11a2f28367604af903f Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 13 Apr 2026 16:33:22 -0400 Subject: [PATCH 1/3] feat(paths): add fromUnixPath to convert MSYS paths to native Windows format --- src/paths/normalize.ts | 68 +++++++- test/unit/paths/normalize.test.mts | 264 +++++++++++++++++++---------- 2 files changed, 233 insertions(+), 99 deletions(-) diff --git a/src/paths/normalize.ts b/src/paths/normalize.ts index 80b62caec..30bbb3419 100644 --- a/src/paths/normalize.ts +++ b/src/paths/normalize.ts @@ -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. * @@ -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 * diff --git a/test/unit/paths/normalize.test.mts b/test/unit/paths/normalize.test.mts index 185949626..d2c3f0b6c 100644 --- a/test/unit/paths/normalize.test.mts +++ b/test/unit/paths/normalize.test.mts @@ -12,12 +12,14 @@ * - pathLikeToString() converts Buffer/URL to string * - relativeResolve() resolves relative paths * - toUnixPath() converts Windows paths to Unix-style POSIX paths for Git Bash tools + * - fromUnixPath() converts MSYS/Unix-style paths back to native Windows paths * Used throughout Socket tools for cross-platform path handling. */ import process from 'node:process' import { describe, expect, it } from 'vitest' import { + fromUnixPath, isAbsolute, isNodeModules, isPath, @@ -83,6 +85,103 @@ describe('paths/normalize', () => { }) }) + describe('fromUnixPath', () => { + const isWindows = process.platform === 'win32' + + it.skipIf(!isWindows)( + 'should convert MSYS drive letter paths to Windows format', + () => { + expect(fromUnixPath('/c/projects/app/file.txt')).toBe( + 'C:/projects/app/file.txt', + ) + expect(fromUnixPath('/d/projects/foo/bar')).toBe('D:/projects/foo/bar') + }, + ) + + it.skipIf(!isWindows)( + 'should convert lowercase drive letters to uppercase', + () => { + expect(fromUnixPath('/c/path')).toBe('C:/path') + expect(fromUnixPath('/d/path')).toBe('D:/path') + expect(fromUnixPath('/z/path')).toBe('Z:/path') + }, + ) + + it.skipIf(!isWindows)('should handle all drive letters a-z', () => { + expect(fromUnixPath('/a/path')).toBe('A:/path') + expect(fromUnixPath('/e/path')).toBe('E:/path') + expect(fromUnixPath('/z/path')).toBe('Z:/path') + }) + + it.skipIf(!isWindows)('should handle bare drive letter path', () => { + expect(fromUnixPath('/c')).toBe('C:/') + }) + + it.skipIf(!isWindows)('should not convert non-drive Unix paths', () => { + expect(fromUnixPath('/tmp/build/output')).toBe('/tmp/build/output') + expect(fromUnixPath('/usr/local/bin')).toBe('/usr/local/bin') + }) + + it.skipIf(isWindows)('should leave Unix paths unchanged on Unix', () => { + expect(fromUnixPath('/tmp/build/output')).toBe('/tmp/build/output') + expect(fromUnixPath('/usr/local/bin')).toBe('/usr/local/bin') + expect(fromUnixPath('/c/projects/app')).toBe('/c/projects/app') + }) + + it.skipIf(isWindows)('should normalize paths on Unix', () => { + expect(fromUnixPath('/usr/local/../bin')).toBe('/usr/bin') + expect(fromUnixPath('/usr//local///bin')).toBe('/usr/local/bin') + }) + + it('should handle relative paths', () => { + const result1 = fromUnixPath('./src/index.ts') + const result2 = fromUnixPath('../lib/utils') + expect(result1).toContain('src') + expect(result2).toContain('lib') + }) + + it('should handle empty string', () => { + expect(fromUnixPath('')).toBe('.') + }) + + it.skipIf(!isWindows)('should handle paths with spaces', () => { + expect(fromUnixPath('/c/Program Files/App')).toBe('C:/Program Files/App') + }) + + it.skipIf(!isWindows)('should handle paths with special characters', () => { + expect(fromUnixPath('/c/projects/file (1).txt')).toBe( + 'C:/projects/file (1).txt', + ) + expect(fromUnixPath('/d/projects/@scope/package')).toBe( + 'D:/projects/@scope/package', + ) + }) + + it('should handle Buffer input', () => { + if (isWindows) { + const buffer = Buffer.from('/c/projects/app') + expect(fromUnixPath(buffer)).toBe('C:/projects/app') + } else { + const buffer = Buffer.from('/usr/local') + expect(fromUnixPath(buffer)).toBe('/usr/local') + } + }) + + it.skipIf(!isWindows)( + 'should be the inverse of toUnixPath on Windows', + () => { + const original = 'C:/projects/app/file.txt' + const unix = toUnixPath(original) + const backToWindows = fromUnixPath(unix) + expect(backToWindows).toBe(original) + }, + ) + + it.skipIf(isWindows)('should handle root path', () => { + expect(fromUnixPath('/')).toBe('/') + }) + }) + describe('isAbsolute', () => { it('should detect Unix absolute paths', () => { expect(isAbsolute('/usr/local')).toBe(true) @@ -364,88 +463,79 @@ describe('paths/normalize', () => { describe('toUnixPath', () => { const isWindows = process.platform === 'win32' - it('should convert Windows drive letter paths with backslashes', () => { - if (isWindows) { - expect(toUnixPath('C:\\Users\\name\\file.txt')).toBe( - '/c/Users/name/file.txt', + it.skipIf(!isWindows)( + 'should convert Windows drive letter paths with backslashes', + () => { + expect(toUnixPath('C:\\projects\\app\\file.txt')).toBe( + '/c/projects/app/file.txt', ) expect(toUnixPath('D:\\projects\\foo\\bar')).toBe('/d/projects/foo/bar') - } - }) + }, + ) - it('should convert Windows drive letter paths with forward slashes', () => { - if (isWindows) { + it.skipIf(!isWindows)( + 'should convert Windows drive letter paths with forward slashes', + () => { expect(toUnixPath('C:/Windows/System32')).toBe('/c/Windows/System32') expect(toUnixPath('D:/data/logs')).toBe('/d/data/logs') - } - }) + }, + ) - it('should convert uppercase drive letters to lowercase', () => { - if (isWindows) { + it.skipIf(!isWindows)( + 'should convert uppercase drive letters to lowercase', + () => { expect(toUnixPath('C:\\path')).toBe('/c/path') expect(toUnixPath('D:\\path')).toBe('/d/path') expect(toUnixPath('Z:\\path')).toBe('/z/path') - } - }) + }, + ) - it('should handle lowercase drive letters', () => { - if (isWindows) { - expect(toUnixPath('c:\\path')).toBe('/c/path') - expect(toUnixPath('d:\\path')).toBe('/d/path') - } + it.skipIf(!isWindows)('should handle lowercase drive letters', () => { + expect(toUnixPath('c:\\path')).toBe('/c/path') + expect(toUnixPath('d:\\path')).toBe('/d/path') }) - it('should handle mixed case drive letters', () => { - if (isWindows) { - expect(toUnixPath('c:\\Windows\\System32')).toBe('/c/Windows/System32') - expect(toUnixPath('D:\\Users\\John')).toBe('/d/Users/John') - } + it.skipIf(!isWindows)('should handle mixed case drive letters', () => { + expect(toUnixPath('c:\\Windows\\System32')).toBe('/c/Windows/System32') + expect(toUnixPath('D:\\projects\\app')).toBe('/d/projects/app') }) - it('should handle UNC paths', () => { - if (isWindows) { - expect(toUnixPath('\\\\server\\share\\file')).toBe( - '//server/share/file', - ) - expect(toUnixPath('\\\\server\\share\\path\\to\\file')).toBe( - '//server/share/path/to/file', - ) - } + it.skipIf(!isWindows)('should handle UNC paths', () => { + expect(toUnixPath('\\\\server\\share\\file')).toBe('//server/share/file') + expect(toUnixPath('\\\\server\\share\\path\\to\\file')).toBe( + '//server/share/path/to/file', + ) }) - it('should handle Unix absolute paths on Unix', () => { - if (!isWindows) { - expect(toUnixPath('/home/user/file')).toBe('/home/user/file') - expect(toUnixPath('/usr/local/bin')).toBe('/usr/local/bin') - expect(toUnixPath('/var/log/app.log')).toBe('/var/log/app.log') - } + it.skipIf(isWindows)('should handle Unix absolute paths on Unix', () => { + expect(toUnixPath('/tmp/build/output')).toBe('/tmp/build/output') + expect(toUnixPath('/usr/local/bin')).toBe('/usr/local/bin') + expect(toUnixPath('/var/log/app.log')).toBe('/var/log/app.log') }) - it('should normalize paths on Unix (collapse .., remove ./, etc)', () => { - if (!isWindows) { - // Verify that normalization still happens on Unix + it.skipIf(isWindows)( + 'should normalize paths on Unix (collapse .., remove ./, etc)', + () => { expect(toUnixPath('/usr/local/../bin')).toBe('/usr/bin') expect(toUnixPath('/usr//local///bin')).toBe('/usr/local/bin') expect(toUnixPath('./src/index.ts')).toBe('src/index.ts') expect(toUnixPath('/usr/./local/bin')).toBe('/usr/local/bin') - } - }) + }, + ) it('should handle relative paths', () => { - // Relative paths get normalized but don't get drive letter conversion const result1 = toUnixPath('./src/index.ts') const result2 = toUnixPath('../lib/utils') expect(result1).toContain('src') expect(result2).toContain('lib') - // On Unix, should be unchanged. On Windows, backslashes become forward slashes expect(result1.includes('\\\\')).toBe(false) expect(result2.includes('\\\\')).toBe(false) }) it('should handle Buffer input', () => { if (isWindows) { - const buffer = Buffer.from('C:\\Users\\name') - expect(toUnixPath(buffer)).toBe('/c/Users/name') + const buffer = Buffer.from('C:\\projects\\app') + expect(toUnixPath(buffer)).toBe('/c/projects/app') } else { const buffer = Buffer.from('/usr/local') expect(toUnixPath(buffer)).toBe('/usr/local') @@ -466,76 +556,62 @@ describe('paths/normalize', () => { }) it('should handle empty string', () => { - // Empty string normalizes to '.' on all platforms (consistent with Node.js path.normalize) expect(toUnixPath('')).toBe('.') }) - it('should handle root paths', () => { - if (!isWindows) { - expect(toUnixPath('/')).toBe('/') - } + it.skipIf(isWindows)('should handle root paths', () => { + expect(toUnixPath('/')).toBe('/') }) - it('should handle paths with spaces', () => { - if (isWindows) { - expect(toUnixPath('C:\\Program Files\\App')).toBe( - '/c/Program Files/App', - ) - expect(toUnixPath('D:\\My Documents\\file.txt')).toBe( - '/d/My Documents/file.txt', - ) - } + it.skipIf(!isWindows)('should handle paths with spaces', () => { + expect(toUnixPath('C:\\Program Files\\App')).toBe('/c/Program Files/App') + expect(toUnixPath('D:\\My Documents\\file.txt')).toBe( + '/d/My Documents/file.txt', + ) }) - it('should handle paths with special characters', () => { - if (isWindows) { - expect(toUnixPath('C:\\Users\\name\\file (1).txt')).toBe( - '/c/Users/name/file (1).txt', - ) - expect(toUnixPath('D:\\projects\\@scope\\package')).toBe( - '/d/projects/@scope/package', - ) - } + it.skipIf(!isWindows)('should handle paths with special characters', () => { + expect(toUnixPath('C:\\projects\\file (1).txt')).toBe( + '/c/projects/file (1).txt', + ) + expect(toUnixPath('D:\\projects\\@scope\\package')).toBe( + '/d/projects/@scope/package', + ) }) - it('should handle mixed separators in path', () => { - if (isWindows) { - expect(toUnixPath('C:\\Users/name\\file.txt')).toBe( - '/c/Users/name/file.txt', - ) - } + it.skipIf(!isWindows)('should handle mixed separators in path', () => { + expect(toUnixPath('C:\\projects/app\\file.txt')).toBe( + '/c/projects/app/file.txt', + ) }) - it('should handle all drive letters A-Z', () => { - if (isWindows) { - expect(toUnixPath('A:\\path')).toBe('/a/path') - expect(toUnixPath('E:\\path')).toBe('/e/path') - expect(toUnixPath('Z:\\path')).toBe('/z/path') - } + it.skipIf(!isWindows)('should handle all drive letters A-Z', () => { + expect(toUnixPath('A:\\path')).toBe('/a/path') + expect(toUnixPath('E:\\path')).toBe('/e/path') + expect(toUnixPath('Z:\\path')).toBe('/z/path') }) - it('should preserve path after drive letter conversion', () => { - if (isWindows) { + it.skipIf(!isWindows)( + 'should preserve path after drive letter conversion', + () => { expect(toUnixPath('C:\\a\\b\\c\\d\\e\\f')).toBe('/c/a/b/c/d/e/f') expect(toUnixPath('D:\\projects\\socket-btm\\build\\dev')).toBe( '/d/projects/socket-btm/build/dev', ) - } - }) + }, + ) - it('should handle Git Bash tar paths correctly', () => { - // This is the primary use case: Git for Windows tar.EXE needs POSIX paths - if (isWindows) { - // Example from Windows CI: D:\a\socket-btm\build\dev + it.skipIf(!isWindows)( + 'should handle MSYS/Git Bash tar paths correctly', + () => { expect(toUnixPath('D:\\a\\socket-btm\\build\\dev')).toBe( '/d/a/socket-btm/build/dev', ) - // tar expects /d/path not D:\path const result = toUnixPath('C:\\Windows\\Temp\\archive.tar.gz') expect(result.startsWith('/c/')).toBe(true) expect(result.includes('\\')).toBe(false) - } - }) + }, + ) }) describe('Edge cases', () => { From 0792a068ecb1311b80382dd5623bb3fd6bda97bd Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 13 Apr 2026 16:56:54 -0400 Subject: [PATCH 2/3] docs(utils): add @example blocks to utility module exports --- src/ansi.ts | 13 +++++++++++++ src/functions.ts | 30 ++++++++++++++++++++++++++++++ src/regexps.ts | 7 +++++++ src/sorts.ts | 33 +++++++++++++++++++++++++++++++++ src/words.ts | 20 ++++++++++++++++++++ 5 files changed, 103 insertions(+) diff --git a/src/ansi.ts b/src/ansi.ts index 9260b9499..4ccfcdda7 100644 --- a/src/ansi.ts +++ b/src/ansi.ts @@ -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 (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 { @@ -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 { diff --git a/src/functions.ts b/src/functions.ts index 60687b536..6395e71c2 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -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(fn: T): T { @@ -36,6 +49,15 @@ export function once(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( @@ -52,6 +74,14 @@ export function silentWrapAsync( /** * 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(fn: T): T { diff --git a/src/regexps.ts b/src/regexps.ts index dec0693d6..a20dd98c7 100644 --- a/src/regexps.ts +++ b/src/regexps.ts @@ -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 { diff --git a/src/sorts.ts b/src/sorts.ts index 7c1909624..b081bd894 100644 --- a/src/sorts.ts +++ b/src/sorts.ts @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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( diff --git a/src/words.ts b/src/words.ts index 520688a10..f487c3992 100644 --- a/src/words.ts +++ b/src/words.ts @@ -5,6 +5,13 @@ /** * Capitalize the first letter of a word. + * + * @example + * ```typescript + * capitalize('hello') // 'Hello' + * capitalize('WORLD') // 'World' + * capitalize('') // '' + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function capitalize(word: string): string { @@ -20,6 +27,12 @@ export function capitalize(word: string): string { /** * Determine the appropriate article (a/an) for a word. + * + * @example + * ```typescript + * determineArticle('apple') // 'an' + * determineArticle('banana') // 'a' + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function determineArticle(word: string): string { @@ -32,6 +45,13 @@ export interface PluralizeOptions { /** * Pluralize a word based on count. + * + * @example + * ```typescript + * pluralize('file') // 'file' + * pluralize('file', { count: 3 }) // 'files' + * pluralize('file', { count: 0 }) // 'files' + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function pluralize( From a25bcf91dbb3add4a0cbae75aae4bd07de616a92 Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 13 Apr 2026 16:58:08 -0400 Subject: [PATCH 3/3] docs(core): add @example blocks to core module exports --- src/abort.ts | 13 +++++ src/cache-with-ttl.ts | 7 +++ src/colors.ts | 12 +++++ src/url.ts | 59 +++++++++++++++++++++ src/versions.ts | 119 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+) diff --git a/src/abort.ts b/src/abort.ts index 1fc01aec9..4493cd2d8 100644 --- a/src/abort.ts +++ b/src/abort.ts @@ -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 @@ -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)) { diff --git a/src/cache-with-ttl.ts b/src/cache-with-ttl.ts index 1ff422135..da53d23e2 100644 --- a/src/cache-with-ttl.ts +++ b/src/cache-with-ttl.ts @@ -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 = { diff --git a/src/colors.ts b/src/colors.ts index a70fd37d7..154d88ad3 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -68,6 +68,12 @@ const colorToRgb: Record = { * 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) @@ -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)) { diff --git a/src/url.ts b/src/url.ts index aed76ffac..6a038d817 100644 --- a/src/url.ts +++ b/src/url.ts @@ -8,6 +8,13 @@ const UrlCtor = URL /** * Check if a value is a valid URL. + * + * @example + * ```typescript + * isUrl('https://example.com') // true + * isUrl('not a url') // false + * isUrl(null) // false + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function isUrl(value: string | URL | null | undefined): boolean { @@ -20,6 +27,12 @@ export function isUrl(value: string | URL | null | undefined): boolean { /** * Parse a value as a URL. + * + * @example + * ```typescript + * parseUrl('https://example.com') // URL { href: 'https://example.com/' } + * parseUrl('invalid') // undefined + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function parseUrl(value: string | URL): URL | undefined { @@ -31,6 +44,12 @@ export function parseUrl(value: string | URL): URL | undefined { /** * Convert a URL search parameter to an array. + * + * @example + * ```typescript + * urlSearchParamAsArray('a, b, c') // ['a', 'b', 'c'] + * urlSearchParamAsArray(null) // [] + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function urlSearchParamAsArray( @@ -51,6 +70,13 @@ export interface UrlSearchParamAsBooleanOptions { /** * Convert a URL search parameter to a boolean. + * + * @example + * ```typescript + * urlSearchParamAsBoolean('true') // true + * urlSearchParamAsBoolean('0') // false + * urlSearchParamAsBoolean(null) // false + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function urlSearchParamAsBoolean( @@ -73,6 +99,12 @@ export function urlSearchParamAsBoolean( /** * Helper to get array from URLSearchParams. + * + * @example + * ```typescript + * const params = new URLSearchParams('tags=a,b,c') + * urlSearchParamsGetArray(params, 'tags') // ['a', 'b', 'c'] + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function urlSearchParamsGetArray( @@ -97,6 +129,13 @@ export interface UrlSearchParamsGetBooleanOptions { /** * Helper to get boolean from URLSearchParams. + * + * @example + * ```typescript + * const params = new URLSearchParams('debug=true') + * urlSearchParamsGetBoolean(params, 'debug') // true + * urlSearchParamsGetBoolean(params, 'other') // false + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function urlSearchParamsGetBoolean( @@ -123,6 +162,12 @@ export interface CreateRelativeUrlOptions { /** * Create a relative URL for testing. + * + * @example + * ```typescript + * createRelativeUrl('/api/test') // 'api/test' + * createRelativeUrl('/api/test', { base: 'https://example.com' }) // 'https://example.com/api/test' + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function createRelativeUrl( @@ -153,6 +198,13 @@ export interface UrlSearchParamAsStringOptions { /** * Get string value from URLSearchParams with a default. + * + * @example + * ```typescript + * const params = new URLSearchParams('name=socket') + * urlSearchParamAsString(params, 'name') // 'socket' + * urlSearchParamAsString(params, 'other') // '' + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function urlSearchParamAsString( @@ -177,6 +229,13 @@ export interface UrlSearchParamAsNumberOptions { /** * Get number value from URLSearchParams with a default. + * + * @example + * ```typescript + * const params = new URLSearchParams('limit=10') + * urlSearchParamAsNumber(params, 'limit') // 10 + * urlSearchParamAsNumber(params, 'other') // 0 + * ``` */ /*@__NO_SIDE_EFFECTS__*/ export function urlSearchParamAsNumber( diff --git a/src/versions.ts b/src/versions.ts index e90bcedb8..9bc7e9754 100644 --- a/src/versions.ts +++ b/src/versions.ts @@ -12,6 +12,13 @@ function getSemver() { /** * Coerce a version string to valid semver format. + * + * @example + * ```typescript + * coerceVersion('1.2') // '1.2.0' + * coerceVersion('v3') // '3.0.0' + * coerceVersion('abc') // undefined + * ``` */ export function coerceVersion(version: string): string | undefined { /* c8 ignore next - External semver call */ @@ -23,6 +30,13 @@ export function coerceVersion(version: string): string | undefined { /** * Compare two semantic version strings. * @returns -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2, or undefined if invalid. + * + * @example + * ```typescript + * compareVersions('1.0.0', '2.0.0') // -1 + * compareVersions('1.0.0', '1.0.0') // 0 + * compareVersions('2.0.0', '1.0.0') // 1 + * ``` */ export function compareVersions( v1: string, @@ -39,6 +53,12 @@ export function compareVersions( /** * Get all versions from an array that satisfy a semver range. + * + * @example + * ```typescript + * filterVersions(['1.0.0', '1.5.0', '2.0.0'], '>=1.0.0 <2.0.0') + * // ['1.0.0', '1.5.0'] + * ``` */ export function filterVersions(versions: string[], range: string): string[] { /* c8 ignore next - External semver call */ @@ -48,6 +68,11 @@ export function filterVersions(versions: string[], range: string): string[] { /** * Get the major version number from a version string. + * + * @example + * ```typescript + * getMajorVersion('3.2.1') // 3 + * ``` */ export function getMajorVersion(version: string): number | undefined { /* c8 ignore next - External semver call */ @@ -58,6 +83,11 @@ export function getMajorVersion(version: string): number | undefined { /** * Get the minor version number from a version string. + * + * @example + * ```typescript + * getMinorVersion('3.2.1') // 2 + * ``` */ export function getMinorVersion(version: string): number | undefined { /* c8 ignore next - External semver call */ @@ -68,6 +98,11 @@ export function getMinorVersion(version: string): number | undefined { /** * Get the patch version number from a version string. + * + * @example + * ```typescript + * getPatchVersion('3.2.1') // 1 + * ``` */ export function getPatchVersion(version: string): number | undefined { /* c8 ignore next - External semver call */ @@ -78,6 +113,13 @@ export function getPatchVersion(version: string): number | undefined { /** * Increment a version by the specified release type. + * + * @example + * ```typescript + * incrementVersion('1.2.3', 'patch') // '1.2.4' + * incrementVersion('1.2.3', 'minor') // '1.3.0' + * incrementVersion('1.2.3', 'major') // '2.0.0' + * ``` */ export function incrementVersion( version: string, @@ -98,6 +140,12 @@ export function incrementVersion( /** * Check if version1 equals version2. + * + * @example + * ```typescript + * isEqual('1.0.0', '1.0.0') // true + * isEqual('1.0.0', '2.0.0') // false + * ``` */ export function isEqual(version1: string, version2: string): boolean { /* c8 ignore next - External semver call */ @@ -107,6 +155,12 @@ export function isEqual(version1: string, version2: string): boolean { /** * Check if version1 is greater than version2. + * + * @example + * ```typescript + * isGreaterThan('2.0.0', '1.0.0') // true + * isGreaterThan('1.0.0', '2.0.0') // false + * ``` */ export function isGreaterThan(version1: string, version2: string): boolean { /* c8 ignore next - External semver call */ @@ -116,6 +170,12 @@ export function isGreaterThan(version1: string, version2: string): boolean { /** * Check if version1 is greater than or equal to version2. + * + * @example + * ```typescript + * isGreaterThanOrEqual('2.0.0', '1.0.0') // true + * isGreaterThanOrEqual('1.0.0', '1.0.0') // true + * ``` */ export function isGreaterThanOrEqual( version1: string, @@ -128,6 +188,12 @@ export function isGreaterThanOrEqual( /** * Check if version1 is less than version2. + * + * @example + * ```typescript + * isLessThan('1.0.0', '2.0.0') // true + * isLessThan('2.0.0', '1.0.0') // false + * ``` */ export function isLessThan(version1: string, version2: string): boolean { /* c8 ignore next - External semver call */ @@ -137,6 +203,12 @@ export function isLessThan(version1: string, version2: string): boolean { /** * Check if version1 is less than or equal to version2. + * + * @example + * ```typescript + * isLessThanOrEqual('1.0.0', '2.0.0') // true + * isLessThanOrEqual('1.0.0', '1.0.0') // true + * ``` */ export function isLessThanOrEqual(version1: string, version2: string): boolean { /* c8 ignore next - External semver call */ @@ -146,6 +218,12 @@ export function isLessThanOrEqual(version1: string, version2: string): boolean { /** * Validate if a string is a valid semantic version. + * + * @example + * ```typescript + * isValidVersion('1.2.3') // true + * isValidVersion('abc') // false + * ``` */ export function isValidVersion(version: string): boolean { /* c8 ignore next - External semver call */ @@ -155,6 +233,11 @@ export function isValidVersion(version: string): boolean { /** * Get the highest version from an array of versions. + * + * @example + * ```typescript + * maxVersion(['1.0.0', '2.0.0', '1.5.0']) // '2.0.0' + * ``` */ export function maxVersion(versions: string[]): string | undefined { /* c8 ignore next - External semver call */ @@ -164,6 +247,11 @@ export function maxVersion(versions: string[]): string | undefined { /** * Get the lowest version from an array of versions. + * + * @example + * ```typescript + * minVersion(['1.0.0', '2.0.0', '1.5.0']) // '1.0.0' + * ``` */ export function minVersion(versions: string[]): string | undefined { /* c8 ignore next - External semver call */ @@ -173,6 +261,12 @@ export function minVersion(versions: string[]): string | undefined { /** * Parse a version string and return major, minor, patch components. + * + * @example + * ```typescript + * parseVersion('1.2.3') + * // { major: 1, minor: 2, patch: 3, prerelease: [], build: [] } + * ``` */ export function parseVersion(version: string): | { @@ -200,6 +294,12 @@ export function parseVersion(version: string): /** * Check if a version satisfies a semver range. + * + * @example + * ```typescript + * satisfiesVersion('1.5.0', '>=1.0.0 <2.0.0') // true + * satisfiesVersion('3.0.0', '>=1.0.0 <2.0.0') // false + * ``` */ export function satisfiesVersion(version: string, range: string): boolean { /* c8 ignore next - External semver call */ @@ -209,6 +309,12 @@ export function satisfiesVersion(version: string, range: string): boolean { /** * Sort versions in ascending order. + * + * @example + * ```typescript + * sortVersions(['2.0.0', '1.0.0', '1.5.0']) + * // ['1.0.0', '1.5.0', '2.0.0'] + * ``` */ export function sortVersions(versions: string[]): string[] { /* c8 ignore next - External semver call */ @@ -218,6 +324,12 @@ export function sortVersions(versions: string[]): string[] { /** * Sort versions in descending order. + * + * @example + * ```typescript + * sortVersionsDesc(['1.0.0', '2.0.0', '1.5.0']) + * // ['2.0.0', '1.5.0', '1.0.0'] + * ``` */ export function sortVersionsDesc(versions: string[]): string[] { /* c8 ignore next - External semver call */ @@ -227,6 +339,13 @@ export function sortVersionsDesc(versions: string[]): string[] { /** * Get the difference between two versions. + * + * @example + * ```typescript + * versionDiff('1.0.0', '2.0.0') // 'major' + * versionDiff('1.0.0', '1.1.0') // 'minor' + * versionDiff('1.0.0', '1.0.1') // 'patch' + * ``` */ export function versionDiff( version1: string,