From f2cf33575035cd8ebf1452c65740e9d4c163461b Mon Sep 17 00:00:00 2001 From: leejooy96 Date: Thu, 5 Dec 2024 13:45:43 +0900 Subject: [PATCH] refactor: separate files for each module purpose, improved tree-shaking --- .eslintrc.json | 2 +- .npmignore | 3 +- .terserrc | 10 - CHANGELOG.md | 4 +- docs/src/en/api/web.md | 2 +- .../installation-javascript.md | 7 +- docs/src/ko/api/web.md | 2 +- .../installation-javascript.md | 4 +- lib/array.ts | 169 ++ lib/crypto.ts | 85 + lib/date.ts | 91 ++ lib/format.ts | 118 ++ lib/index.ts | 1380 +---------------- lib/math.ts | 78 + lib/misc.ts | 36 + lib/object.ts | 200 +++ lib/string.ts | 344 ++++ lib/types/global.ts | 16 + lib/verify.ts | 133 ++ package-lock.json | 31 +- package.json | 4 +- terser.config.json | 10 + 22 files changed, 1334 insertions(+), 1395 deletions(-) delete mode 100644 .terserrc create mode 100644 lib/array.ts create mode 100644 lib/crypto.ts create mode 100644 lib/date.ts create mode 100644 lib/format.ts create mode 100644 lib/math.ts create mode 100644 lib/misc.ts create mode 100644 lib/object.ts create mode 100644 lib/string.ts create mode 100644 lib/types/global.ts create mode 100644 lib/verify.ts create mode 100644 terser.config.json diff --git a/.eslintrc.json b/.eslintrc.json index 4c30633..60f1b87 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,6 +15,7 @@ "arrow-parens": 0, "max-len": 0, "no-bitwise": 0, + "import/extensions": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/explicit-function-return-type": 2 @@ -23,7 +24,6 @@ { "files": ["test/*.test.ts"], "rules": { - "import/extensions": 0, "import/no-unresolved": 0, "no-undef": 0 } diff --git a/.npmignore b/.npmignore index c11b67d..c81ba37 100644 --- a/.npmignore +++ b/.npmignore @@ -17,12 +17,11 @@ CODE_OF_CONDUCT.md # For development .git/ -.terserrc +terser.config.json .eslintignore .eslintrc.json .prettierignore .prettierrc -.mocharc.json .editorconfig tsconfig.json tsconfig.prod.json diff --git a/.terserrc b/.terserrc deleted file mode 100644 index 9dea2bc..0000000 --- a/.terserrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "module": true, - "compress": true, - "ie8": false, - "safari10": false, - "mangle": {}, - "output": {}, - "parse": {}, - "rename": {} -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 853e273..1e5df2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Change Log -## 1.5.1 (2024--) +## 1.6.0 (2024--) +- **BREAKING CHANGES**: The `qsu` package no longer uses classes, so if you want to import the entire module at once, you must use something like `import * as _ from 'qsu''`. (`_` -> `* as _`) - **BREAKING CHANGES**: The `objectTo1d` method have been renamed to `objTo1d` +- Separate files for each module purpose. Improved tree-shaking. ## 1.5.0 (2024-10-24) diff --git a/docs/src/en/api/web.md b/docs/src/en/api/web.md index ec27d20..b4c869b 100644 --- a/docs/src/en/api/web.md +++ b/docs/src/en/api/web.md @@ -5,7 +5,7 @@ order: 100 # Methods: Web -This method is only available in the `qsu-web` package. +This method is only available in the `qsu-web` (JavaScript) package. ## `isBotAgent` diff --git a/docs/src/en/getting-started/installation-javascript.md b/docs/src/en/getting-started/installation-javascript.md index c833201..d8b7109 100644 --- a/docs/src/en/getting-started/installation-javascript.md +++ b/docs/src/en/getting-started/installation-javascript.md @@ -27,7 +27,7 @@ $ pnpm install qsu-web # (Optional) When using the Add-on for Web ## How to Use -### Using named import (Multiple utilities in a single require) - Recommend +일반적으로 각각의 함수를 아래와 같이 부분적으로 import하여 사용할 수 있습니다. ```javascript import { today, strCount } from 'qsu'; @@ -38,12 +38,13 @@ function main() { } ``` -### Using whole class (multiple utilities simultaneously with one object) +코드와 모듈의 구분을 위해 아래처럼 언더스코어(`_`)기호 등을 사용하여 메소드를 사용할 수 있습니다. 특별한 경우가 아니면 부분 가져오기를 사용하는 것을 권장합니다. ```javascript -import _ from 'qsu'; +import * as _ from 'qsu'; function main() { console.log(_.today()); // '20xx-xx-xx' + console.log(_.strCount('123412341234', '1')); // 3 } ``` diff --git a/docs/src/ko/api/web.md b/docs/src/ko/api/web.md index ec27d20..b4c869b 100644 --- a/docs/src/ko/api/web.md +++ b/docs/src/ko/api/web.md @@ -5,7 +5,7 @@ order: 100 # Methods: Web -This method is only available in the `qsu-web` package. +This method is only available in the `qsu-web` (JavaScript) package. ## `isBotAgent` diff --git a/docs/src/ko/getting-started/installation-javascript.md b/docs/src/ko/getting-started/installation-javascript.md index 3e0b8dc..50bd2b5 100644 --- a/docs/src/ko/getting-started/installation-javascript.md +++ b/docs/src/ko/getting-started/installation-javascript.md @@ -27,7 +27,7 @@ $ pnpm install qsu-web # (선택적) Web용 추가 유틸을 사용할 때 ## 사용 방법 -### 명명된 가져오기 사용(단일 요구 사항에 여러 유틸리티 사용) - 권장 사항 +In general, you can partially import and use each function as shown below. ```javascript import { today, strCount } from 'qsu'; @@ -38,7 +38,7 @@ function main() { } ``` -### 전체 클래스 사용(하나의 객체에 여러 유틸리티를 동시에 사용) +You can use methods with underscore (`_`) symbols to separate code and modules, as shown below. We recommend using partial imports unless there are special cases. ```javascript import _ from 'qsu'; diff --git a/lib/array.ts b/lib/array.ts new file mode 100644 index 0000000..4d4957d --- /dev/null +++ b/lib/array.ts @@ -0,0 +1,169 @@ +import type { NumberValueObject, PositiveNumber } from './types/global'; +import { is2dArray, isObject } from './verify'; + +export function arrShuffle(array: any[]): any[] { + if (array.length === 1) { + return array[0]; + } + + const newArray = array; + + for (let i = array.length - 1; i > 0; i -= 1) { + const j = Math.floor(Math.random() * (i + 1)); + [newArray[i], newArray[j]] = [array[j], array[i]]; + } + + return newArray; +} + +export function arrWithDefault(defaultValue: any, length = 0): any[] { + if (length < 1) { + return []; + } + + return Array(length).fill(defaultValue); +} + +export function arrUnique(array: any[]): any[] { + if (is2dArray(array)) { + return Array.from(new Set(array.map((x) => JSON.stringify(x))), (x) => JSON.parse(x)); + } + + return [...new Set(array)]; +} + +export function arrWithNumber(start: number, end: number): number[] { + if (start > end) { + throw new Error('`end` is greater than `start`.'); + } + + return Array.from({ length: end - start + 1 }, (_, i) => i + start); +} + +export function average(array: number[]): number { + return array.reduce((p, c) => p + c, 0) / array.length; +} + +export function arrMove( + array: any[], + from: PositiveNumber, + to: PositiveNumber +): any[] { + const arrayLength = array.length; + + if (arrayLength <= from || arrayLength <= to) { + throw new Error('Invalid move params'); + } + + array.splice(to, 0, array.splice(from, 1)[0]); + + return array; +} + +export function arrTo1dArray(array: any[]): any[] { + const convert1dArray = (arr: any[]): any[] => { + const tempArr = []; + const arrayLength = arr.length; + + for (let i = 0; i < arrayLength; i += 1) { + if (typeof arr[i] !== 'object') { + tempArr.push(arr[i]); + } else if (is2dArray(arr[i])) { + tempArr.push(...convert1dArray(arr[i])); + } else { + tempArr.push(...arr[i]); + } + } + + return tempArr; + }; + + return convert1dArray(array); +} + +export function arrRepeat(array: any, count: PositiveNumber): any[] { + if (!array || count < 1 || typeof array !== 'object') { + return []; + } + + const isObj = isObject(array); + const result: any[] = []; + + for (let i = 0, iLen = count; i < iLen; i += 1) { + if (isObj) { + result.push(array); + } else { + result.push(...array); + } + } + + return result; +} + +export function arrCount(array: string[] | number[]): NumberValueObject { + const result: NumberValueObject = {}; + + for (let i = 0; i < array.length; i += 1) { + const x = array[i]; + + result[x] = (result[x] || 0) + 1; + } + + return result; +} + +export function sortByObjectKey( + array: any[], + key: string, + descending = false, + numerically = false +): any[] { + if (numerically) { + const collator = new Intl.Collator([], { numeric: true }); + const result = array.sort((a: any, b: any) => collator.compare(a[key], b[key])); + + return descending ? result.reverse() : result; + } + + return array.sort((a: any, b: any) => { + if (!descending) { + if (a[key] < b[key]) return -1; + if (a[key] > b[key]) return 1; + + return 0; + } + + if (a[key] > b[key]) return -1; + if (a[key] < b[key]) return 1; + + return 0; + }); +} + +export function sortNumeric(array: string[], descending = false): string[] { + const collator = new Intl.Collator([], { numeric: true }); + const result = array.sort((a: any, b: any) => collator.compare(a, b)); + + return descending ? result.reverse() : result; +} + +export function arrGroupByMaxCount(array: any[], maxLengthPerGroup = 1): any[] { + const result = []; + const arrayLength = array.length; + let tempArray = []; + + for (let i = 0; i < arrayLength; i += 1) { + if (tempArray.length === maxLengthPerGroup) { + result.push(tempArray); + tempArray = []; + } + + tempArray.push(array[i]); + } + + if (tempArray.length > 0) { + result.push(tempArray); + } + + return result; +} diff --git a/lib/crypto.ts b/lib/crypto.ts new file mode 100644 index 0000000..3a6d84c --- /dev/null +++ b/lib/crypto.ts @@ -0,0 +1,85 @@ +import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'node:crypto'; + +export function encrypt( + str: string, + secret: string, + algorithm = 'aes-256-cbc', + ivSize = 16, + toBase64 = false +): string { + if (!str || str.length < 1) { + return ''; + } + + const iv: Buffer = randomBytes(ivSize); + const cipher = createCipheriv(algorithm, secret, iv); + let enc = cipher.update(str); + + enc = Buffer.concat([enc, cipher.final()]); + + const encoding: BufferEncoding = toBase64 ? 'base64' : 'hex'; + + return `${iv.toString(encoding)}:${enc.toString(encoding)}`; +} + +export function decrypt( + str: string, + secret: string, + algorithm = 'aes-256-cbc', + toBase64 = false +): string { + if (!str || str.length < 1) { + return ''; + } + + const encoding: BufferEncoding = toBase64 ? 'base64' : 'hex'; + const arrStr: any[] = str.split(':'); + const decipher = createDecipheriv(algorithm, secret, Buffer.from(arrStr.shift(), encoding)); + let decrypted = decipher.update(Buffer.from(arrStr.join(':'), encoding)); + + decrypted = Buffer.concat([decrypted, decipher.final()]); + + return decrypted.toString(); +} + +export function objectId(): string { + return ( + Math.floor(Date.now() / 1000).toString(16) + + 'x'.repeat(16).replace(/x/g, () => Math.floor(Math.random() * 16).toString(16)) + ); +} + +export function md5Hash(str: string): string { + return createHash('md5').update(str).digest('hex'); +} + +export function sha1Hash(str: string): string { + return createHash('sha1').update(str).digest('hex'); +} + +export function sha256Hash(str: string): string { + return createHash('sha256').update(str).digest('hex'); +} + +export function encodeBase64(str: string): string { + return Buffer.from(str, 'utf8').toString('base64'); +} + +export function decodeBase64(encodedStr: string): string { + return Buffer.from(encodedStr, 'base64').toString('utf8'); +} + +export function strToNumberHash(str: string): number { + if (!str) { + return 0; + } + + let hash = 0; + + for (let i = 0; i < str.length; i += 1) { + hash = (hash << 5) - hash + str.charCodeAt(i); + hash |= 0; + } + + return hash; +} diff --git a/lib/date.ts b/lib/date.ts new file mode 100644 index 0000000..e010cd6 --- /dev/null +++ b/lib/date.ts @@ -0,0 +1,91 @@ +export function dayDiff(date1: Date, date2?: Date): number { + const date2c = date2 || new Date(); + + return Math.ceil(Math.abs(date2c.getTime() - date1.getTime()) / (1000 * 3600 * 24)); +} + +export function today(separator = '-', yearFirst = true): string { + const date = new Date(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + const dateArr = [`${month < 10 ? '0' : ''}${month}`, `${day < 10 ? '0' : ''}${day}`]; + + if (yearFirst) { + dateArr.unshift(date.getFullYear().toString()); + } else { + dateArr.push(date.getFullYear().toString()); + } + + return dateArr.join(separator); +} + +export function isValidDate(dateYYYYMMDD: string): boolean { + if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(dateYYYYMMDD)) { + throw new Error("The date format must be 'YYYY-MM-DD'"); + } + + const convertedDate: string[] = dateYYYYMMDD.split('-'); + + return /^(?=\d)(?:(?:31(?!.(?:0?[2469]|11))|(?:30|29)(?!.0?2)|29(?=.0?2.(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:16|[2468][048]|[3579][26])00)(?:\x20|$))|(?:2[0-8]|1\d|0?[1-9]))([-./])(?:1[012]|0?[1-9])\1(?:1[6-9]|[2-9]\d)?\d\d(?:(?=\x20\d)\x20|$))?(((0?[1-9]|1[012])(:[0-5]\d){0,2}(\x20[AP]M))|([01]\d|2[0-3])(:[0-5]\d){1,2})?$/.test( + `${parseInt(convertedDate[2], 10)}-${parseInt(convertedDate[1], 10)}-${parseInt( + convertedDate[0], + 10 + )}` + ); +} + +export function dateToYYYYMMDD(date: Date, separator = '-'): string { + const month: number = date.getMonth() + 1; + const day: number = date.getDate(); + + return `${date.getFullYear()}${separator}${month < 10 ? `0${month}` : month}${separator}${ + day < 10 ? `0${day}` : day + }`; +} + +export function createDateListFromRange(startDate: Date, endDate: Date): string[] { + if (!isValidDate(dateToYYYYMMDD(startDate)) || !isValidDate(dateToYYYYMMDD(endDate))) { + throw new Error('Either the start date or end date is an invalid date.'); + } + + const dateDiff = Math.floor( + (Date.parse(endDate.toString()) - Date.parse(startDate.toString())) / 86400000 + ); + + if (dateDiff < 0) { + throw new Error('The start date is more recent than the end date.'); + } + + const endDateStr: string = dateToYYYYMMDD(endDate); + const allDate: string[] = []; + let currentYear: number = startDate.getFullYear(); + let currentMonth: number = startDate.getMonth() + 1; + let currentDay: number = startDate.getDate(); + let currentDateStr = ''; + + const createNewDateStr = (year: number, month: number, day: number): string => + `${year}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; + + while (endDateStr !== currentDateStr) { + if (/[0-9]{4}-12-31/g.test(currentDateStr)) { + currentYear += 1; + currentMonth = 1; + currentDay = 1; + } + + const currentNewDateStr = createNewDateStr(currentYear, currentMonth, currentDay); + + if (isValidDate(currentNewDateStr)) { + currentDay += 1; + allDate.push(currentNewDateStr); + currentDateStr = currentNewDateStr; + } else { + currentMonth += 1; + currentDay = 1; + currentDateStr = createNewDateStr(currentYear, currentMonth, currentDay); + } + } + + return allDate; +} diff --git a/lib/format.ts b/lib/format.ts new file mode 100644 index 0000000..2d75047 --- /dev/null +++ b/lib/format.ts @@ -0,0 +1,118 @@ +import { basename, extname, win32 } from 'path'; +import type { AnyValueObject, DurationOptions, PositiveNumber } from './types/global'; + +export function numberFormat(number: number | string): string { + return new Intl.NumberFormat().format(number as number); +} + +export function fileName(filePath: string, withExtension = false): string { + if (!filePath) { + return ''; + } + + if (filePath.indexOf('/') === -1) { + // Windows path + if (withExtension) { + return win32.basename(filePath); + } + + return win32.basename(filePath, extname(filePath)); + } + + if (withExtension) { + return basename(filePath); + } + + return basename(filePath, extname(filePath)); +} + +export function fileSize(bytes: PositiveNumber, decimals = 2): string { + if (!bytes || bytes === 0 || bytes < 0) { + return '0 Bytes'; + } + + const byteCalc = Math.floor(Math.log(bytes) / Math.log(1024)); + + return `${parseFloat((bytes / 1024 ** byteCalc).toFixed(decimals < 0 ? 0 : decimals))} ${ + ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][byteCalc] + }`; +} + +export function fileExt(filePath: string): string { + if (filePath.indexOf('.') === -1) { + return 'Unknown'; + } + + const pSpl = filePath.trim().toLowerCase().split('.'); + + return pSpl.length > 0 ? pSpl[pSpl.length - 1] : 'Unknown'; +} + +export function duration(milliseconds: number, options?: DurationOptions): string { + const { + useShortString = false, + useSpace = true, + withZeroValue = false, + separator = ' ' + } = { ...options }; + const units = [ + { name: 'Millisecond', short: 'ms', divider: 1000 }, + { name: 'Second', short: 'S', divider: 60 }, + { name: 'Minute', short: 'M', divider: 60 }, + { name: 'Hour', short: 'H', divider: 24 }, + { name: 'Day', short: 'D', divider: 31 } + ]; + const result = []; + let currentMilliseconds = milliseconds; + + for (let i = 0; i < units.length; i += 1) { + const unit = units[i]; + let divideValue = currentMilliseconds % unit.divider; + + if (i === units.length - 1) { + divideValue = Math.trunc(currentMilliseconds); + } else { + currentMilliseconds = (currentMilliseconds - divideValue) / unit.divider; + } + + if (withZeroValue || (!withZeroValue && divideValue !== 0)) { + result.push( + `${divideValue}${useSpace ? ' ' : ''}${useShortString ? unit.short : `${unit.name}${divideValue < 2 ? '' : 's'}`}` + ); + } + } + + return result.reverse().join(separator); +} + +export function safeJSONParse(jsonString: any, fallback = {}): AnyValueObject { + if (!jsonString) { + return fallback; + } + + if (Array.isArray(jsonString) || typeof jsonString === 'object') { + try { + return JSON.parse(JSON.stringify(jsonString)); + } catch (e) { + return fallback; + } + } + + try { + return JSON.parse(jsonString); + } catch (e) { + return fallback; + } +} + +export function safeParseInt(value: any, fallback = 0, radix = 10): number { + if (!value || value.toString().length < 1) { + return fallback; + } + + try { + return parseInt(value.toString().split('.')[0], radix); + } catch (e) { + return fallback; + } +} diff --git a/lib/index.ts b/lib/index.ts index 8f51380..b9e9eeb 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,1371 +1,9 @@ -import { basename, extname, win32 } from 'path'; -import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto'; - -declare type PositiveNumber = number extends N - ? N - : `${N}` extends `-${string}` - ? never - : N; - -declare type NumberValueObject = { [key: string]: number }; - -declare type AnyValueObject = { [key: string]: any }; - -declare type DurationOptions = { - useShortString?: boolean; - useSpace?: boolean; - withZeroValue?: boolean; - separator?: string; -}; - -export default class Qsu { - /* - * Misc - * */ - static sleep(delay: PositiveNumber): Promise { - return new Promise((resolve) => { - setTimeout(resolve, delay); - }); - } - - static funcTimes(times: PositiveNumber, iteratee: any): Array { - const results = []; - - for (let i = 0; i < times; i += 1) { - if (typeof iteratee === 'function') { - results[i] = iteratee.call(); - } else { - results[i] = iteratee; - } - } - - return results; - } - - static debounce( - func: (...args: any[]) => void, - timeout: PositiveNumber - ): (...args: any[]) => void { - let timer: NodeJS.Timeout; - - return (...args: any[]): void => { - clearTimeout(timer); - - timer = setTimeout(() => { - func.apply(this, args); - }, timeout); - }; - } - - /* - * Math - * */ - static numRandom(min: number, max: number): number { - if (!min && !max) { - return Math.random() > 0.5 ? 1 : 0; - } - - const limit = !max ? min : max; - const offset = !max || min >= max ? null : min; - - return Math.floor(Math.random() * (offset ? limit - offset + 1 : limit + 1)) + (offset || 0); - } - - static sum(...args: any[]): number; - - static sum(...args: Array): number; - - static sum(...args: Array | number[]): number { - const val = args.length > 0 && typeof args[0] === 'object' ? args[0] : args; - let total = 0; - - for (let i = 0, iLen = val.length; i < iLen; i += 1) { - if (typeof val[i] === 'number') { - total += val[i]; - } - } - - return total; - } - - static mul(...args: any[]): number; - - static mul(...args: Array): number; - - static mul(...args: Array | number[]): number { - const val = args.length > 0 && typeof args[0] === 'object' ? args[0] : args; - let total = val[0]; - - for (let i = 1, iLen = val.length; i < iLen; i += 1) { - if (typeof val[i] === 'number') { - total *= val[i]; - } - } - - return total; - } - - static sub(...args: any[]): number; - - static sub(...args: Array): number; - - static sub(...args: Array | number[]): number { - const val = args.length > 0 && typeof args[0] === 'object' ? args[0] : args; - let total = val[0]; - - for (let i = 1, iLen = val.length; i < iLen; i += 1) { - if (typeof val[i] === 'number') { - total -= val[i]; - } - } - - return total; - } - - static div(...args: any[]): number; - - static div(...args: Array): number; - - static div(...args: Array | number[]): number { - const val = args.length > 0 && typeof args[0] === 'object' ? args[0] : args; - let total = val[0]; - - for (let i = 1, iLen = val.length; i < iLen; i += 1) { - if (typeof val[i] === 'number') { - total /= val[i]; - } - } - - return total; - } - - /* - * Date - * */ - static dayDiff(date1: Date, date2?: Date): number { - const date2c = date2 || new Date(); - - return Math.ceil(Math.abs(date2c.getTime() - date1.getTime()) / (1000 * 3600 * 24)); - } - - static today(separator = '-', yearFirst = true): string { - const date = new Date(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - const dateArr = [`${month < 10 ? '0' : ''}${month}`, `${day < 10 ? '0' : ''}${day}`]; - - if (yearFirst) { - dateArr.unshift(date.getFullYear().toString()); - } else { - dateArr.push(date.getFullYear().toString()); - } - - return dateArr.join(separator); - } - - static isValidDate(dateYYYYMMDD: string): boolean { - if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(dateYYYYMMDD)) { - throw new Error("The date format must be 'YYYY-MM-DD'"); - } - - const convertedDate: string[] = dateYYYYMMDD.split('-'); - - return /^(?=\d)(?:(?:31(?!.(?:0?[2469]|11))|(?:30|29)(?!.0?2)|29(?=.0?2.(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:16|[2468][048]|[3579][26])00)(?:\x20|$))|(?:2[0-8]|1\d|0?[1-9]))([-./])(?:1[012]|0?[1-9])\1(?:1[6-9]|[2-9]\d)?\d\d(?:(?=\x20\d)\x20|$))?(((0?[1-9]|1[012])(:[0-5]\d){0,2}(\x20[AP]M))|([01]\d|2[0-3])(:[0-5]\d){1,2})?$/.test( - `${parseInt(convertedDate[2], 10)}-${parseInt(convertedDate[1], 10)}-${parseInt( - convertedDate[0], - 10 - )}` - ); - } - - static dateToYYYYMMDD(date: Date, separator = '-'): string { - const month: number = date.getMonth() + 1; - const day: number = date.getDate(); - - return `${date.getFullYear()}${separator}${month < 10 ? `0${month}` : month}${separator}${ - day < 10 ? `0${day}` : day - }`; - } - - static createDateListFromRange(startDate: Date, endDate: Date): string[] { - if ( - !Qsu.isValidDate(Qsu.dateToYYYYMMDD(startDate)) || - !Qsu.isValidDate(Qsu.dateToYYYYMMDD(endDate)) - ) { - throw new Error('Either the start date or end date is an invalid date.'); - } - - const dateDiff = Math.floor( - (Date.parse(endDate.toString()) - Date.parse(startDate.toString())) / 86400000 - ); - - if (dateDiff < 0) { - throw new Error('The start date is more recent than the end date.'); - } - - const endDateStr: string = Qsu.dateToYYYYMMDD(endDate); - const allDate: string[] = []; - let currentYear: number = startDate.getFullYear(); - let currentMonth: number = startDate.getMonth() + 1; - let currentDay: number = startDate.getDate(); - let currentDateStr = ''; - - const createNewDateStr = (year: number, month: number, day: number): string => - `${year}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; - - while (endDateStr !== currentDateStr) { - if (/[0-9]{4}-12-31/g.test(currentDateStr)) { - currentYear += 1; - currentMonth = 1; - currentDay = 1; - } - - const currentNewDateStr = createNewDateStr(currentYear, currentMonth, currentDay); - - if (Qsu.isValidDate(currentNewDateStr)) { - currentDay += 1; - allDate.push(currentNewDateStr); - currentDateStr = currentNewDateStr; - } else { - currentMonth += 1; - currentDay = 1; - currentDateStr = createNewDateStr(currentYear, currentMonth, currentDay); - } - } - - return allDate; - } - - /* - * Array - * */ - static arrShuffle(array: any[]): any[] { - if (array.length === 1) { - return array[0]; - } - - const newArray = array; - - for (let i = array.length - 1; i > 0; i -= 1) { - const j = Math.floor(Math.random() * (i + 1)); - [newArray[i], newArray[j]] = [array[j], array[i]]; - } - - return newArray; - } - - static arrWithDefault(defaultValue: any, length = 0): any[] { - if (length < 1) { - return []; - } - - return Array(length).fill(defaultValue); - } - - static arrUnique(array: any[]): any[] { - if (Qsu.is2dArray(array)) { - return Array.from(new Set(array.map((x) => JSON.stringify(x))), (x) => JSON.parse(x)); - } - - return [...new Set(array)]; - } - - static arrWithNumber(start: number, end: number): number[] { - if (start > end) { - throw new Error('`end` is greater than `start`.'); - } - - return Array.from({ length: end - start + 1 }, (_, i) => i + start); - } - - static average(array: number[]): number { - return array.reduce((p, c) => p + c, 0) / array.length; - } - - static arrMove( - array: any[], - from: PositiveNumber, - to: PositiveNumber - ): any[] { - const arrayLength = array.length; - - if (arrayLength <= from || arrayLength <= to) { - throw new Error('Invalid move params'); - } - - array.splice(to, 0, array.splice(from, 1)[0]); - - return array; - } - - static arrTo1dArray(array: any[]): any[] { - const convert1dArray = (arr: any[]): any[] => { - const tempArr = []; - const arrayLength = arr.length; - - for (let i = 0; i < arrayLength; i += 1) { - if (typeof arr[i] !== 'object') { - tempArr.push(arr[i]); - } else if (Qsu.is2dArray(arr[i])) { - tempArr.push(...convert1dArray(arr[i])); - } else { - tempArr.push(...arr[i]); - } - } - - return tempArr; - }; - - return convert1dArray(array); - } - - static arrRepeat(array: any, count: PositiveNumber): any[] { - if (!array || count < 1 || typeof array !== 'object') { - return []; - } - - const isObject = Qsu.isObject(array); - const result: any[] = []; - - for (let i = 0, iLen = count; i < iLen; i += 1) { - if (isObject) { - result.push(array); - } else { - result.push(...array); - } - } - - return result; - } - - static arrCount(array: string[] | number[]): NumberValueObject { - const result: NumberValueObject = {}; - - for (let i = 0; i < array.length; i += 1) { - const x = array[i]; - - result[x] = (result[x] || 0) + 1; - } - - return result; - } - - static sortByObjectKey( - array: any[], - key: string, - descending = false, - numerically = false - ): any[] { - if (numerically) { - const collator = new Intl.Collator([], { numeric: true }); - const result = array.sort((a: any, b: any) => collator.compare(a[key], b[key])); - - return descending ? result.reverse() : result; - } - - return array.sort((a: any, b: any) => { - if (!descending) { - if (a[key] < b[key]) return -1; - if (a[key] > b[key]) return 1; - - return 0; - } - - if (a[key] > b[key]) return -1; - if (a[key] < b[key]) return 1; - - return 0; - }); - } - - static sortNumeric(array: string[], descending = false): string[] { - const collator = new Intl.Collator([], { numeric: true }); - const result = array.sort((a: any, b: any) => collator.compare(a, b)); - - return descending ? result.reverse() : result; - } - - static arrGroupByMaxCount(array: any[], maxLengthPerGroup = 1): any[] { - const result = []; - const arrayLength = array.length; - let tempArray = []; - - for (let i = 0; i < arrayLength; i += 1) { - if (tempArray.length === maxLengthPerGroup) { - result.push(tempArray); - tempArray = []; - } - - tempArray.push(array[i]); - } - - if (tempArray.length > 0) { - result.push(tempArray); - } - - return result; - } - - /* - * Object - * */ - static objToQueryString(obj: AnyValueObject): string { - return Object.keys(obj) - .map((key) => { - let value = obj[key]; - - if (typeof value === 'object') { - value = JSON.stringify(value); - } - - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - }) - .join('&'); - } - - static objFindItemRecursiveByKey( - obj: AnyValueObject | any[], - searchKey: string, - searchValue: any, - childKey: string - ): AnyValueObject | null { - const findItemFromList = (lists: AnyValueObject): any | null => { - let searchArray: any[]; - - if (typeof lists !== 'object' || !Array.isArray(lists)) { - searchArray = [lists]; - } else { - searchArray = lists; - } - - for (let i = 0, iLen = searchArray.length; i < iLen; i += 1) { - if (searchArray[i][searchKey] === searchValue) { - return searchArray[i]; - } - if (!Qsu.isEmpty(searchArray[i][childKey])) { - const childItem = findItemFromList(searchArray[i][childKey]); - - if (childItem) { - return childItem; - } - } - } - return null; - }; - - return findItemFromList(obj); - } - - static objToPrettyStr(obj: AnyValueObject): string { - return JSON.stringify(obj, null, '\t'); - } - - static objToArray(obj: AnyValueObject, recursive = false): any[] { - const convertToArray = (o: AnyValueObject): any[] => { - const r = []; - const oLen = Object.keys(o).length; - - for (let i = 0; i < oLen; i += 1) { - const key = Object.keys(o)[i]; - - if (recursive && Qsu.isObject(o[key])) { - r.push([key, convertToArray(o[key])]); - } else { - r.push([key, o[key]]); - } - } - - return r; - }; - - return convertToArray(obj); - } - - static objTo1d(obj: AnyValueObject, separator = '.'): AnyValueObject { - if (!separator || separator.length < 1) { - throw new Error('`separator` must have value at least 1 character.'); - } - - const convertObjectTo1d = (o: AnyValueObject, objPath = ''): AnyValueObject => { - let result: AnyValueObject = {}; - const objectLength = Object.keys(o).length; - const isFirstDepth = objPath.length < 1; - - for (let i = 0; i < objectLength; i += 1) { - const key = Object.keys(o)[i]; - const value = o[key]; - const newObjPath = `${objPath}${isFirstDepth ? '' : separator}${key}`; - - if (Qsu.isObject(value)) { - result = Object.assign(result, convertObjectTo1d(value, newObjPath)); - delete result[key]; - } else { - result[newObjPath] = value; - } - } - - return result; - }; - - return convertObjectTo1d(obj); - } - - static objDeleteKeyByValue( - obj: AnyValueObject, - searchValue: string | number | null | undefined, - recursive = false - ): AnyValueObject | null { - if (!obj || typeof obj !== 'object') { - return null; - } - - const newObj = Object.assign(obj, {}); - - for (let i = Object.keys(newObj).length; i >= 0; i -= 1) { - const key = Object.keys(newObj)[i]; - - if (recursive && newObj[key] && Qsu.isObject(newObj[key])) { - Qsu.objDeleteKeyByValue(newObj[key], searchValue, recursive); - } else if (newObj[key] === searchValue) { - delete newObj[key]; - } - } - - return newObj; - } - - static objUpdate( - obj: AnyValueObject, - key: string, - value: any, - recursive = false, - upsert = false - ): AnyValueObject | null { - if (!obj || typeof obj !== 'object') { - return null; - } - - const newObj = Object.assign(obj, {}); - let hasUpdated = false; - - const checkObjectKey = (currentObj: AnyValueObject): void => { - for (let i = 0; i < Object.keys(currentObj).length; i += 1) { - const currentKey = Object.keys(currentObj)[i]; - - if (recursive && currentObj[currentKey] && Qsu.isObject(currentObj[currentKey])) { - checkObjectKey(currentObj[currentKey]); - } - - if (Object.hasOwn(currentObj, key)) { - // eslint-disable-next-line no-param-reassign - currentObj[key] = value; - hasUpdated = true; - } - } - }; - - checkObjectKey(newObj); - - if (!hasUpdated && upsert) { - newObj[key] = value; - } - - return newObj; - } - - static objMergeNewKey(obj: AnyValueObject, obj2: AnyValueObject): AnyValueObject | null { - if (!obj || typeof obj !== 'object' || !obj2 || typeof obj2 !== 'object') { - return null; - } - - const merged: AnyValueObject = { ...obj }; - - Object.keys(obj2).forEach((key: string) => { - const data = obj2[key]; - - if (Object.hasOwn(merged, key)) { - if (Array.isArray(merged[key]) && Array.isArray(data)) { - if (merged[key].length === data.length) { - for (let i = 0; i < merged[key].length; i += 1) { - const update = data[i]; - - if (Qsu.isObject(update)) { - merged[key][i] = Qsu.objMergeNewKey(merged[key][i], update); - } - } - } - } else if (Qsu.isObject(merged[key]) && Qsu.isObject(data)) { - merged[key] = Qsu.objMergeNewKey(merged[key], data); - } else { - merged[key] = data; - } - } else { - merged[key] = data; - } - }); - - return merged; - } - - /* - * String - * */ - static trim(str?: string | null): string | null { - if (typeof str !== 'string' && !str) { - return null; - } - - return str.trim().replace(/\s{2,}/g, ' '); - } - - static removeSpecialChar(str: string, exceptionCharacters?: string): string { - if (!str) { - return ''; - } - - return str.replace( - new RegExp( - `[^a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ0-9\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f${ - exceptionCharacters ?? '' - }]`, - 'gi' - ), - '' - ); - } - - static replaceBetween(str: string, startChar: string, endChar: string, replaceWith = ''): string { - if (!str) { - return ''; - } - - const specialCharacters = /[.*+?^${}()|[\]\\]/g; - const startCharRegExp = specialCharacters.test(startChar) ? `\\${startChar}` : startChar; - const endCharRegExp = specialCharacters.test(endChar) ? `\\${endChar}` : endChar; - - return str.replace(new RegExp(`${startCharRegExp}.*?${endCharRegExp}`, 'g'), replaceWith); - } - - static removeNewLine(str: string, replaceTo = ''): string { - if (!str) { - return ''; - } - - return str.replace(/(\r\n|\n|\r)/gm, replaceTo).trim(); - } - - static capitalizeFirst(str: string): string { - if (!str) { - return ''; - } - - return str.charAt(0).toUpperCase() + str.slice(1); - } - - static capitalizeEverySentence(str: string, splitChar?: string): string { - if (!str) { - return ''; - } - - const splitter: string = splitChar || '.'; - const splitStr = str.split(splitter); - let resultStr = ''; - let sentenceChars; - - for (let i = 0, iLen = splitStr.length; i < iLen; i += 1) { - sentenceChars = [...splitStr[i]]; - - for (let j = 0, jLen = sentenceChars.length; j < jLen; j += 1) { - if (/[a-zA-Z]/.test(splitStr[i][j])) { - sentenceChars[j] = splitStr[i][j].toUpperCase(); - break; - } - } - - resultStr += `${sentenceChars.join('')}${i < iLen - 1 ? splitter : ''}`; - } - - return resultStr; - } - - static capitalizeEachWords(str: string, natural?: boolean): string { - if (!str) { - return ''; - } - - const splitStr = str.trim().toLowerCase().split(' '); - - for (let i = 0, iLen = splitStr.length; i < iLen; i += 1) { - if ( - !natural || - !Qsu.contains( - splitStr[i], - [ - 'in', - 'on', - 'the', - 'at', - 'and', - 'or', - 'of', - 'for', - 'to', - 'that', - 'a', - 'by', - 'it', - 'is', - 'as', - 'are', - 'were', - 'was', - 'nor', - 'an' - ], - true - ) - ) { - splitStr[i] = Qsu.capitalizeFirst(splitStr[i]); - } - } - - return Qsu.capitalizeFirst(splitStr.join(' ')); - } - - static strCount(str: string, search: string): number { - if (!str || !search) { - return 0; - } - - let count = 0; - let pos = str.indexOf(search); - - while (pos > -1) { - count += 1; - pos = str.indexOf(search, (pos += search.length)); - } - - return count; - } - - static strShuffle(str: string): string { - if (!str) { - return ''; - } - - return [...str].sort(() => Math.random() - 0.5).join(''); - } - - static strRandom( - length: PositiveNumber, - additionalCharacters?: string - ): string { - const availCharacters = `abcdefghijklmnopqrstuvwxyz0123456789${additionalCharacters}`; - const availCharacterLength = availCharacters.length; - let result = ''; - let newChar; - - for (let i = 0; i < length; i += 1) { - newChar = availCharacters.charAt(Math.floor(Math.random() * availCharacterLength)); - newChar = Math.random() < 0.5 ? newChar.toUpperCase() : newChar; - result += newChar; - } - - return result; - } - - static strBlindRandom( - str: string, - blindLength: PositiveNumber, - blindStr = '*' - ): string { - if (!str) { - return ''; - } - - let currentStr = str; - let hideCount = 0; - let tempIdx = 0; - let currentStrLength = 0; - - const totalStrLength = currentStr.length; - - while (hideCount < blindLength && currentStrLength < totalStrLength) { - tempIdx = Qsu.numRandom(0, totalStrLength); - - if (/[a-zA-Z가-힣]/.test(currentStr.substring(tempIdx, tempIdx + 1))) { - currentStr = `${currentStr.substring(0, tempIdx + 1)}${blindStr}${currentStr.substring( - tempIdx + 2 - )}`; - hideCount += 1; - } - - currentStrLength += 1; - } - - return currentStr; - } - - static truncate(str: string, length: PositiveNumber, ellipsis = ''): string { - if (!str) { - return ''; - } - - let convStr = str; - - if (str.length > length) { - convStr = str.substring(0, length) + ellipsis; - } - - return convStr; - } - - static truncateExpect( - str: string, - expectLength: PositiveNumber, - endStringChar = '.' - ): string { - if (!str) { - return ''; - } - - const isEndStringCharLastSentence = str.slice(-1) === endStringChar; - const splitStr = str.split(endStringChar); - const splitStrLength = splitStr.length; - let convStr = ''; - let currentLength = 0; - - for (let i = 0; i < splitStrLength; i += 1) { - if (currentLength < expectLength) { - convStr += `${splitStr[i]}${ - i !== splitStrLength - 1 || isEndStringCharLastSentence ? endStringChar : '' - }`; - currentLength += splitStr[i].length + endStringChar.length; - } else { - break; - } - } - - return convStr; - } - - static split(str: string, ...splitter: any[]): string[]; - - static split(str: string, ...splitter: Array): string[]; - - static split(str: string, ...splitter: Array | string[]): string[] { - if (!str) { - return []; - } - - const splitters = - splitter.length > 0 && typeof splitter[0] === 'object' ? splitter[0] : splitter; - const splitterLength: number = splitters.length; - let charPattern = ''; - let strPattern = ''; - - for (let i = 0; i < splitterLength; i += 1) { - const spl = splitters[i]; - - if (spl.length > 1) { - strPattern += `${strPattern.length < 1 ? '' : '|'}${spl - .replace(/\\/g, '\\\\') - .replace(/\[/g, '\\[') - .replace(/]/g, '\\]') - .replace(/\?/g, '\\?') - .replace(/\./g, '\\.') - .replace(/\{/g, '\\{') - .replace(/}/g, '\\}') - .replace(/\+/g, '\\+')}`; - } else if (spl === '-' || spl === '[' || spl === ']') { - charPattern += `\\${spl}`; - } else { - charPattern += spl; - } - } - - if (charPattern.length < 1 && strPattern.length < 1) { - return [str]; - } - - if (charPattern.length > 0) { - charPattern = `[${charPattern}]`; - if (strPattern.length > 0) { - strPattern = `|${strPattern}`; - } - } - - return str.split(new RegExp(`${charPattern}${strPattern}+`, 'gi')); - } - - static strToAscii(str: string): number[] { - const arr = []; - for (let i = 0; i < str.length; i += 1) { - arr.push(str.charCodeAt(i)); - } - return arr; - } - - static strUnique(str: string): string { - if (!str) { - return ''; - } - - return [...new Set(str)].join(''); - } - - static urlJoin(...args: any[]): string { - if (!args) { - return ''; - } - - const argLength = args.length; - let urlResult = ''; - let joinCount = 0; - - for (let i = 0; i < argLength; i += 1) { - if (args[i] !== null && args[i] !== undefined) { - if ( - joinCount === 0 || - args[i].startsWith('/') || - args[i].startsWith('?') || - args[i].startsWith('&') - ) { - urlResult += args[i]; - } else { - urlResult += `/${args[i]}`; - } - - joinCount += 1; - } - } - - return urlResult.replace(/\/$/g, ''); - } - - /* - * Verify - * */ - static isObject(data: any): boolean { - return data !== null && data !== undefined && Object.getPrototypeOf(data) === Object.prototype; - } - - static isEqual(leftOperand: any, ...rightOperand: Array): boolean { - const rightOperands = - rightOperand.length > 0 && typeof rightOperand[0] === 'object' - ? rightOperand[0] - : rightOperand; - const rightOperandLength: number = rightOperands.length; - - for (let i = 0; i < rightOperandLength; i += 1) { - // eslint-disable-next-line eqeqeq - if (rightOperands[i] != leftOperand) { - return false; - } - } - - return true; - } - - static isEqualStrict(leftOperand: any, ...rightOperand: Array): boolean { - const rightOperands = - rightOperand.length > 0 && typeof rightOperand[0] === 'object' - ? rightOperand[0] - : rightOperand; - const rightOperandLength: number = rightOperands.length; - - for (let i = 0; i < rightOperandLength; i += 1) { - if (rightOperands[i] !== leftOperand) { - return false; - } - } - - return true; - } - - static isEmpty(data?: any): boolean { - if (!data) { - return true; - } - - switch (typeof data) { - case 'string': - return data.length < 1; - case 'object': - if (Array.isArray(data)) { - return data.length < 1; - } - return Object.keys(data).length < 1; - default: - return false; - } - } - - static isUrl(url: string, withProtocol = false, strict = false): boolean { - if (strict && url.indexOf('.') === -1) { - return false; - } - - try { - new URL(`${withProtocol && url.indexOf('://') === -1 ? 'https://' : ''}${url}`).toString(); - } catch (e) { - return false; - } - - return true; - } - - static contains(str: any[] | string, search: any[] | string, exact = false): boolean { - if (typeof search === 'string') { - return str.length < 1 ? false : str.indexOf(search) !== -1; - } - - for (let i = 0, iLen = search.length; i < iLen; i += 1) { - if (exact) { - if (str === search[i]) { - return true; - } - } else if (str.indexOf(search[i]) !== -1) { - return true; - } - } - - return false; - } - - static is2dArray(array: any[]): boolean { - return array.filter(Array.isArray).length > 0; - } - - static between(range: [number, number], number: number, inclusive = false): boolean { - const minM = Math.min.apply(Math, [range[0], range[1]]); - const maxM = Math.max.apply(Math, [range[0], range[1]]); - - return inclusive ? number >= minM && number <= maxM : number > minM && number < maxM; - } - - static len(data: any): number { - if (!data) { - return 0; - } - switch (typeof data) { - case 'object': - return Array.isArray(data) ? data.length : Object.keys(data).length; - case 'number': - case 'bigint': - return data.toString().length; - case 'boolean': - return data ? 4 : 5; - case 'function': - return data().length; - case 'string': - default: - return data.length; - } - } - - static isEmail(email: string): boolean { - // RFC2822 Email Validation - return /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test( - email - ); - } - - static isTrueMinimumNumberOfTimes(conditions: boolean[], minimumCount = 1): boolean { - const conditionLength = conditions.length; - let trueCount = 0; - - for (let i = 0; i < conditionLength; i += 1) { - if (typeof conditions[i] === 'boolean' && conditions[i]) { - trueCount += 1; - } - } - - return trueCount >= minimumCount; - } - - /* - * Format - * */ - static numberFormat(number: number | string): string { - return new Intl.NumberFormat().format(number as number); - } - - static fileName(filePath: string, withExtension = false): string { - if (!filePath) { - return ''; - } - - if (filePath.indexOf('/') === -1) { - // Windows path - if (withExtension) { - return win32.basename(filePath); - } - - return win32.basename(filePath, extname(filePath)); - } - - if (withExtension) { - return basename(filePath); - } - - return basename(filePath, extname(filePath)); - } - - static fileSize(bytes: PositiveNumber, decimals = 2): string { - if (!bytes || bytes === 0 || bytes < 0) { - return '0 Bytes'; - } - - const byteCalc = Math.floor(Math.log(bytes) / Math.log(1024)); - - return `${parseFloat((bytes / 1024 ** byteCalc).toFixed(decimals < 0 ? 0 : decimals))} ${ - ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][byteCalc] - }`; - } - - static fileExt(filePath: string): string { - if (filePath.indexOf('.') === -1) { - return 'Unknown'; - } - - const pSpl = filePath.trim().toLowerCase().split('.'); - - return pSpl.length > 0 ? pSpl[pSpl.length - 1] : 'Unknown'; - } - - static duration(milliseconds: number, options?: DurationOptions): string { - const { - useShortString = false, - useSpace = true, - withZeroValue = false, - separator = ' ' - } = { ...options }; - const units = [ - { name: 'Millisecond', short: 'ms', divider: 1000 }, - { name: 'Second', short: 'S', divider: 60 }, - { name: 'Minute', short: 'M', divider: 60 }, - { name: 'Hour', short: 'H', divider: 24 }, - { name: 'Day', short: 'D', divider: 31 } - ]; - const result = []; - let currentMilliseconds = milliseconds; - - for (let i = 0; i < units.length; i += 1) { - const unit = units[i]; - let divideValue = currentMilliseconds % unit.divider; - - if (i === units.length - 1) { - divideValue = Math.trunc(currentMilliseconds); - } else { - currentMilliseconds = (currentMilliseconds - divideValue) / unit.divider; - } - - if (withZeroValue || (!withZeroValue && divideValue !== 0)) { - result.push( - `${divideValue}${useSpace ? ' ' : ''}${useShortString ? unit.short : `${unit.name}${divideValue < 2 ? '' : 's'}`}` - ); - } - } - - return result.reverse().join(separator); - } - - static safeJSONParse(jsonString: any, fallback = {}): AnyValueObject { - if (!jsonString) { - return fallback; - } - - if (Array.isArray(jsonString) || typeof jsonString === 'object') { - try { - return JSON.parse(JSON.stringify(jsonString)); - } catch (e) { - return fallback; - } - } - - try { - return JSON.parse(jsonString); - } catch (e) { - return fallback; - } - } - - static safeParseInt(value: any, fallback = 0, radix = 10): number { - if (!value || value.toString().length < 1) { - return fallback; - } - - try { - return parseInt(value.toString().split('.')[0], radix); - } catch (e) { - return fallback; - } - } - - /* - * Crypto - * */ - - static encrypt( - str: string, - secret: string, - algorithm = 'aes-256-cbc', - ivSize = 16, - toBase64 = false - ): string { - if (!str || str.length < 1) { - return ''; - } - - const iv: Buffer = randomBytes(ivSize); - const cipher = createCipheriv(algorithm, secret, iv); - let enc = cipher.update(str); - - enc = Buffer.concat([enc, cipher.final()]); - - const encoding: BufferEncoding = toBase64 ? 'base64' : 'hex'; - - return `${iv.toString(encoding)}:${enc.toString(encoding)}`; - } - - static decrypt(str: string, secret: string, algorithm = 'aes-256-cbc', toBase64 = false): string { - if (!str || str.length < 1) { - return ''; - } - - const encoding: BufferEncoding = toBase64 ? 'base64' : 'hex'; - const arrStr: any[] = str.split(':'); - const decipher = createDecipheriv(algorithm, secret, Buffer.from(arrStr.shift(), encoding)); - let decrypted = decipher.update(Buffer.from(arrStr.join(':'), encoding)); - - decrypted = Buffer.concat([decrypted, decipher.final()]); - - return decrypted.toString(); - } - - static objectId(): string { - return ( - Math.floor(Date.now() / 1000).toString(16) + - 'x'.repeat(16).replace(/x/g, () => Math.floor(Math.random() * 16).toString(16)) - ); - } - - static md5Hash(str: string): string { - return createHash('md5').update(str).digest('hex'); - } - - static sha1Hash(str: string): string { - return createHash('sha1').update(str).digest('hex'); - } - - static sha256Hash(str: string): string { - return createHash('sha256').update(str).digest('hex'); - } - - static encodeBase64(str: string): string { - return Buffer.from(str, 'utf8').toString('base64'); - } - - static decodeBase64(encodedStr: string): string { - return Buffer.from(encodedStr, 'base64').toString('utf8'); - } - - static strToNumberHash(str: string): number { - if (!str) { - return 0; - } - - let hash = 0; - - for (let i = 0; i < str.length; i += 1) { - hash = (hash << 5) - hash + str.charCodeAt(i); - hash |= 0; - } - - return hash; - } -} - -export { Qsu }; - -export const { - sleep, - funcTimes, - debounce, - objectId, - numRandom, - sum, - mul, - sub, - div, - dayDiff, - today, - isValidDate, - dateToYYYYMMDD, - createDateListFromRange, - arrShuffle, - arrWithDefault, - arrUnique, - arrWithNumber, - arrRepeat, - arrCount, - average, - arrMove, - arrTo1dArray, - sortByObjectKey, - sortNumeric, - arrGroupByMaxCount, - objToQueryString, - objToPrettyStr, - objFindItemRecursiveByKey, - objToArray, - objTo1d, - objDeleteKeyByValue, - objUpdate, - objMergeNewKey, - trim, - replaceBetween, - removeSpecialChar, - removeNewLine, - capitalizeFirst, - capitalizeEverySentence, - capitalizeEachWords, - strCount, - strShuffle, - strRandom, - strBlindRandom, - truncate, - truncateExpect, - split, - encrypt, - decrypt, - md5Hash, - sha1Hash, - sha256Hash, - encodeBase64, - decodeBase64, - strToNumberHash, - strUnique, - strToAscii, - urlJoin, - isObject, - isEqual, - isEqualStrict, - isEmpty, - isUrl, - contains, - is2dArray, - between, - len, - isEmail, - isTrueMinimumNumberOfTimes, - numberFormat, - fileName, - fileSize, - fileExt, - safeJSONParse, - safeParseInt, - duration -} = Qsu; +export * from './array'; +export * from './crypto'; +export * from './date'; +export * from './format'; +export * from './math'; +export * from './misc'; +export * from './object'; +export * from './string'; +export * from './verify'; diff --git a/lib/math.ts b/lib/math.ts new file mode 100644 index 0000000..ae5e219 --- /dev/null +++ b/lib/math.ts @@ -0,0 +1,78 @@ +export function numRandom(min: number, max: number): number { + if (!min && !max) { + return Math.random() > 0.5 ? 1 : 0; + } + + const limit = !max ? min : max; + const offset = !max || min >= max ? null : min; + + return Math.floor(Math.random() * (offset ? limit - offset + 1 : limit + 1)) + (offset || 0); +} + +export function sum(...args: any[]): number; + +export function sum(...args: Array): number; + +export function sum(...args: Array | number[]): number { + const val = args.length > 0 && typeof args[0] === 'object' ? args[0] : args; + let total = 0; + + for (let i = 0, iLen = val.length; i < iLen; i += 1) { + if (typeof val[i] === 'number') { + total += val[i]; + } + } + + return total; +} + +export function mul(...args: any[]): number; + +export function mul(...args: Array): number; + +export function mul(...args: Array | number[]): number { + const val = args.length > 0 && typeof args[0] === 'object' ? args[0] : args; + let total = val[0]; + + for (let i = 1, iLen = val.length; i < iLen; i += 1) { + if (typeof val[i] === 'number') { + total *= val[i]; + } + } + + return total; +} + +export function sub(...args: any[]): number; + +export function sub(...args: Array): number; + +export function sub(...args: Array | number[]): number { + const val = args.length > 0 && typeof args[0] === 'object' ? args[0] : args; + let total = val[0]; + + for (let i = 1, iLen = val.length; i < iLen; i += 1) { + if (typeof val[i] === 'number') { + total -= val[i]; + } + } + + return total; +} + +export function div(...args: any[]): number; + +export function div(...args: Array): number; + +export function div(...args: Array | number[]): number { + const val = args.length > 0 && typeof args[0] === 'object' ? args[0] : args; + let total = val[0]; + + for (let i = 1, iLen = val.length; i < iLen; i += 1) { + if (typeof val[i] === 'number') { + total /= val[i]; + } + } + + return total; +} diff --git a/lib/misc.ts b/lib/misc.ts new file mode 100644 index 0000000..1d1fa18 --- /dev/null +++ b/lib/misc.ts @@ -0,0 +1,36 @@ +import type { PositiveNumber } from './types/global'; + +export function sleep(delay: PositiveNumber): Promise { + return new Promise((resolve) => { + setTimeout(resolve, delay); + }); +} + +export function funcTimes(times: PositiveNumber, iteratee: any): Array { + const results = []; + + for (let i = 0; i < times; i += 1) { + if (typeof iteratee === 'function') { + results[i] = iteratee.call(); + } else { + results[i] = iteratee; + } + } + + return results; +} + +export function debounce( + func: (...args: any[]) => void, + timeout: PositiveNumber +): (...args: any[]) => void { + let timer: NodeJS.Timeout; + + return (...args: any[]): void => { + clearTimeout(timer); + + timer = setTimeout(() => { + func.apply(args); + }, timeout); + }; +} diff --git a/lib/object.ts b/lib/object.ts new file mode 100644 index 0000000..779061e --- /dev/null +++ b/lib/object.ts @@ -0,0 +1,200 @@ +import type { AnyValueObject } from './types/global'; +import { isEmpty, isObject } from './verify'; + +export function objToQueryString(obj: AnyValueObject): string { + return Object.keys(obj) + .map((key) => { + let value = obj[key]; + + if (typeof value === 'object') { + value = JSON.stringify(value); + } + + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + }) + .join('&'); +} + +export function objFindItemRecursiveByKey( + obj: AnyValueObject | any[], + searchKey: string, + searchValue: any, + childKey: string +): AnyValueObject | null { + const findItemFromList = (lists: AnyValueObject): any | null => { + let searchArray: any[]; + + if (typeof lists !== 'object' || !Array.isArray(lists)) { + searchArray = [lists]; + } else { + searchArray = lists; + } + + for (let i = 0, iLen = searchArray.length; i < iLen; i += 1) { + if (searchArray[i][searchKey] === searchValue) { + return searchArray[i]; + } + if (!isEmpty(searchArray[i][childKey])) { + const childItem = findItemFromList(searchArray[i][childKey]); + + if (childItem) { + return childItem; + } + } + } + return null; + }; + + return findItemFromList(obj); +} + +export function objToPrettyStr(obj: AnyValueObject): string { + return JSON.stringify(obj, null, '\t'); +} + +export function objToArray(obj: AnyValueObject, recursive = false): any[] { + const convertToArray = (o: AnyValueObject): any[] => { + const r = []; + const oLen = Object.keys(o).length; + + for (let i = 0; i < oLen; i += 1) { + const key = Object.keys(o)[i]; + + if (recursive && isObject(o[key])) { + r.push([key, convertToArray(o[key])]); + } else { + r.push([key, o[key]]); + } + } + + return r; + }; + + return convertToArray(obj); +} + +export function objTo1d(obj: AnyValueObject, separator = '.'): AnyValueObject { + if (!separator || separator.length < 1) { + throw new Error('`separator` must have value at least 1 character.'); + } + + const convertObjectTo1d = (o: AnyValueObject, objPath = ''): AnyValueObject => { + let result: AnyValueObject = {}; + const objectLength = Object.keys(o).length; + const isFirstDepth = objPath.length < 1; + + for (let i = 0; i < objectLength; i += 1) { + const key = Object.keys(o)[i]; + const value = o[key]; + const newObjPath = `${objPath}${isFirstDepth ? '' : separator}${key}`; + + if (isObject(value)) { + result = Object.assign(result, convertObjectTo1d(value, newObjPath)); + delete result[key]; + } else { + result[newObjPath] = value; + } + } + + return result; + }; + + return convertObjectTo1d(obj); +} + +export function objDeleteKeyByValue( + obj: AnyValueObject, + searchValue: string | number | null | undefined, + recursive = false +): AnyValueObject | null { + if (!obj || typeof obj !== 'object') { + return null; + } + + const newObj = Object.assign(obj, {}); + + for (let i = Object.keys(newObj).length; i >= 0; i -= 1) { + const key = Object.keys(newObj)[i]; + + if (recursive && newObj[key] && isObject(newObj[key])) { + objDeleteKeyByValue(newObj[key], searchValue, recursive); + } else if (newObj[key] === searchValue) { + delete newObj[key]; + } + } + + return newObj; +} + +export function objUpdate( + obj: AnyValueObject, + key: string, + value: any, + recursive = false, + upsert = false +): AnyValueObject | null { + if (!obj || typeof obj !== 'object') { + return null; + } + + const newObj = Object.assign(obj, {}); + let hasUpdated = false; + + const checkObjectKey = (currentObj: AnyValueObject): void => { + for (let i = 0; i < Object.keys(currentObj).length; i += 1) { + const currentKey = Object.keys(currentObj)[i]; + + if (recursive && currentObj[currentKey] && isObject(currentObj[currentKey])) { + checkObjectKey(currentObj[currentKey]); + } + + if (Object.hasOwn(currentObj, key)) { + // eslint-disable-next-line no-param-reassign + currentObj[key] = value; + hasUpdated = true; + } + } + }; + + checkObjectKey(newObj); + + if (!hasUpdated && upsert) { + newObj[key] = value; + } + + return newObj; +} + +export function objMergeNewKey(obj: AnyValueObject, obj2: AnyValueObject): AnyValueObject | null { + if (!obj || typeof obj !== 'object' || !obj2 || typeof obj2 !== 'object') { + return null; + } + + const merged: AnyValueObject = { ...obj }; + + Object.keys(obj2).forEach((key: string) => { + const data = obj2[key]; + + if (Object.hasOwn(merged, key)) { + if (Array.isArray(merged[key]) && Array.isArray(data)) { + if (merged[key].length === data.length) { + for (let i = 0; i < merged[key].length; i += 1) { + const update = data[i]; + + if (isObject(update)) { + merged[key][i] = objMergeNewKey(merged[key][i], update); + } + } + } + } else if (isObject(merged[key]) && isObject(data)) { + merged[key] = objMergeNewKey(merged[key], data); + } else { + merged[key] = data; + } + } else { + merged[key] = data; + } + }); + + return merged; +} diff --git a/lib/string.ts b/lib/string.ts new file mode 100644 index 0000000..c892f5e --- /dev/null +++ b/lib/string.ts @@ -0,0 +1,344 @@ +import type { PositiveNumber } from './types/global'; +import { contains } from './verify'; +import { numRandom } from './math'; + +export function trim(str?: string | null): string | null { + if (typeof str !== 'string' && !str) { + return null; + } + + return str.trim().replace(/\s{2,}/g, ' '); +} + +export function removeSpecialChar(str: string, exceptionCharacters?: string): string { + if (!str) { + return ''; + } + + return str.replace( + new RegExp( + `[^a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ0-9\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f${ + exceptionCharacters ?? '' + }]`, + 'gi' + ), + '' + ); +} + +export function replaceBetween( + str: string, + startChar: string, + endChar: string, + replaceWith = '' +): string { + if (!str) { + return ''; + } + + const specialCharacters = /[.*+?^${}()|[\]\\]/g; + const startCharRegExp = specialCharacters.test(startChar) ? `\\${startChar}` : startChar; + const endCharRegExp = specialCharacters.test(endChar) ? `\\${endChar}` : endChar; + + return str.replace(new RegExp(`${startCharRegExp}.*?${endCharRegExp}`, 'g'), replaceWith); +} + +export function removeNewLine(str: string, replaceTo = ''): string { + if (!str) { + return ''; + } + + return str.replace(/(\r\n|\n|\r)/gm, replaceTo).trim(); +} + +export function capitalizeFirst(str: string): string { + if (!str) { + return ''; + } + + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function capitalizeEverySentence(str: string, splitChar?: string): string { + if (!str) { + return ''; + } + + const splitter: string = splitChar || '.'; + const splitStr = str.split(splitter); + let resultStr = ''; + let sentenceChars; + + for (let i = 0, iLen = splitStr.length; i < iLen; i += 1) { + sentenceChars = [...splitStr[i]]; + + for (let j = 0, jLen = sentenceChars.length; j < jLen; j += 1) { + if (/[a-zA-Z]/.test(splitStr[i][j])) { + sentenceChars[j] = splitStr[i][j].toUpperCase(); + break; + } + } + + resultStr += `${sentenceChars.join('')}${i < iLen - 1 ? splitter : ''}`; + } + + return resultStr; +} + +export function capitalizeEachWords(str: string, natural?: boolean): string { + if (!str) { + return ''; + } + + const splitStr = str.trim().toLowerCase().split(' '); + + for (let i = 0, iLen = splitStr.length; i < iLen; i += 1) { + if ( + !natural || + !contains( + splitStr[i], + [ + 'in', + 'on', + 'the', + 'at', + 'and', + 'or', + 'of', + 'for', + 'to', + 'that', + 'a', + 'by', + 'it', + 'is', + 'as', + 'are', + 'were', + 'was', + 'nor', + 'an' + ], + true + ) + ) { + splitStr[i] = capitalizeFirst(splitStr[i]); + } + } + + return capitalizeFirst(splitStr.join(' ')); +} + +export function strCount(str: string, search: string): number { + if (!str || !search) { + return 0; + } + + let count = 0; + let pos = str.indexOf(search); + + while (pos > -1) { + count += 1; + pos = str.indexOf(search, (pos += search.length)); + } + + return count; +} + +export function strShuffle(str: string): string { + if (!str) { + return ''; + } + + return [...str].sort(() => Math.random() - 0.5).join(''); +} + +export function strRandom( + length: PositiveNumber, + additionalCharacters?: string +): string { + const availCharacters = `abcdefghijklmnopqrstuvwxyz0123456789${additionalCharacters}`; + const availCharacterLength = availCharacters.length; + let result = ''; + let newChar; + + for (let i = 0; i < length; i += 1) { + newChar = availCharacters.charAt(Math.floor(Math.random() * availCharacterLength)); + newChar = Math.random() < 0.5 ? newChar.toUpperCase() : newChar; + result += newChar; + } + + return result; +} + +export function strBlindRandom( + str: string, + blindLength: PositiveNumber, + blindStr = '*' +): string { + if (!str) { + return ''; + } + + let currentStr = str; + let hideCount = 0; + let tempIdx = 0; + let currentStrLength = 0; + + const totalStrLength = currentStr.length; + + while (hideCount < blindLength && currentStrLength < totalStrLength) { + tempIdx = numRandom(0, totalStrLength); + + if (/[a-zA-Z가-힣]/.test(currentStr.substring(tempIdx, tempIdx + 1))) { + currentStr = `${currentStr.substring(0, tempIdx + 1)}${blindStr}${currentStr.substring( + tempIdx + 2 + )}`; + hideCount += 1; + } + + currentStrLength += 1; + } + + return currentStr; +} + +export function truncate( + str: string, + length: PositiveNumber, + ellipsis = '' +): string { + if (!str) { + return ''; + } + + let convStr = str; + + if (str.length > length) { + convStr = str.substring(0, length) + ellipsis; + } + + return convStr; +} + +export function truncateExpect( + str: string, + expectLength: PositiveNumber, + endStringChar = '.' +): string { + if (!str) { + return ''; + } + + const isEndStringCharLastSentence = str.slice(-1) === endStringChar; + const splitStr = str.split(endStringChar); + const splitStrLength = splitStr.length; + let convStr = ''; + let currentLength = 0; + + for (let i = 0; i < splitStrLength; i += 1) { + if (currentLength < expectLength) { + convStr += `${splitStr[i]}${ + i !== splitStrLength - 1 || isEndStringCharLastSentence ? endStringChar : '' + }`; + currentLength += splitStr[i].length + endStringChar.length; + } else { + break; + } + } + + return convStr; +} + +export function split(str: string, ...splitter: any[]): string[]; + +export function split(str: string, ...splitter: Array): string[]; + +export function split(str: string, ...splitter: Array | string[]): string[] { + if (!str) { + return []; + } + + const splitters = splitter.length > 0 && typeof splitter[0] === 'object' ? splitter[0] : splitter; + const splitterLength: number = splitters.length; + let charPattern = ''; + let strPattern = ''; + + for (let i = 0; i < splitterLength; i += 1) { + const spl = splitters[i]; + + if (spl.length > 1) { + strPattern += `${strPattern.length < 1 ? '' : '|'}${spl + .replace(/\\/g, '\\\\') + .replace(/\[/g, '\\[') + .replace(/]/g, '\\]') + .replace(/\?/g, '\\?') + .replace(/\./g, '\\.') + .replace(/\{/g, '\\{') + .replace(/}/g, '\\}') + .replace(/\+/g, '\\+')}`; + } else if (spl === '-' || spl === '[' || spl === ']') { + charPattern += `\\${spl}`; + } else { + charPattern += spl; + } + } + + if (charPattern.length < 1 && strPattern.length < 1) { + return [str]; + } + + if (charPattern.length > 0) { + charPattern = `[${charPattern}]`; + if (strPattern.length > 0) { + strPattern = `|${strPattern}`; + } + } + + return str.split(new RegExp(`${charPattern}${strPattern}+`, 'gi')); +} + +export function strToAscii(str: string): number[] { + const arr = []; + for (let i = 0; i < str.length; i += 1) { + arr.push(str.charCodeAt(i)); + } + return arr; +} + +export function strUnique(str: string): string { + if (!str) { + return ''; + } + + return [...new Set(str)].join(''); +} + +export function urlJoin(...args: any[]): string { + if (!args) { + return ''; + } + + const argLength = args.length; + let urlResult = ''; + let joinCount = 0; + + for (let i = 0; i < argLength; i += 1) { + if (args[i] !== null && args[i] !== undefined) { + if ( + joinCount === 0 || + args[i].startsWith('/') || + args[i].startsWith('?') || + args[i].startsWith('&') + ) { + urlResult += args[i]; + } else { + urlResult += `/${args[i]}`; + } + + joinCount += 1; + } + } + + return urlResult.replace(/\/$/g, ''); +} diff --git a/lib/types/global.ts b/lib/types/global.ts new file mode 100644 index 0000000..c89cebc --- /dev/null +++ b/lib/types/global.ts @@ -0,0 +1,16 @@ +export type PositiveNumber = number extends N + ? N + : `${N}` extends `-${string}` + ? never + : N; + +export type NumberValueObject = { [key: string]: number }; + +export type AnyValueObject = { [key: string]: any }; + +export type DurationOptions = { + useShortString?: boolean; + useSpace?: boolean; + withZeroValue?: boolean; + separator?: string; +}; diff --git a/lib/verify.ts b/lib/verify.ts new file mode 100644 index 0000000..44fc649 --- /dev/null +++ b/lib/verify.ts @@ -0,0 +1,133 @@ +export function isObject(data: any): boolean { + return data !== null && data !== undefined && Object.getPrototypeOf(data) === Object.prototype; +} + +export function isEqual(leftOperand: any, ...rightOperand: Array): boolean { + const rightOperands = + rightOperand.length > 0 && typeof rightOperand[0] === 'object' ? rightOperand[0] : rightOperand; + const rightOperandLength: number = rightOperands.length; + + for (let i = 0; i < rightOperandLength; i += 1) { + // eslint-disable-next-line eqeqeq + if (rightOperands[i] != leftOperand) { + return false; + } + } + + return true; +} + +export function isEqualStrict(leftOperand: any, ...rightOperand: Array): boolean { + const rightOperands = + rightOperand.length > 0 && typeof rightOperand[0] === 'object' ? rightOperand[0] : rightOperand; + const rightOperandLength: number = rightOperands.length; + + for (let i = 0; i < rightOperandLength; i += 1) { + if (rightOperands[i] !== leftOperand) { + return false; + } + } + + return true; +} + +export function isEmpty(data?: any): boolean { + if (!data) { + return true; + } + + switch (typeof data) { + case 'string': + return data.length < 1; + case 'object': + if (Array.isArray(data)) { + return data.length < 1; + } + return Object.keys(data).length < 1; + default: + return false; + } +} + +export function isUrl(url: string, withProtocol = false, strict = false): boolean { + if (strict && url.indexOf('.') === -1) { + return false; + } + + try { + new URL(`${withProtocol && url.indexOf('://') === -1 ? 'https://' : ''}${url}`).toString(); + } catch (e) { + return false; + } + + return true; +} + +export function contains(str: any[] | string, search: any[] | string, exact = false): boolean { + if (typeof search === 'string') { + return str.length < 1 ? false : str.indexOf(search) !== -1; + } + + for (let i = 0, iLen = search.length; i < iLen; i += 1) { + if (exact) { + if (str === search[i]) { + return true; + } + } else if (str.indexOf(search[i]) !== -1) { + return true; + } + } + + return false; +} + +export function is2dArray(array: any[]): boolean { + return array.filter(Array.isArray).length > 0; +} + +export function between(range: [number, number], number: number, inclusive = false): boolean { + const minM = Math.min.apply(Math, [range[0], range[1]]); + const maxM = Math.max.apply(Math, [range[0], range[1]]); + + return inclusive ? number >= minM && number <= maxM : number > minM && number < maxM; +} + +export function len(data: any): number { + if (!data) { + return 0; + } + switch (typeof data) { + case 'object': + return Array.isArray(data) ? data.length : Object.keys(data).length; + case 'number': + case 'bigint': + return data.toString().length; + case 'boolean': + return data ? 4 : 5; + case 'function': + return data().length; + case 'string': + default: + return data.length; + } +} + +export function isEmail(email: string): boolean { + // RFC2822 Email Validation + return /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test( + email + ); +} + +export function isTrueMinimumNumberOfTimes(conditions: boolean[], minimumCount = 1): boolean { + const conditionLength = conditions.length; + let trueCount = 0; + + for (let i = 0; i < conditionLength; i += 1) { + if (typeof conditions[i] === 'boolean' && conditions[i]) { + trueCount += 1; + } + } + + return trueCount >= minimumCount; +} diff --git a/package-lock.json b/package-lock.json index 08072ef..2d58901 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "eslint-plugin-import": "2.29.1", "glob": "^11.0.0", "prettier": "^3.3.3", - "terser": "^5.36.0", + "terser-glob": "^1.1.0", "tsx": "^4.19.2", "typescript": "^5.6.3" }, @@ -3060,6 +3060,18 @@ "node": "20 || >=22" } }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3940,6 +3952,23 @@ "node": ">=10" } }, + "node_modules/terser-glob": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/terser-glob/-/terser-glob-1.1.0.tgz", + "integrity": "sha512-Y9/ptzOh5AVdCHCY5hwo7tePtMfBYtoiRcMqkZGAsYSgIz31fCoUTZbOuavv59OaxjQoF0zuR7rtMWrydIwTCQ==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "meow": "^13.2.0", + "terser": "^5.36.0" + }, + "bin": { + "terser-glob": "dist/index.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index fb7b31e..ccd3c00 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test": "npm run build && glob -c \"tsx --test\" \"./test/**/*.test.ts\"", "lint": "eslint .", "lint:fix": "eslint --fix .", - "minify": "terser dist/index.js --config-file .terserrc -o dist/index.js", + "minify": "terser-glob 'dist/**/*.js' --config-file terser.config.json", "prepare": "npm run build", "format": "prettier .", "format:fix": "prettier . --write" @@ -66,7 +66,7 @@ "eslint-plugin-import": "2.29.1", "glob": "^11.0.0", "prettier": "^3.3.3", - "terser": "^5.36.0", + "terser-glob": "^1.1.0", "tsx": "^4.19.2", "typescript": "^5.6.3" } diff --git a/terser.config.json b/terser.config.json new file mode 100644 index 0000000..497cbde --- /dev/null +++ b/terser.config.json @@ -0,0 +1,10 @@ +{ + "module": true, + "compress": true, + "ie8": false, + "safari10": false, + "mangle": {}, + "output": {}, + "parse": {}, + "rename": {} +}