From 6b73b49f9e52d923c3dbf57422e5edbde2e4a0d7 Mon Sep 17 00:00:00 2001 From: nayy Date: Mon, 29 Sep 2025 15:44:37 +0100 Subject: [PATCH] Feat: Implement common helpers library --- package.json | 1 + packages/support/src/GlobalBootstrap.ts | 304 ++++++++++++++++++++++++ packages/support/src/Helpers/Arr.ts | 84 ++++++- packages/support/src/Helpers/Crypto.ts | 196 +++++++++++++++ packages/support/src/Helpers/Str.ts | 234 ++++++++---------- packages/support/src/Helpers/Time.ts | 243 +++++++++++++++++++ packages/support/src/index.ts | 3 + packages/support/tests/arr.test.ts | 54 +++++ packages/support/tests/crypto.test.ts | 42 ++++ packages/support/tests/str.test.ts | 50 +++- packages/support/tests/time.test.ts | 49 ++++ pnpm-lock.yaml | 34 +-- 12 files changed, 1127 insertions(+), 167 deletions(-) create mode 100644 packages/support/src/GlobalBootstrap.ts create mode 100644 packages/support/src/Helpers/Crypto.ts create mode 100644 packages/support/src/Helpers/Time.ts create mode 100644 packages/support/tests/arr.test.ts create mode 100644 packages/support/tests/crypto.test.ts create mode 100644 packages/support/tests/time.test.ts diff --git a/package.json b/package.json index fa0abbe0..3ba39d9e 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "resolve-from": "^5.0.0", "rimraf": "^6.0.1", "ts-jest": "^29.4.4", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "tsdown": "^0.15.4", "typescript": "^5.9.2", diff --git a/packages/support/src/GlobalBootstrap.ts b/packages/support/src/GlobalBootstrap.ts new file mode 100644 index 00000000..4b47e4c9 --- /dev/null +++ b/packages/support/src/GlobalBootstrap.ts @@ -0,0 +1,304 @@ +import * as Arr from './Helpers/Arr' +import * as Crypto from './Helpers/Crypto' +import * as Number from './Helpers/Number' +import * as Obj from './Helpers/Obj' +import * as Str from './Helpers/Str' +import * as Time from './Helpers/Time' +import * as DumpDie from './Helpers/DumpDie' + +/** + * Global helpers interface that mirrors Laravel's helpers + * and provides convenient access to all utility functions + */ +export interface GlobalHelpers { + // Array helpers + Arr: typeof Arr + chunk: typeof Arr.chunk + collapse: typeof Arr.collapse + alternate: typeof Arr.alternate + combine: typeof Arr.combine + find: typeof Arr.find + forget: typeof Arr.forget + first: typeof Arr.first + last: typeof Arr.last + isEmpty: typeof Arr.isEmpty + isNotEmpty: typeof Arr.isNotEmpty + pop: typeof Arr.pop + prepend: typeof Arr.prepend + take: typeof Arr.take + reverse: typeof Arr.reverse + shift: typeof Arr.shift + range: typeof Arr.range + flatten: typeof Arr.flatten + + // String helpers + Str: typeof Str + after: typeof Str.after + afterLast: typeof Str.afterLast + before: typeof Str.before + beforeLast: typeof Str.beforeLast + capitalize: typeof Str.capitalize + pluralize: typeof Str.pluralize + singularize: typeof Str.singularize + slugify: typeof Str.slugify + subString: typeof Str.subString + substitute: typeof Str.substitute + truncate: typeof Str.truncate + substr: typeof Str.substr + sub: typeof Str.sub + esc: typeof Str.esc + padString: typeof Str.padString + split: typeof Str.split + chop: typeof Str.chop + isNumber: typeof Str.isNumber + isInteger: typeof Str.isInteger + rot: typeof Str.rot + replacePunctuation: typeof Str.replacePunctuation + translate: typeof Str.translate + ss: typeof Str.ss + firstLines: typeof Str.firstLines + lastLines: typeof Str.lastLines + + // Object helpers + Obj: typeof Obj + dot: typeof Obj.dot + extractProperties: typeof Obj.extractProperties + getValue: typeof Obj.getValue + modObj: typeof Obj.modObj + safeDot: typeof Obj.safeDot + setNested: typeof Obj.setNested + slugifyKeys: typeof Obj.slugifyKeys + + // Crypto helpers + Crypto: typeof Crypto + uuid: typeof Crypto.uuid + random: typeof Crypto.random + randomSecure: typeof Crypto.randomSecure + hash: typeof Crypto.hash + hmac: typeof Crypto.hmac + base64Encode: typeof Crypto.base64Encode + base64Decode: typeof Crypto.base64Decode + xor: typeof Crypto.xor + randomColor: typeof Crypto.randomColor + randomPassword: typeof Crypto.randomPassword + secureToken: typeof Crypto.secureToken + checksum: typeof Crypto.checksum + verifyChecksum: typeof Crypto.verifyChecksum + caesarCipher: typeof Crypto.caesarCipher + + // Time helpers + Time: typeof Time + now: typeof Time.now + unix: typeof Time.unix + format: typeof Time.format + fromTimestamp: typeof Time.fromTimestamp + diff: typeof Time.diff + subtract: typeof Time.subtract + add: typeof Time.add + start: typeof Time.start + end: typeof Time.end + fromNow: typeof Time.fromNow + randomTime: typeof Time.randomTime + isBetween: typeof Time.isBetween + dayOfYear: typeof Time.dayOfYear + firstDayOfMonth: typeof Time.firstDayOfMonth + lastDayOfMonth: typeof Time.lastDayOfMonth + isLeapYear: typeof Time.isLeapYear + + // Number helpers + Number: typeof Number + abbreviate: typeof Number.abbreviate + humanize: typeof Number.humanize + toBytes: typeof Number.toBytes + toHumanTime: typeof Number.toHumanTime + + // Debug helpers + dump: typeof DumpDie.dump + dd: typeof DumpDie.dd +} + +/** + * Bootstrap the global helpers into the global scope. + * This enables optional global access to all helper functions. + * + * Example usage: + * ```typescript + * import { bootstrap } from '@h3ravel/support' + * + * // Make helpers globally available + * bootstrap() + * + * // Now you can use: + * Arr.chunk([1, 2, 3, 4], 2) + * // or directly: + * chunk([1, 2, 3, 4], 2) + * Str.capitalize('hello world') + * // or directly: + * capitalize('hello world') + * ``` + * + * @param target - The target object to attach helpers to (default: globalThis) + */ +export function bootstrap(target: any = globalThis): void { + const globalHelpers: GlobalHelpers = { + // Re-export helpers as modules + Arr, + Crypto, + Number, + Obj, + Str, + Time, + + // Array helpers + chunk: Arr.chunk, + collapse: Arr.collapse, + alternate: Arr.alternate, + combine: Arr.combine, + find: Arr.find, + forget: Arr.forget, + first: Arr.first, + last: Arr.last, + isEmpty: Arr.isEmpty, + isNotEmpty: Arr.isNotEmpty, + pop: Arr.pop, + prepend: Arr.prepend, + take: Arr.take, + reverse: Arr.reverse, + shift: Arr.shift, + range: Arr.range, + flatten: Arr.flatten, + + // String helpers + after: Str.after, + afterLast: Str.afterLast, + before: Str.before, + beforeLast: Str.beforeLast, + capitalize: Str.capitalize, + pluralize: Str.pluralize, + singularize: Str.singularize, + slugify: Str.slugify, + subString: Str.subString, + substitute: Str.substitute, + truncate: Str.truncate, + substr: Str.substr, + sub: Str.sub, + esc: Str.esc, + padString: Str.padString, + split: Str.split, + chop: Str.chop, + isNumber: Str.isNumber, + isInteger: Str.isInteger, + rot: Str.rot, + replacePunctuation: Str.replacePunctuation, + translate: Str.translate, + ss: Str.ss, + firstLines: Str.firstLines, + lastLines: Str.lastLines, + + // Object helpers + dot: Obj.dot, + extractProperties: Obj.extractProperties, + getValue: Obj.getValue, + modObj: Obj.modObj, + safeDot: Obj.safeDot as any, + setNested: Obj.setNested, + slugifyKeys: Obj.slugifyKeys, + + // Crypto helpers + uuid: Crypto.uuid, + random: Crypto.random, + randomSecure: Crypto.randomSecure, + hash: Crypto.hash, + hmac: Crypto.hmac, + base64Encode: Crypto.base64Encode, + base64Decode: Crypto.base64Decode, + xor: Crypto.xor, + randomColor: Crypto.randomColor, + randomPassword: Crypto.randomPassword, + secureToken: Crypto.secureToken, + checksum: Crypto.checksum, + verifyChecksum: Crypto.verifyChecksum, + caesarCipher: Crypto.caesarCipher, + + // Time helpers + now: Time.now, + unix: Time.unix, + format: Time.format, + fromTimestamp: Time.fromTimestamp, + diff: Time.diff, + subtract: Time.subtract, + add: Time.add, + start: Time.start, + end: Time.end, + fromNow: Time.fromNow, + randomTime: Time.randomTime, + isBetween: Time.isBetween, + dayOfYear: Time.dayOfYear, + firstDayOfMonth: Time.firstDayOfMonth, + lastDayOfMonth: Time.lastDayOfMonth, + isLeapYear: Time.isLeapYear, + + // Number helpers + abbreviate: Number.abbreviate, + humanize: Number.humanize, + toBytes: Number.toBytes, + toHumanTime: Number.toHumanTime, + + // Debug helpers + dump: DumpDie.dump, + dd: DumpDie.dd, + } + + // Attach helpers to target + Object.assign(target, globalHelpers) +} + +/** + * Clean up global helpers by removing them from the global scope. + * This function removes all global helper attachments. + * + * @param target - The target object to clean up (default: globalThis) + */ +export function cleanBootstrap(target: any = globalThis): void { + const helpersToRemove = [ + // Array helpers + 'Arr', 'chunk', 'collapse', 'alternate', 'combine', 'each', 'keys', 'find', + 'forget', 'first', 'last', 'isEmpty', 'isNotEmpty', 'pop', 'prepend', 'take', + 'reverse', 'shift', 'range', 'where', 'skip', 'flatten', + + // String helpers + 'Str', 'after', 'afterLast', 'before', 'beforeLast', 'capitalize', 'pluralize', + 'singularize', 'slugify', 'subString', 'substitute', 'truncate', 'startsWith', + 'endsWith', 'substr', 'sub', 'esc', 'padString', 'trim', 'ltrim', 'rtrim', + 'trimChars', 'split', 'chop', 'isNumber', 'isInteger', 'rot', 'replacePunctuation', + 'translate', 'ss', 'firstLines', 'lastLines', + + // Object helpers + 'Obj', 'dot', 'extractProperties', 'getValue', 'modObj', 'safeDot', 'setNested', 'slugifyKeys', + + // Crypto helpers + 'Crypto', 'uuid', 'random', 'randomSecure', 'hash', 'hmac', 'base64Encode', + 'base64Decode', 'xor', 'randomColor', 'randomPassword', 'secureToken', + 'checksum', 'verifyChecksum', 'caesarCipher', + + // Time helpers + 'Time', 'now', 'unix', 'format', 'fromTimestamp', 'diff', 'subtract', 'add', + 'start', 'end', 'fromNow', 'randomTime', 'isBetween', 'dayOfYear', + 'firstDayOfMonth', 'lastDayOfMonth', 'isLeapYear', + + // Number helpers + 'Number', 'abbreviate', 'humanize', 'toBytes', 'toHumanTime', + + // Debug helpers + 'dump', 'dd' + ] + + helpersToRemove.forEach(helper => { + if (helper in target) { + delete target[helper] + } + }) +} + +// Also export as default bootstrap function for convenience +export default bootstrap diff --git a/packages/support/src/Helpers/Arr.ts b/packages/support/src/Helpers/Arr.ts index 6044a356..3f57b0aa 100644 --- a/packages/support/src/Helpers/Arr.ts +++ b/packages/support/src/Helpers/Arr.ts @@ -18,6 +18,79 @@ export const chunk = (arr: T[], size: number = 2): T[][] => { return chunks } +/** + * Collapse an array of arrays into a single array. + */ +export const collapse = (arr: (T | T[])[]): T[] => { + const result: T[] = [] + for (const item of arr) { + if (Array.isArray(item)) result.push(...item) + else result.push(item) + } + return result +} + +/** + * Alternates between two arrays, creating a zipped result. + */ +export const alternate = (a: T[], b: T[]): T[] => { + const result: T[] = [] + const max = Math.max(a.length, b.length) + for (let i = 0; i < max; i++) { + if (i < a.length) result.push(a[i]) + if (i < b.length) result.push(b[i]) + } + return result +} + +/** + * Combine arrays and sum their values element by element. + */ +export const combine = (...arr: number[][]): number[] => { + const maxLength = Math.max(...arr.map(a => a.length)) + const result: number[] = new Array(maxLength).fill(0) + for (let i = 0; i < maxLength; i++) { + for (const array of arr) result[i] += (array[i] || 0) + } + return result +} + +/** Find the value associated with a given key. */ +export const find = (key: T, arr: T[]): T | null => arr.find(item => item === key) || null + +/** Returns a new array without the given indices. */ +export const forget = (arr: T[], keys: number[]): T[] => arr.filter((_, i) => !keys.includes(i)) + +/** Remove the first element and return tuple [el, rest]. */ +export const first = (arr: T[]): [T, T[]] => { + if (!arr.length) throw new Error('Cannot shift from empty array') + return [arr[0], arr.slice(1)] +} + +/** Remove the last element and return tuple [el, rest]. */ +export const last = (arr: T[]): [T, T[]] => { + if (!arr.length) throw new Error('Cannot pop from empty array') + const lastItem = arr[arr.length - 1] + return [lastItem, arr.slice(0, -1)] +} + +/** Check if array is empty. */ +export const isEmpty = (arr: T[]): boolean => arr.length === 0 + +/** Pop the element off the end of array. */ +export const pop = (arr: T[]): T[] => arr.slice(0, -1) + +/** Add elements to the beginning of array. */ +export const prepend = (arr: T[], ...elements: T[]): T[] => [...elements, ...arr] + +/** Take first n elements of array. */ +export const take = (amount: number, arr: T[]): T[] => arr.slice(0, Math.max(0, amount)) + +/** Create a new array in reverse order. */ +export const reverse = (arr: T[]): T[] => [...arr].reverse() + +/** Alias for first element removal. */ +export const shift = first /** * Generates an array of sequential numbers. @@ -28,6 +101,15 @@ export const chunk = (arr: T[], size: number = 2): T[][] => { */ export const range = (size: number, startAt: number = 0): number[] => { if (size <= 0 || !Number.isFinite(size)) return [] - return Array.from({ length: size }, (_, i) => startAt + i) } + +/** Flatten multi-dimensional arrays into single level. */ +export const flatten = (arr: T[]): T[] => { + const result: T[] = [] + const recurse = (input: any[]): void => { + for (const item of input) Array.isArray(item) ? recurse(item) : result.push(item) + } + recurse(arr as any[]) + return result +} diff --git a/packages/support/src/Helpers/Crypto.ts b/packages/support/src/Helpers/Crypto.ts new file mode 100644 index 00000000..04713a01 --- /dev/null +++ b/packages/support/src/Helpers/Crypto.ts @@ -0,0 +1,196 @@ +import { randomUUID, randomBytes, createHash, createHmac } from 'crypto' + +/** + * Generate a random UUID string. + * + * @returns A random UUID string + */ +export const uuid = (): string => { + return randomUUID() +} + +/** + * Generate a random string of specified length. + * + * @param length - Length of the random string (default: 16) + * @param charset - Character set to use (default: alphanumeric) + * @returns A random string + */ +export const random = (length: number = 16, charset: string = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): string => { + let result = '' + for (let i = 0; i < length; i++) { + result += charset.charAt(Math.floor(Math.random() * charset.length)) + } + return result +} + +/** + * Secure random string generator that uses crypto.randomBytes. + * + * @param length - Length of the random string (default: 32) + * @returns A cryptographically secure random string + */ +export const randomSecure = (length: number = 32): string => { + return randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length) +} + +/** + * Hash a string using the specified algorithm. + * + * @param data - Data to hash + * @param algorithm - Hash algorithm (default: 'sha256') + * @returns Hexadecimal hash string + */ +export const hash = (data: string, algorithm: string = 'sha256'): string => { + return createHash(algorithm).update(data).digest('hex') +} + +/** + * Hash a string with salt using HMAC. + * + * @param data - Data to hash + * @param key - Secret key for HMAC + * @param algorithm - Hash algorithm (default: 'sha256') + * @returns Hexadecimal hash string + */ +export const hmac = (data: string, key: string, algorithm: string = 'sha256'): string => { + return createHmac(algorithm, key).update(data).digest('hex') +} + +/** + * Encode data to base64. + * + * @param data - Data to encode + * @returns Base64 encoded string + */ +export const base64Encode = (data: string): string => { + return Buffer.from(data, 'utf8').toString('base64') +} + +/** + * Decode base64 data. + * + * @param data - Base64 string to decode + * @returns Decoded string + */ +export const base64Decode = (data: string): string => { + return Buffer.from(data, 'base64').toString('utf8') +} + +/** + * Simple XOR encryption/decryption. + * + * @param data - Data to encrypt/decrypt + * @param key - Encryption key + * @returns Encrypted/decrypted string + */ +export const xor = (data: string, key: string): string => { + let result = '' + const keyLength = key.length + + for (let i = 0; i < data.length; i++) { + const dataCharCode = data.charCodeAt(i) + const keyCharCode = key.charCodeAt(i % keyLength) + result += String.fromCharCode(dataCharCode ^ keyCharCode) + } + + return result +} + +/** + * Generate a random hex color code. + * + * @returns A hex color code string (e.g., '#a3b2f3') + */ +export const randomColor = (): string => { + const hex = random(6, '0123456789abcdef').toLowerCase() + return `#${hex}` +} + +/** + * Generate a secure password using configurable parameters. + * + * @param length - Password length (default: 16) + * @param options - Character options + * @returns A secure password string + */ +export interface PasswordOptions { + useUppercase?: boolean + useLowercase?: boolean + useNumbers?: boolean + useSymbols?: boolean +} + +export const randomPassword = (length: number = 16, options: PasswordOptions = {}): string => { + const defaults: Required = { + useUppercase: true, + useLowercase: true, + useNumbers: true, + useSymbols: true + } + + const opts = { ...defaults, ...options } + let charset = '' + + if (opts.useUppercase) charset += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + if (opts.useLowercase) charset += 'abcdefghijklmnopqrstuvwxyz' + if (opts.useNumbers) charset += '0123456789' + if (opts.useSymbols) charset += '!@#$%^&*()_+-=[]{}|;:,.<>?' + + if (charset.length === 0) { + throw new Error('At least one character type must be enabled') + } + + return random(length, charset) +} + +/** + * Generate a cryptographically secure token for APIs, sessions, etc. + * + * @param strength - Token strength (bytes) (default: 32) + * @returns A secure token string + */ +export const secureToken = (strength: number = 32): string => { + return randomBytes(strength).toString('hex') +} + +/** + * Create a checksum for data integrity verification. + * + * @param data - Data to create checksum for + * @param algorithm - Hash algorithm (default: 'sha256') + * @returns SHA256 checksum + */ +export const checksum = (data: string, algorithm: string = 'sha256'): string => { + return hash(data, algorithm) +} + +/** + * Verify data integrity using checksum. + * + * @param data - Data to verify + * @param expectedChecksum - Expected checksum + * @param algorithm - Hash algorithm (default: 'sha256') + * @returns True if checksums match + */ +export const verifyChecksum = (data: string, expectedChecksum: string, algorithm: string = 'sha256'): boolean => { + const actualChecksum = checksum(data, algorithm) + return actualChecksum === expectedChecksum +} + +/** + * Simple Caesar cipher implementation. + * + * @param text - Text to encrypt/decrypt + * @param shift - Number of positions to shift (default: 13) + * @returns Encrypted/decrypted text + */ +export const caesarCipher = (text: string, shift: number = 13): string => { + return text.replace(/[a-zA-Z]/g, (char: string) => { + const isUpperCase = char === char.toUpperCase() + const baseCharCode = isUpperCase ? 'A' : 'a' + const shiftedCharCode = ((char.charCodeAt(0) - baseCharCode.charCodeAt(0) + shift) % 26) + const newCharCode = (shiftedCharCode < 0 ? 26 + shiftedCharCode : shiftedCharCode) + baseCharCode.charCodeAt(0) + return String.fromCharCode(newCharCode) + }) +} diff --git a/packages/support/src/Helpers/Str.ts b/packages/support/src/Helpers/Str.ts index 466db99a..a9e63d40 100644 --- a/packages/support/src/Helpers/Str.ts +++ b/packages/support/src/Helpers/Str.ts @@ -52,205 +52,159 @@ export const beforeLast = (value: string, search: string): string => { return lastIndex !== -1 ? value.slice(0, lastIndex) : value } -/** - * Capitalizes the first character of a string. - * - * @param str - The input string - * @returns The string with the first character capitalized - */ +/** Capitalizes the first character of a string. */ export function capitalize (str: string): string { - if (!str) return '' // Handle empty or undefined strings safely + if (!str) return '' return str[0].toUpperCase() + str.slice(1) } - /** * Returns the pluralized form of a word based on the given number. - * - * @param word - The word to pluralize - * @param count - The number determining pluralization - * @returns Singular if count === 1, otherwise plural form */ export const pluralize = (word: string, count: number): string => { - // If count is exactly 1 → singular if (count === 1) return word - - // Irregular plurals map const irregularPlurals: Record = { - foot: 'feet', - child: 'children', - mouse: 'mice', - goose: 'geese', - person: 'people', - man: 'men', - woman: 'women', - } - - // Handle irregular cases first - if (word in irregularPlurals) { - return irregularPlurals[word] + foot: 'feet', child: 'children', mouse: 'mice', goose: 'geese', + person: 'people', man: 'men', woman: 'women', } - - // If word ends with consonant + "y" → replace "y" with "ies" - if ( - word.endsWith('y') && - !['a', 'e', 'i', 'o', 'u'].includes(word[word.length - 2]?.toLowerCase() ?? '') - ) { + if (word in irregularPlurals) return irregularPlurals[word] + if (word.endsWith('y') && !['a','e','i','o','u'].includes(word.at(-2)?.toLowerCase() ?? '')) { return word.slice(0, -1) + 'ies' } - - // If word ends in "s", "ss", "sh", "ch", "x", or "z" → add "es" - if (/(s|ss|sh|ch|x|z)$/i.test(word)) { - return word + 'es' - } - - // Default: just add "s" + if (/(s|ss|sh|ch|x|z)$/i.test(word)) return word + 'es' return word + 's' } -/** - * Converts a plural English word into its singular form. - * - * @param word - The word to singularize - * @returns The singular form of the word - */ +/** Converts a plural English word into its singular form. */ export const singularize = (word: string): string => { - // Irregular plurals map (reverse of pluralize) const irregulars: Record = { - feet: 'foot', - children: 'child', - mice: 'mouse', - geese: 'goose', - people: 'person', - men: 'man', - women: 'woman', + feet: 'foot', children: 'child', mice: 'mouse', geese: 'goose', + people: 'person', men: 'man', women: 'woman', } - - // Handle irregular cases if (word in irregulars) return irregulars[word] - - // Words ending in "ies" → change to "y" (e.g., "bodies" → "body") - if (/ies$/i.test(word) && word.length > 3) { - return word.replace(/ies$/i, 'y') - } - - // Words ending in "es" after certain consonants → remove "es" - if (/(ches|shes|sses|xes|zes)$/i.test(word)) { - return word.replace(/es$/i, '') - } - - // Generic case: remove trailing "s" - if (/s$/i.test(word) && word.length > 1) { - return word.replace(/s$/i, '') - } - + if (/ies$/i.test(word) && word.length > 3) return word.replace(/ies$/i, 'y') + if (/(ches|shes|sses|xes|zes)$/i.test(word)) return word.replace(/es$/i, '') + if (/s$/i.test(word) && word.length > 1) return word.replace(/s$/i, '') return word } -/** - * Converts a string into a slug format. - * Handles camelCase, spaces, and non-alphanumeric characters. - * - * @param str - The input string to slugify - * @param joiner - The character used to join words (default: "_") - * @returns A slugified string - */ +/** Converts a string into a slug format. */ export const slugify = (str: string, joiner = '_'): string => { - return str - // Handle camelCase by adding joiner between lowercase → uppercase + const core = str .replace(/([a-z])([A-Z])/g, `$1${joiner}$2`) - // Replace spaces and non-alphanumeric characters with joiner .replace(/[\s\W]+/g, joiner) - // Remove duplicate joiners .replace(new RegExp(`${joiner}{2,}`, 'g'), joiner) - // Trim joiners from start/end - .replace(new RegExp(`^${joiner}|${joiner}$`, 'g'), '') .toLowerCase() + return core } -/** - * Truncates a string to a specified length and appends an ellipsis if needed. - * - * @param str - The input string - * @param len - Maximum length of the result (including ellipsis) - * @param ellipsis - String to append if truncated (default: "...") - * @returns The truncated string - */ +/** Truncates a string to a specified length and appends an ellipsis if needed. */ export const subString = ( str: string, len: number, ellipsis: string = '...' ): string => { if (!str) return '' - if (len <= ellipsis.length) return ellipsis // Avoid negative slicing - - return str.length > len - ? str.substring(0, len - ellipsis.length).trimEnd() + ellipsis - : str + if (len <= ellipsis.length) return ellipsis + return str.length > len ? str.substring(0, len - ellipsis.length).trimEnd() + ellipsis : str } -/** - * Replaces placeholders in a string with corresponding values from a data object. - * - * Example: - * substitute("Hello { user.name }!", { user: { name: "John" } }) - * // "Hello John!" - * - * @param str - The string containing placeholders wrapped in { } braces. - * @param data - Object containing values to substitute. Supports nested keys via dot notation. - * @param def - Default value to use if a key is missing. (Optional) - * @returns The substituted string or undefined if the input string or data is invalid. - */ +/** Substitute placeholders { key } using object with dot notation. */ export const substitute = ( str: string, data: Record = {}, def?: string ): string | undefined => { if (!str || !data) return undefined - - // Matches { key } or { nested.key } placeholders const regex = /{\s*([a-zA-Z0-9_.]+)\s*}/g - - // Flatten the data so we can directly access dot notation keys const flattened = dot(data) - - // Replace each placeholder with its value or the default - const out = str.replace(regex, (_, key: string) => { + return str.replace(regex, (_, key: string) => { const value = flattened[key] return value !== undefined ? String(value) : def ?? '' }) - - return out } -/** - * Truncates a string to a specified length, removing HTML tags and - * appending a suffix if the string exceeds the length. - * - * @param str - The string to truncate - * @param len - Maximum length (default: 20) - * @param suffix - Suffix to append if truncated (default: "...") - * @returns The truncated string - */ +/** Truncate string removing HTML tags and append suffix if needed. */ export const truncate = ( str: string, len: number = 20, suffix: string = '...' ): string => { if (!str) return '' - - // Remove any HTML tags const clean = str.replace(/<[^>]+>/g, '') + const out = clean.length > len ? clean.substring(0, len - suffix.length) + suffix : clean + return out.replace(/\n/g, ' ').replace(new RegExp(`\\s+${suffix.replace(/\./g, '\\.')}$`), suffix) +} + +/** Get substring from offset/length similar to PHP substr. */ +export const substr = (string: string, offset: number, length?: number): string => { + if (offset < 0) offset += string.length + if (length === undefined) return string.substring(offset) + return string.substring(offset, offset + length) +} - // Determine if we need to truncate - const truncated = - clean.length > len - ? clean.substring(0, len - suffix.length) + suffix - : clean +/** Get substring by start/stop indexes. */ +export const sub = (string: string, start: number, stop: number): string => string.substring(start, stop) - // Normalize spaces and line breaks - return truncated - .replace(/\n/g, ' ') // Replace all line breaks - .replace(new RegExp(`\\s+${suffix.replace(/\./g, '\\.')}$`), suffix) // Avoid extra space before suffix +/** Escape string for JSON encoding (returns string without quotes). */ +export const esc = (string: string): string => JSON.stringify(string).slice(1, -1) + +/** Padding to a fixed size, right by default. */ +export const padString = ( + string: string, + size: number, + padString: string = ' ', + padRight: boolean = true +): string => { + if (string.length >= size) return string + const pad = padString.repeat(size - string.length) + return padRight ? string + pad : pad + string +} + +/** Split by delimiter with edge-case rule. */ +export const split = (string: string, delimiter: string): string[] => { + if (string.startsWith(delimiter) || string.endsWith(delimiter)) return [''] + return string.split(delimiter) +} + +/** Returns all the characters except the last. */ +export const chop = (string: string): string => string.slice(0, -1) + +/** Number checks. */ +export const isNumber = (string: string): boolean => !isNaN(Number(string)) && string.trim() !== '' +export const isInteger = (string: string): boolean => Number.isInteger(Number(string)) && string.trim() !== '' + +/** ROT-N cipher. */ +export const rot = (string: string, n: number = 13): string => { + return string.replace(/[a-zA-Z]/g, (char: string) => { + const code = char.charCodeAt(0) + const start = char >= 'a' ? 'a'.charCodeAt(0) : 'A'.charCodeAt(0) + const end = char >= 'a' ? 'z'.charCodeAt(0) : 'Z'.charCodeAt(0) + let next = code + n + while (next < start) next += 26 + while (next > end) next -= 26 + return String.fromCharCode(next) + }) } +/** Replace trailing punctuation with new format. */ +export const replacePunctuation = (string: string, newFormat: string): string => string.replace(/[.,;:!?]*$/, '') + newFormat + +/** Array/object driven text replacement. */ +export const translate = (string: string, replacements: Record | Array<[string, string]>): string => { + let result = string + if (Array.isArray(replacements)) { + for (const [from, to] of replacements) result = result.replace(new RegExp(from, 'g'), to) + } else { + for (const [from, to] of Object.entries(replacements)) result = result.replace(new RegExp(from, 'g'), to) + } + return result +} + +/** Strip slashes recursively. */ +export const ss = (string: string): string => string.replace(/\\(.)/g, '$1') + +/** First and last N lines. */ +export const firstLines = (string: string, amount: number = 1): string => string.split('\n').slice(0, amount).join('\n') +export const lastLines = (string: string, amount: number = 1): string => string.split('\n').slice(-amount).join('\n') + diff --git a/packages/support/src/Helpers/Time.ts b/packages/support/src/Helpers/Time.ts new file mode 100644 index 00000000..8b204270 --- /dev/null +++ b/packages/support/src/Helpers/Time.ts @@ -0,0 +1,243 @@ +export type TimeFormat = 'Y-m-d' | 'Y-m-d H:i:s' | 'd-m-Y' | 'd/m/Y' | 'M j, Y' | 'F j, Y' | 'D j M' | 'timestamp' | 'unix' +export type TimeUnit = 'milliseconds' | 'seconds' | 'minutes' | 'hours' | 'days' + +/** + * Get current timestamp in milliseconds. + * + * @returns Current timestamp as number + */ +export const now = (): number => { + return Date.now() +} + +/** + * Get current Unix timestamp. + * + * @returns Current Unix timestamp + */ +export const unix = (): number => { + return Math.floor(Date.now() / 1000) +} + +/** + * Format a date string according to a specified format (UTC-based for determinism). + * + * @param date - Date string or Date object + * @param format - Format to output (default: 'Y-m-d H:i:s') + * @returns Formatted date string + */ +export const format = (date: string | Date, format: TimeFormat = 'Y-m-d H:i:s'): string => { + const d = new Date(date) + + if (isNaN(d.getTime())) { + throw new Error('Invalid date provided') + } + + const year = d.getUTCFullYear() + const month = String(d.getUTCMonth() + 1).padStart(2, '0') + const day = String(d.getUTCDate()).padStart(2, '0') + const hours = String(d.getUTCHours()).padStart(2, '0') + const minutes = String(d.getUTCMinutes()).padStart(2, '0') + const seconds = String(d.getUTCSeconds()).padStart(2, '0') + + const monthNames = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ] + const monthNamesShort = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ] + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + + switch (format) { + case 'Y-m-d': + return `${year}-${month}-${day}` + case 'Y-m-d H:i:s': + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` + case 'd-m-Y': + return `${day}-${month}-${year}` + case 'd/m/Y': + return `${day}/${month}/${year}` + case 'M j, Y': + return `${monthNamesShort[d.getUTCMonth()]} ${d.getUTCDate()}, ${year}` + case 'F j, Y': + return `${monthNames[d.getUTCMonth()]} ${d.getUTCDate()}, ${year}` + case 'D j M': + return `${dayNames[d.getUTCDay()]} ${d.getUTCDate()} ${monthNamesShort[d.getUTCMonth()]}` + case 'timestamp': + return d.toISOString() + case 'unix': + return Math.floor(d.getTime() / 1000).toString() + default: + return d.toISOString() + } +} + +/** + * Create a date for a given timestamp. + * + * @param timestamp - Unix timestamp + * @returns Date object + */ +export const fromTimestamp = (timestamp: number): Date => { + return new Date(timestamp * 1000) +} + +/** + * Return the difference for given date in seconds. + * + * @param date - Date to compare + * @param referenceDate - Reference date (optional, defaults to now) + * @returns Number of seconds difference + */ +export const diff = (date: string | Date, referenceDate?: string | Date): number => { + const d1 = new Date(date) + const d2 = referenceDate ? new Date(referenceDate) : new Date() + + if (isNaN(d1.getTime()) || isNaN(d2.getTime())) { + throw new Error('Invalid date provided') + } + + const diffInSeconds = Math.floor((d2.getTime() - d1.getTime()) / 1000) + return diffInSeconds +} + +/** + * Subtract time from the given date. + */ +export const subtract = (date: string | Date, amount: number = 1, unit: TimeUnit = 'days'): Date => { + const d = new Date(date) + if (isNaN(d.getTime())) throw new Error('Invalid date provided') + const amounts: Record = { + milliseconds: 1, + seconds: 1000, + minutes: 60 * 1000, + hours: 60 * 60 * 1000, + days: 24 * 60 * 60 * 1000 + } + const multiplier = amounts[unit] || 1000 + return new Date(d.getTime() - (amount * multiplier)) +} + +/** + * Add time to the given date. + */ +export const add = (date: string | Date, amount: number = 1, unit: TimeUnit = 'days'): Date => { + const d = new Date(date) + if (isNaN(d.getTime())) throw new Error('Invalid date provided') + const amounts: Record = { + milliseconds: 1, + seconds: 1000, + minutes: 60 * 1000, + hours: 60 * 60 * 1000, + days: 24 * 60 * 60 * 1000 + } + const multiplier = amounts[unit] || 1000 + return new Date(d.getTime() + (amount * multiplier)) +} + +/** + * Start time of a specific unit. + */ +export const start = (date: string | Date, unit: TimeUnit = 'days'): Date => { + const d = new Date(date) + if (isNaN(d.getTime())) throw new Error('Invalid date provided') + const newDt = new Date(d) + switch (unit) { + case 'days': newDt.setHours(0, 0, 0, 0); break + case 'hours': newDt.setMinutes(0, 0, 0); break + case 'minutes': newDt.setSeconds(0, 0); break + case 'seconds': newDt.setMilliseconds(0); break + case 'milliseconds': break + } + return newDt +} + +/** + * End time of a specific unit. + */ +export const end = (date: string | Date, unit: TimeUnit = 'days'): Date => { + const d = new Date(date) + if (isNaN(d.getTime())) throw new Error('Invalid date provided') + const newDt = new Date(d) + switch (unit) { + case 'days': newDt.setHours(23, 59, 59, 999); break + case 'hours': newDt.setMinutes(59, 59, 999); break + case 'minutes': newDt.setSeconds(59, 999); break + case 'seconds': newDt.setMilliseconds(999); break + case 'milliseconds': break + } + return newDt +} + +/** + * Get the difference in days from today. + */ +export const fromNow = (date: string | Date): number => { + return diff(date) / (24 * 60 * 60) +} + +/** + * Get a random time between the specified hour and minute. + */ +export const randomTime = ( + startHour: number = 9, + startMinute: number = 0, + endHour: number = 17, + endMinute: number = 0 +): Date => { + const today = new Date() + const startMinutes = startHour * 60 + startMinute + const endMinutes = endHour * 60 + endMinute + const randomMinutes = Math.floor(Math.random() * (endMinutes - startMinutes)) + startMinutes + const hour = Math.floor(randomMinutes / 60) + const minute = randomMinutes % 60 + const date = new Date(today) + date.setHours(hour, minute, 0, 0) + return date +} + +/** + * Check if the current time is between the specified durations. + */ +export const isBetween = (startTime: string, endTime: string): boolean => { + const now = new Date() + const currentHours = now.getHours() + const currentMinutes = now.getMinutes() + const currentTotalMinutes = currentHours * 60 + currentMinutes + const parseTime = (timeStr: string) => { + const [hours, minutes] = timeStr.split(':').map(Number) + return hours * 60 + minutes + } + const startTotalMinutes = parseTime(startTime) + const endTotalMinutes = parseTime(endTime) + if (startTotalMinutes <= endTotalMinutes) { + return currentTotalMinutes >= startTotalMinutes && currentTotalMinutes <= endTotalMinutes + } else { + return currentTotalMinutes >= startTotalMinutes || currentTotalMinutes <= endTotalMinutes + } +} + +/** Day of year, first/last day of month, leap year checks. */ +export const dayOfYear = (date: string | Date = new Date()): number => { + const d = new Date(date) + const start = new Date(Date.UTC(d.getUTCFullYear(), 0, 0)) + const diff = d.getTime() - start.getTime() + return Math.floor(diff / (1000 * 60 * 60 * 24)) +} + +export const firstDayOfMonth = (date: string | Date = new Date()): Date => { + const d = new Date(date) + return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1)) +} + +export const lastDayOfMonth = (date: string | Date = new Date()): Date => { + const d = new Date(date) + return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)) +} + +export const isLeapYear = (year: number = new Date().getUTCFullYear()): boolean => { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 +} + diff --git a/packages/support/src/index.ts b/packages/support/src/index.ts index c59c200a..c6811a64 100644 --- a/packages/support/src/index.ts +++ b/packages/support/src/index.ts @@ -1,7 +1,10 @@ export * from './Contracts/ObjContract' export * from './Contracts/StrContract' +export * from './GlobalBootstrap' export * from './Helpers/Arr' +export * from './Helpers/Crypto' export * from './Helpers/DumpDie' export * from './Helpers/Number' export * from './Helpers/Obj' export * from './Helpers/Str' +export * from './Helpers/Time' diff --git a/packages/support/tests/arr.test.ts b/packages/support/tests/arr.test.ts new file mode 100644 index 00000000..8a64a4cf --- /dev/null +++ b/packages/support/tests/arr.test.ts @@ -0,0 +1,54 @@ +import * as Arr from '../src/Helpers/Arr' + +describe('Arr helpers', () => { + test('chunk: splits into chunks and handles remainders', () => { + expect(Arr.chunk([1,2,3,4,5], 2)).toEqual([[1,2],[3,4],[5]]) + expect(Arr.chunk([], 2)).toEqual([]) + expect(() => Arr.chunk([1], 0)).toThrow('Chunk size must be greater than 0') + }) + + test('collapse: flattens one level', () => { + expect(Arr.collapse([1,[2,3],[4]])).toEqual([1,2,3,4]) + }) + + test('alternate: zips two arrays of different sizes', () => { + expect(Arr.alternate([1,3,5], [2,4])).toEqual([1,2,3,4,5]) + expect(Arr.alternate([], [1,2])).toEqual([1,2]) + }) + + test('combine: sums by index across arrays', () => { + expect(Arr.combine([1,2,3],[4,5,6])).toEqual([5,7,9]) + expect(Arr.combine([1],[2,3,4])).toEqual([3,3,4]) + }) + + test('find/forget/first/last/isEmpty', () => { + expect(Arr.find(2, [1,2,3])).toBe(2) + expect(Arr.find(4, [1,2,3])).toBeNull() + expect(Arr.forget([1,2,3,4], [1,3])).toEqual([1,3]) + expect(Arr.isEmpty([])).toBe(true) + const [first, rest1] = Arr.first([1,2,3]) + expect(first).toBe(1) + expect(rest1).toEqual([2,3]) + const [last, rest2] = Arr.last([1,2,3]) + expect(last).toBe(3) + expect(rest2).toEqual([1,2]) + }) + + test('pop/prepend/take/reverse/shift', () => { + expect(Arr.pop([1,2,3])).toEqual([1,2]) + expect(Arr.prepend([2,3], 0, 1)).toEqual([0,1,2,3]) + expect(Arr.take(0, [1,2,3])).toEqual([]) + expect(Arr.take(2, [1,2,3])).toEqual([1,2]) + expect(Arr.reverse([1,2,3])).toEqual([3,2,1]) + const [shifted, rest] = Arr.shift([1,2,3]) + expect(shifted).toBe(1) + expect(rest).toEqual([2,3]) + }) + + test('range/flatten', () => { + expect(Arr.range(3)).toEqual([0,1,2]) + expect(Arr.range(3, 5)).toEqual([5,6,7]) + expect(Arr.range(0)).toEqual([]) + expect(Arr.flatten([1,[2,[3]],4])).toEqual([1,2,3,4]) + }) +}) diff --git a/packages/support/tests/crypto.test.ts b/packages/support/tests/crypto.test.ts new file mode 100644 index 00000000..0f3c4364 --- /dev/null +++ b/packages/support/tests/crypto.test.ts @@ -0,0 +1,42 @@ +import * as Crypto from '../src/Helpers/Crypto' + +describe('Crypto helpers', () => { + test('uuid format', () => { + const id = Crypto.uuid() + expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + }) + + test('random vs randomSecure length', () => { + expect(Crypto.random(10)).toHaveLength(10) + expect(Crypto.randomSecure(10)).toHaveLength(10) + }) + + test('hash/hmac/base64/xor', () => { + const h = Crypto.hash('hello') + expect(h).toHaveLength(64) + const hm = Crypto.hmac('data','key') + expect(hm).toHaveLength(64) + const b64 = Crypto.base64Encode('hi') + expect(Crypto.base64Decode(b64)).toBe('hi') + const text = 'secret' + const key = 'k' + const enc = Crypto.xor(text, key) + expect(Crypto.xor(enc, key)).toBe(text) + }) + + test('colors/password/token/checksum', () => { + expect(Crypto.randomColor()).toMatch(/^#[0-9a-f]{6}$/i) + const pwd = Crypto.randomPassword(12) + expect(pwd).toHaveLength(12) + const token = Crypto.secureToken(16) + expect(token).toHaveLength(32) + const sum = Crypto.checksum('abc') + expect(Crypto.verifyChecksum('abc', sum)).toBe(true) + expect(Crypto.verifyChecksum('abd', sum)).toBe(false) + }) + + test('caesarCipher roundtrip', () => { + const enc = Crypto.caesarCipher('Hello Zz', 3) + expect(Crypto.caesarCipher(enc, -3)).toBe('Hello Zz') + }) +}) diff --git a/packages/support/tests/str.test.ts b/packages/support/tests/str.test.ts index c1818077..f9b9fbd9 100644 --- a/packages/support/tests/str.test.ts +++ b/packages/support/tests/str.test.ts @@ -1,7 +1,49 @@ -import { pluralize } from '../src/Helpers/Str' +import { after, afterLast, before, beforeLast, capitalize, pluralize, singularize, slugify, subString, substitute, truncate, substr, sub, esc, padString, split, chop, isNumber, isInteger, rot, replacePunctuation, translate, ss, firstLines, lastLines } from '../src/Helpers/Str' -describe('Support Package\'s Str', () => { - test('plural of user is users', () => { - expect(pluralize('user', 2)).toBe('users') +describe('Str helpers', () => { + test('after/afterLast/before/beforeLast', () => { + expect(after('hello world', 'l')).toBe('lo world') + expect(after('hello', 'z')).toBe('hello') + expect(afterLast('a.b.c', '.')).toBe('c') + expect(before('hello world', ' ')).toBe('hello') + expect(beforeLast('a.b.c', '.')).toBe('a.b') + }) + + test('capitalize/slugify/subString/substring variants', () => { + expect(capitalize('hello')).toBe('Hello') + expect(slugify('Hello World!')).toBe('hello_world_') + expect(subString('abcdef', 4)).toBe('a...') + expect(substr('abcdef', 2)).toBe('cdef') + expect(substr('abcdef', -2)).toBe('ef') + expect(sub('abcdef', 1, 3)).toBe('bc') + }) + + test('substitute/truncate/esc', () => { + expect(substitute('Hi { user.name }!', { user: { name: 'Jane' } })).toBe('Hi Jane!') + expect(substitute('Test {missing}', {}, 'N/A')).toBe('Test N/A') + expect(truncate('

Hello world

', 5)).toBe('He...') + expect(esc('a"b')).toBe('a\\"b') + }) + + test('pad/split/chop/number checks/rot', () => { + expect(padString('1', 3, '0')).toBe('100') + expect(padString('1', 3, '0', false)).toBe('001') + expect(split('a,b,c', ',')).toEqual(['a','b','c']) + expect(split(',a,b,', ',')).toEqual(['']) + expect(chop('abc')).toBe('ab') + expect(isNumber('12.3')).toBe(true) + expect(isInteger('12.3')).toBe(false) + const encoded = rot('hello', 13) + expect(rot(encoded, 13)).toBe('hello') + }) + + test('replace/translate/strip slashes/first-last lines', () => { + expect(replacePunctuation('hello...', '!')).toBe('hello!') + expect(translate('hello world', { 'world': 'earth' })).toBe('hello earth') + expect(translate('foo bar baz', [['foo','f'], ['bar','b']])).toBe('f b baz') + expect(ss('a\\/b')).toBe('a/b') + const text = 'l1\nl2\nl3\nl4' + expect(firstLines(text, 2)).toBe('l1\nl2') + expect(lastLines(text, 2)).toBe('l3\nl4') }) }) diff --git a/packages/support/tests/time.test.ts b/packages/support/tests/time.test.ts new file mode 100644 index 00000000..b57bf5fc --- /dev/null +++ b/packages/support/tests/time.test.ts @@ -0,0 +1,49 @@ +import * as Time from '../src/Helpers/Time' + +describe('Time helpers', () => { + test('now/unix monotonicity', () => { + const a = Time.now() + const b = Time.now() + expect(b).toBeGreaterThanOrEqual(a) + expect(Time.unix()).toBeGreaterThan(0) + }) + + test('format and fromTimestamp', () => { + const date = new Date('2023-12-25T15:30:45Z') + expect(Time.format(date, 'Y-m-d')).toBe('2023-12-25') + expect(Time.format(date, 'Y-m-d H:i:s')).toBe('2023-12-25 15:30:45') + const d = Time.fromTimestamp(1700000000) + expect(d instanceof Date).toBe(true) + }) + + test('diff/add/subtract', () => { + const ref = new Date('2023-01-01T00:00:00Z') + const prev = new Date('2022-12-31T23:59:00Z') + expect(Time.diff(prev, ref)).toBe(60) + expect(Time.add(ref, 1, 'days').getUTCDate()).toBe(2) + expect(Time.subtract(ref, 1, 'days').getUTCDate()).toBe(31) + }) + + test('start/end', () => { + const d = new Date('2023-12-25T15:30:45') + const s = Time.start(d, 'days') + expect(s.getHours()).toBe(0) + const e = Time.end(d, 'days') + expect(e.getHours()).toBe(23) + }) + + test('isBetween/day utilities', () => { + const origH = Date.prototype.getHours + const origM = Date.prototype.getMinutes + Date.prototype.getHours = function () { return 14 } + Date.prototype.getMinutes = function () { return 30 } + expect(Time.isBetween('14:00','15:00')).toBe(true) + expect(Time.isBetween('15:00','16:00')).toBe(false) + Date.prototype.getHours = origH + Date.prototype.getMinutes = origM + expect(Time.dayOfYear(new Date('2023-01-01'))).toBe(1) + const f = Time.firstDayOfMonth(new Date('2023-12-15')) + expect(f.getDate()).toBe(1) + expect(Time.isLeapYear(2020)).toBe(true) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 109cf0fb..1f17523b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: ts-jest: specifier: ^29.4.4 version: 29.4.4(@babel/core@7.28.4)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.4))(jest-util@30.0.5)(jest@30.1.3(@types/node@24.5.2)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.5.2)(typescript@5.9.2)))(typescript@5.9.2) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.13.5)(@types/node@24.5.2)(typescript@5.9.2) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -5972,7 +5975,6 @@ snapshots: '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - optional: true '@emnapi/core@1.5.0': dependencies: @@ -6400,7 +6402,6 @@ snapshots: dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - optional: true '@js-joda/core@5.6.5': {} @@ -7148,17 +7149,13 @@ snapshots: '@tootallnate/once@1.1.2': optional: true - '@tsconfig/node10@1.0.11': - optional: true + '@tsconfig/node10@1.0.11': {} - '@tsconfig/node12@1.0.11': - optional: true + '@tsconfig/node12@1.0.11': {} - '@tsconfig/node14@1.0.3': - optional: true + '@tsconfig/node14@1.0.3': {} - '@tsconfig/node16@1.0.4': - optional: true + '@tsconfig/node16@1.0.4': {} '@tybys/wasm-util@0.10.1': dependencies: @@ -7413,7 +7410,6 @@ snapshots: acorn-walk@8.3.4: dependencies: acorn: 8.15.0 - optional: true acorn@8.15.0: {} @@ -7488,8 +7484,7 @@ snapshots: readable-stream: 3.6.2 optional: true - arg@4.1.3: - optional: true + arg@4.1.3: {} argparse@1.0.10: dependencies: @@ -7777,8 +7772,7 @@ snapshots: cookie-es@2.0.0: {} - create-require@1.1.1: - optional: true + create-require@1.1.1: {} cross-env@10.0.0: dependencies: @@ -7839,8 +7833,7 @@ snapshots: dependencies: address: 2.0.3 - diff@4.0.2: - optional: true + diff@4.0.2: {} diff@8.0.2: {} @@ -9933,7 +9926,6 @@ snapshots: yn: 3.1.1 optionalDependencies: '@swc/core': 1.13.5 - optional: true tsconfig-paths@4.2.0: dependencies: @@ -10076,8 +10068,7 @@ snapshots: uuid@9.0.1: {} - v8-compile-cache-lib@3.0.1: - optional: true + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: dependencies: @@ -10159,8 +10150,7 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yn@3.1.1: - optional: true + yn@3.1.1: {} yocto-queue@0.1.0: {}