🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@complex-suite/utils

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@complex-suite/utils - npm Package Compare versions

Comparing version
3.0.14
to
3.0.15
+36
-0
history.md

@@ -0,1 +1,37 @@

<!--
版本规则:
- 顶部 `### current` 记录当前未发版的改动(可能为空)。
- 版本升级发版时,将 `current` 更替为本次发版版本号(如 `### 3.0.12`),并在其上方新建一个空的 `### current`。
- 这样每次发版后,顶部始终保留一个 `### current`(可能为空)用于记录后续改动。
-->
### current
### 3.0.15
- fix(observe): `observe` 类型守卫改为 `type !== 'object' && type !== 'array'`,数组可正常进入 Observer。
- fix(type): `isSame` 新增 Map/Set 比较分支:Map 比较 size 后逐 key 递归比较 value;Set 比较 size 后逐元素递归比较。
- fix(type): `isEmptyObject` 改用 `getComplexType`,map/set 用 size===0 判断,date/regExp/file/blob 返回 false。
- fix(object): `deepCloneData` 补充 date 分支 `new Date(data)` 和 regExp 分支 `new RegExp(data.source, data.flags)`。
- fix(color): `parseColor` 无效颜色值在 `exportMsg` 后显式 `return { r: 0, g: 0, b: 0, a: 1 }`,保证始终返回合法 RGBA 对象。
- fix(string): `getRandomInList` 返回类型改为 `T | undefined`,空列表直接 return undefined;`getRandomLetter` 在 list 为空时 return '' 兜底。
- fix(function): `triggerPromise` 将 `.then(success, fail)` 拆为 `.then(success).catch(fail)`,catch 同时捕获 rejection 和 success 内部异常。
- fix(utils): `openAnchor` 解析文件名时去除查询参数和 hash:`url.split('/').pop()?.split('?')[0]?.split('#')[0] || 'download'`。
- fix(observe): `Watcher.run` 条件改为 `value !== this.value || (typeof value == 'object' && value !== null)`,排除 null 值误触发。
- fix(type): `isSame` 增加 Date/RegExp 复杂对象的比较,Object.keys 无法正确反映它们的值。
- fix(worker): `startWorker` 在 onerror 和 onmessage 处理后增加 `worker.terminate()`,避免 Worker 线程累积泄漏。
- fix(observe): `traverse` 用 try/finally 包裹,确保异常路径下 seenObjects 也被清理,避免污染下次调用导致依赖收集失效。
- fix(object): `deepCloneData` 对 Map 的 key 进行深拷贝(`result.set(deepCloneData(key, map), deepCloneData(value, map))`),与 Set 元素处理保持一致,基本类型直接返回原值无性能影响。
- fix(observe): `defineReactive` 浅拷贝调用方传入的 descriptor(`{ ...(option.descriptor || {}) }`),避免污染调用方对象。
- fix(color): `parseColor` 在十六进制颜色长度不为 6 时调用 `exportMsg` 提示,无效格式不再静默返回 undefined。
- fix(wait): `Wait.trigger` 用 try/finally 包裹 forEach,保证回调抛错时 destroy 仍执行,避免实例泄漏。
- perf(tree): `formatTree.parse` 将扩展运算符改为 for 循环逐个 push,避免大数组栈溢出,结果与原逻辑一致。
- fix(number): `parseNum` 检测科学计数法(含 'e')时用 `toLocaleString('fullwide', { useGrouping: false })` 展开,修正极小/极大数解析错误。注:IE11 不支持 'fullwide' 会静默回退,但项目仅兼容 Android 16+ 不受影响。
- fix(object): `jsonToForm` 按类型分支处理 FormData.append:object/array→JSON.stringify,file/blob→直接 append,null/undefined→跳过不 append,其余→String() 转换,避免 number/boolean 抛错。
- fix(utils): `$exportMsg` 前缀由 `[complex-plugin]` 改为 `[complex-suite/utils]`,与包名一致。
- fix(utils): `openAnchor` 下载文件名提取增加空字符串回退 `url.split('/').pop() || 'download'`,URL 以 `/` 结尾时不再为空。
- perf(object): `deepCloneData` 普通对象改用 `Object.create(Object.getPrototypeOf(data))` 创建时指定原型,避免 `setPrototypeOf` 导致隐藏类失效;数组仍用 `[]` 创建(Object.create(Array.prototype) 无 length 内部槽),仅在原型不同时才 setPrototypeOf。
- fix(storage): `storage` 模块用 `typeof localStorage !== 'undefined'` 守卫,`_build*` 方法增加 `!targetStorage` 短路返回,SSR 环境(Node.js)中不再抛 ReferenceError。
- fix(string): `camelToLine` 正则改为 `/(?!^)([A-Z])/g`,首字母大写不插入前导分隔符(如 `FirstName` → `first_name` 而非 `_first_name`)。
- fix(utils): `getQueryData` 中 `split('=')` 改为 `indexOf('=')` + `slice`,只切第一个等号,含等号的值(如 base64)不再被截断。decodeURIComponent 跳过(用户确认不处理)。
### 3.0.11

@@ -2,0 +38,0 @@ - chore: 小版本升级,保持与套件其他子包版本同步。

+1
-1
{
"name": "@complex-suite/utils",
"version": "3.0.14",
"version": "3.0.15",
"description": "a complex utils",

@@ -5,0 +5,0 @@ "type": "module",

@@ -51,6 +51,10 @@

trigger() {
this.list.forEach(value => {
value()
})
this.destroy()
// try/finally 包裹,保证回调抛错时 destroy 仍执行,避免 Wait 实例泄漏
try {
this.list.forEach(value => {
value()
})
} finally {
this.destroy()
}
}

@@ -57,0 +61,0 @@ destroy() {

@@ -39,3 +39,4 @@ import $exportMsg from '../utils/$exportMsg'

triggerFunction(success, res)
}, err => {
}).catch(err => {
// 同时捕获 promise rejection 和 success 回调内部异常
triggerFunction(fail, err)

@@ -42,0 +43,0 @@ }).finally(() => {

@@ -13,3 +13,8 @@

// 将数字转换为字符串,避免浮点数精度问题
const numStr = num.toString()
// 注意:num.toString() 对极小/极大数字会生成科学计数法(如 1e-7 → "1e-7"),导致 parseInt('1e-7') 返回 1(错误,应为 0)
// 因此检测科学计数法时用 toLocaleString('fullwide') 展开(不产生精度尾巴)
let numStr = num.toString()
if (numStr.includes('e')) {
numStr = num.toLocaleString('fullwide', { useGrouping: false })
}
const [integerStr, decimalStr] = numStr.split('.')

@@ -16,0 +21,0 @@ const integer = parseInt(integerStr) || 0

@@ -18,5 +18,15 @@ import getComplexType from './../type/getComplexType'

} else {
result = type === 'object' ? {} : []
if (Object.getPrototypeOf(result) !== Object.getPrototypeOf(data)) {
Object.setPrototypeOf(result, Object.getPrototypeOf(data))
// 用 Object.create 创建时就指定原型,避免 setPrototypeOf 导致 V8 隐藏类失效(性能下降)
// Object.create(Object.getPrototypeOf(data)) 与原 {} / [] 行为一致:
// - 对于普通对象,Object.getPrototypeOf({}) === Object.prototype,Object.create(Object.prototype) 等价于 {}
// - 对于数组,Object.getPrototypeOf([]) === Array.prototype,Object.create(Array.prototype) 不会自动初始化 length 等内部槽
// 因此数组仍需用 [] 创建后再设置原型(Object.create(Array.prototype) 不是真正的数组)
if (type === 'array') {
result = []
if (Object.getPrototypeOf(result) !== Object.getPrototypeOf(data)) {
Object.setPrototypeOf(result, Object.getPrototypeOf(data))
}
} else {
// 普通对象直接用 Object.create 创建,避免后续 setPrototypeOf
result = Object.create(Object.getPrototypeOf(data))
}

@@ -35,3 +45,4 @@ map.set(data, result)

(data as unknown as Map<any, any>).forEach((value, key) => {
result.set(key, deepCloneData(value, map))
// key 同样需要深拷贝,与 Set 元素处理保持一致;基本类型 deepCloneData 直接返回原值,无性能影响
result.set(deepCloneData(key, map), deepCloneData(value, map))
})

@@ -46,2 +57,7 @@ return result as unknown as T

return result as unknown as T
} else if (type === 'date') {
return new Date(data as unknown as Date) as unknown as T
} else if (type === 'regExp') {
const d = data as unknown as RegExp
return new RegExp(d.source, d.flags) as unknown as T
} else {

@@ -48,0 +64,0 @@ return data

@@ -89,4 +89,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */

if (!item.load && item.data[this.childrenProp]) {
// 使用 push 和扩展运算符替换 concat,可以避免重复创建新数组,性能更好。
list.push(...item.data[this.childrenProp])
// 使用 for 循环逐个 push,避免扩展运算符对大数组触发参数数量上限导致的栈溢出
const children = item.data[this.childrenProp]
for (let i = 0; i < children.length; i++) {
list.push(children[i])
}
}

@@ -93,0 +96,0 @@ })

@@ -13,6 +13,11 @@ import getComplexType from './../type/getComplexType'

const type = getComplexType(jsonData[prop])
if (type === 'object') {
if (type === 'object' || type === 'array') {
formData.append(prop, JSON.stringify(jsonData[prop]))
} else if (type === 'file' || type === 'blob') {
formData.append(prop, jsonData[prop])
} else if (type === 'null' || type === 'undefined') {
// null/undefined 跳过不 append
continue
} else {
formData.append(prop, jsonData[prop])
formData.append(prop, String(jsonData[prop]))
}

@@ -19,0 +24,0 @@ }

@@ -10,4 +10,8 @@ import { observeProp, observeObject } from "./Observer"

function traverse(val: unknown) {
_traverse(val, seenObjects)
seenObjects.clear()
try {
_traverse(val, seenObjects)
} finally {
// 确保异常路径下 seenObjects 也被清理,避免污染下次调用导致依赖收集失效
seenObjects.clear()
}
}

@@ -14,0 +18,0 @@

@@ -20,3 +20,4 @@ import getComplexType from "../type/getComplexType"

function observe(value: unknown) {
if (getComplexType(value) !== 'object') {
const type = getComplexType(value)
if (type !== 'object' && type !== 'array') {
return

@@ -23,0 +24,0 @@ }

@@ -128,3 +128,3 @@ import getType from '../type/getType'

const value = this.get()
if (value !== this.value || typeof value == 'object') {
if (value !== this.value || (typeof value == 'object' && value !== null)) {
const oldValue = this.value

@@ -131,0 +131,0 @@ this.value = value

@@ -34,3 +34,4 @@ import getComplexType from '../type/getComplexType'

const setter = currentDescriptor?.set
const descriptor = option.descriptor || {}
// 浅拷贝 descriptor,避免修改调用方传入的对象(configurable/enumerable 默认值会污染原对象)
const descriptor = { ...(option.descriptor || {}) }
if (descriptor.configurable == undefined) {

@@ -37,0 +38,0 @@ descriptor.configurable = true

@@ -23,4 +23,7 @@

},
_buildSetData(targetStorage: Storage) {
_buildSetData(targetStorage: Storage | undefined) {
return function(name: string, value?: unknown, time?: number) {
if (!targetStorage) {
return false
}
const prop = storage.getProp(name)

@@ -41,4 +44,7 @@ const timeProp = storage.getTimeProp(name)

},
_buildGetData(targetStorage: Storage) {
_buildGetData(targetStorage: Storage | undefined) {
return function(name: string, option?: true | number, refresh?: boolean) {
if (!targetStorage) {
return undefined
}
const prop = storage.getProp(name)

@@ -77,4 +83,7 @@ const timeProp = storage.getTimeProp(name)

},
_buildRemoveData(targetStorage: Storage) {
_buildRemoveData(targetStorage: Storage | undefined) {
return function(name: string) {
if (!targetStorage) {
return
}
const prop = storage.getProp(name)

@@ -86,4 +95,7 @@ const timeProp = storage.getTimeProp(name)

},
_buildClearData(targetStorage: Storage) {
_buildClearData(targetStorage: Storage | undefined) {
return function() {
if (!targetStorage) {
return
}
targetStorage.clear()

@@ -102,12 +114,16 @@ }

storage.setData = storage._buildSetData(localStorage)
storage.setSessionData = storage._buildSetData(sessionStorage)
storage.getData = storage._buildGetData(localStorage)
storage.getSessionData = storage._buildGetData(sessionStorage)
storage.removeData = storage._buildRemoveData(localStorage)
storage.removeSessionData = storage._buildRemoveData(sessionStorage)
storage.clearData = storage._buildClearData(localStorage)
storage.clearSessionData = storage._buildClearData(sessionStorage)
// typeof 守卫:SSR 环境(Node.js)中 localStorage/sessionStorage 未定义,直接访问会抛 ReferenceError
const _localStorage = typeof localStorage !== 'undefined' ? localStorage : undefined
const _sessionStorage = typeof sessionStorage !== 'undefined' ? sessionStorage : undefined
storage.setData = storage._buildSetData(_localStorage)
storage.setSessionData = storage._buildSetData(_sessionStorage)
storage.getData = storage._buildGetData(_localStorage)
storage.getSessionData = storage._buildGetData(_sessionStorage)
storage.removeData = storage._buildRemoveData(_localStorage)
storage.removeSessionData = storage._buildRemoveData(_sessionStorage)
storage.clearData = storage._buildClearData(_localStorage)
storage.clearSessionData = storage._buildClearData(_sessionStorage)
export default storage

@@ -17,6 +17,11 @@ import { describe, it, expect } from 'vitest'

it('should handle a string that starts with an uppercase letter', () => {
expect(camelToLine('HelloWorld')).toBe('_hello_world')
it('should handle a string that starts with an uppercase letter (no leading underscore)', () => {
expect(camelToLine('HelloWorld')).toBe('hello_world')
expect(camelToLine('XMLHttpRequest')).toBe('x_m_l_http_request')
})
it('should use custom separator', () => {
expect(camelToLine('HelloWorld', '-')).toBe('hello-world')
})
it('should handle an empty string', () => {

@@ -23,0 +28,0 @@ expect(camelToLine('')).toBe('')

function camelToLine(str: string, line = '_'): string {
return str.replace(/([A-Z])/g, line + '$1').toLowerCase()
// (?!^) 排除首字符,首字母大写不插入前导分隔符(如 'FirstName' → 'first_name' 而非 '_first_name')
return str.replace(/(?!^)([A-Z])/g, line + '$1').toLowerCase()
}
export default camelToLine

@@ -8,3 +8,4 @@ import getRandomNum from './../number/getRandomNum'

*/
function getRandomInList<T>(list: T[]):T {
function getRandomInList<T>(list: T[]): T | undefined {
if (!list.length) return undefined
return list[getRandomNum(0, list.length - 1)]

@@ -11,0 +12,0 @@ }

@@ -46,5 +46,6 @@ import getRandomInList from './getRandomInList'

}
return getRandomInList(list)
if (list.length === 0) return ''
return getRandomInList(list)!
}
export default getRandomLetter

@@ -1,2 +0,2 @@

import getType from './getType'
import getComplexType from './getComplexType'
import isEmptyArray from './isEmptyArray'

@@ -8,3 +8,2 @@ import isEmptyObject from './isEmptyObject'

* @param {*} value 需要判断的值
* @param {string[]} [checkList] 需要深入判断的数据类型,对象和数组可选
* @returns {boolean} value is Empty

@@ -17,4 +16,4 @@ */

} else {
const type = getType(value)
if (type === 'object') {
const type = getComplexType(value)
if (type === 'object' || type === 'map' || type === 'set') {
return isEmptyObject(value, type)

@@ -21,0 +20,0 @@ } else if (type === 'array') {

@@ -1,2 +0,2 @@

import getType from './getType'
import getComplexType from './getComplexType'

@@ -6,3 +6,3 @@ /**

* @param {*} value 需要判断的值
* @param {string} type 类型
* @param {string} type 类型(getComplexType 返回值)
* @returns {boolean} value is EmptyObject

@@ -12,4 +12,10 @@ */

if (!type) {
type = getType(value)
type = getComplexType(value)
}
if (type === 'map' || type === 'set') {
return (value as Map<any, any> | Set<any>).size === 0
}
if (type === 'date' || type === 'regExp' || type === 'file' || type === 'blob') {
return false
}
if (type === 'object') {

@@ -20,7 +26,6 @@ for (const _prop in value as Record<PropertyKey, any>) {

return true
} else {
return false
}
return false
}
export default isEmptyObject
/* eslint-disable @typescript-eslint/no-explicit-any */
import getType from './getType'
import getComplexType from './getComplexType'
/**
* 是否相同=>对象则直接进行属性的依次对比
* 额外处理 Date(比较 getTime)、RegExp(比较 source/flags)、Map(逐键值)、Set(逐元素) 等复杂对象
* File/Blob 通过 Object.keys 比较属性
* @param {*} value 需要判断的数据
* @param {*} other 需要判断的数据
*/
function isSame(value: any, other: any): boolean {
const type = getType(value)
const otherType = getType(other)
if (type !== otherType) {
// 类型不同直接返回不同
return false
} else {
if (type == 'object' || type === 'array') {
// 对象或者数组则依次对比每个属性
const propList = Object.keys(value)
const otherPropList = Object.keys(other)
if (propList.length === otherPropList.length) {
if (propList.length === 0) {
return true
} else {
for (let i = 0; i < propList.length; i++) {
const prop = propList[i]
if (otherPropList.indexOf(prop) < 0) {
return false
}
const fg = isSame(value[prop], other[prop])
if (!fg) {
return false
}
}
}
return true
} else {
return false
}
} else {
if (type === 'number' && isNaN(value) && isNaN(other)) {
// NAN === NAN
return true
}
return value === other
const type = getComplexType(value)
const otherType = getComplexType(other)
if (type !== otherType) return false
if (type === 'date') return value.getTime() === other.getTime()
if (type === 'regExp') return value.source === other.source && value.flags === other.flags
if (type === 'map') {
if (value.size !== other.size) return false
for (const [k, v] of value) {
if (!other.has(k) || !isSame(v, other.get(k))) return false
}
return true
}
if (type === 'set') {
if (value.size !== other.size) return false
for (const v of value) {
if (![...other].some(ov => isSame(v, ov))) return false
}
return true
}
// object / array / file / blob 通过 Object.keys 逐属性比较
if (type === 'object' || type === 'array' || type === 'file' || type === 'blob') {
const propList = Object.keys(value)
const otherPropList = Object.keys(other)
if (propList.length !== otherPropList.length) return false
if (propList.length === 0) return true
for (let i = 0; i < propList.length; i++) {
const prop = propList[i]
if (otherPropList.indexOf(prop) < 0) return false
if (!isSame(value[prop], other[prop])) return false
}
return true
}
// 基础类型:NaN === NaN 特殊处理
if (type === 'number' && isNaN(value) && isNaN(other)) return true
return value === other
}
export default isSame

@@ -8,5 +8,5 @@ import exportMsg, { consoleType } from './exportMsg'

function $exportMsg(msg: string, type?: consoleType) {
exportMsg('[complex-plugin]' + msg, type)
exportMsg('[complex-suite/utils]' + msg, type)
}
export default $exportMsg

@@ -13,4 +13,8 @@ /**

if (item) {
const [key, value] = item.split('=')
queryData[key] = value
const eqIndex = item.indexOf('=')
if (eqIndex > -1) {
queryData[item.slice(0, eqIndex)] = item.slice(eqIndex + 1)
} else {
queryData[item] = undefined
}
}

@@ -17,0 +21,0 @@ }

@@ -18,4 +18,4 @@

if (download === true) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
download = url.split('/').pop()!
// url 以 / 结尾时 pop() 返回空字符串,回退到默认文件名
download = url.split('/').pop()?.split('?')[0]?.split('#')[0] || 'download'
}

@@ -22,0 +22,0 @@ anchor.setAttribute('download', download)

@@ -25,3 +25,7 @@ import exportMsg from "./exportMsg"

return { r, g, b, a: 1 }
} else {
// 非法长度(既非3也非6),给出错误提示
exportMsg(utilsLocale.t('color.invalidFormat'))
}
return { r: 0, g: 0, b: 0, a: 1 }
} else {

@@ -46,2 +50,3 @@ // 处理 rgb 和 rgba 颜色值

exportMsg(utilsLocale.t('color.invalidFormat'))
return { r: 0, g: 0, b: 0, a: 1 }
}

@@ -48,0 +53,0 @@ }

@@ -80,2 +80,3 @@ import getType from '../type/getType'

URL.revokeObjectURL(url)
worker.terminate()
}

@@ -90,2 +91,3 @@ worker.onmessage = function (event) {

URL.revokeObjectURL(url)
worker.terminate()
}

@@ -92,0 +94,0 @@ worker.postMessage({