@fast-china/utils 2.0.3 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +11 -1
- package/README.zh.md +12 -2
- package/dist/array/index.mjs +1 -1
- package/dist/array/index.mjs.map +1 -1
- package/dist/async/index.mjs +21 -19
- package/dist/async/index.mjs.map +1 -1
- package/dist/base64/index.d.mts +2 -2
- package/dist/base64/index.mjs +13 -12
- package/dist/base64/index.mjs.map +1 -1
- package/dist/color/index.mjs +4 -4
- package/dist/color/index.mjs.map +1 -1
- package/dist/crypto/index.d.mts +4 -3
- package/dist/crypto/index.mjs +38 -47
- package/dist/crypto/index.mjs.map +1 -1
- package/dist/date/index.mjs +3 -3
- package/dist/date/index.mjs.map +1 -1
- package/dist/dom/style.mjs +3 -3
- package/dist/dom/style.mjs.map +1 -1
- package/dist/env/index.d.mts +2 -2
- package/dist/env/index.mjs +12 -10
- package/dist/env/index.mjs.map +1 -1
- package/dist/identity/index.d.mts +6 -6
- package/dist/identity/index.mjs +8 -8
- package/dist/identity/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.global.min.js +2 -2
- package/dist/index.global.min.js.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/internal/text.mjs +4 -5
- package/dist/internal/text.mjs.map +1 -1
- package/dist/logger/index.mjs +5 -6
- package/dist/logger/index.mjs.map +1 -1
- package/dist/number/index.d.mts +6 -5
- package/dist/number/index.mjs +22 -21
- package/dist/number/index.mjs.map +1 -1
- package/dist/object/index.mjs +1 -1
- package/dist/object/index.mjs.map +1 -1
- package/dist/storage/index.mjs +24 -25
- package/dist/storage/index.mjs.map +1 -1
- package/dist/string/index.d.mts +18 -6
- package/dist/string/index.mjs +84 -31
- package/dist/string/index.mjs.map +1 -1
- package/dist/vue/emits.mjs +2 -2
- package/dist/vue/emits.mjs.map +1 -1
- package/dist/vue/func.mjs +1 -1
- package/dist/vue/func.mjs.map +1 -1
- package/dist/vue/install.mjs +12 -12
- package/dist/vue/install.mjs.map +1 -1
- package/dist/vue/props.d.mts +1 -1
- package/dist/vue/props.mjs.map +1 -1
- package/dist/vue/render.mjs +1 -1
- package/dist/vue/render.mjs.map +1 -1
- package/docs/API.md +31 -15
- package/docs/API.zh-CN.md +22 -6
- package/docs/RUNTIME_CONTRACT.md +2 -2
- package/package.json +9 -9
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { allEqualBy, chunk, difference, groupBy, hasDuplicatesBy, intersection, partition, removeNullishValues, unique, uniqueBy } from "./array/index.mjs";
|
|
2
2
|
import { debounce, mapConcurrent, retry, sleep, throttle, withTimeout } from "./async/index.mjs";
|
|
3
|
-
import { average, clamp, formatBytes, inRange, lerp,
|
|
3
|
+
import { average, clamp, formatBytes, inRange, lerp, randomInt, roundTo, sum } from "./number/index.mjs";
|
|
4
4
|
import { decodeBase64, decodeBase64Bytes, decodeBase64Url, decodeBase64UrlBytes, decodeLatin1Base64, decodeSecureBase64, encodeBase64, encodeBase64Bytes, encodeBase64Url, encodeBase64UrlBytes, encodeLatin1Base64, encodeSecureBase64 } from "./base64/index.mjs";
|
|
5
5
|
import { contrastRatio, formatHexColor, mixHexColorWithBlack, mixHexColorWithWhite, mixHexColors, parseHexColor, pickHigherContrastColor, relativeLuminance } from "./color/index.mjs";
|
|
6
6
|
import { AESDecrypt, AESDecryptAuthenticated, AESDecryptWithPassword, AESEncrypt, AESEncryptAuthenticated, AESEncryptWithPassword, DeriveECDHKeySHA256, DeriveECDHSecret, ECDSASign, ECDSAVerify, FixedTimeEquals, GenerateECDHKeyPair, GenerateECDSAKeyPair, GenerateRSAKeyPair, GenerateRandomBytes, HKDFSHA256, HMACSHA256Encrypt, HMACSHA384Encrypt, HMACSHA512Encrypt, HashPasswordPBKDF2SHA256, MD5Encrypt, PBKDF2SHA256, RSADecryptOAEP, RSAEncryptOAEP, RSASignPSS, RSAVerifyPSS, SHA1Encrypt, SHA256Bytes, SHA256Encrypt, SHA384Bytes, SHA384Encrypt, SHA512Bytes, SHA512Encrypt, VerifyPasswordPBKDF2SHA256 } from "./crypto/index.mjs";
|
|
@@ -8,7 +8,7 @@ import { addDays, addMonths, addYears, createDateRangeShortcuts, createDateShort
|
|
|
8
8
|
import { addCssUnit, serializeStyle } from "./dom/style.mjs";
|
|
9
9
|
import { detectRuntime, hasWebCrypto, isBrowser, isMobileUserAgent, isNode, isTabletUserAgent, isUniApp, isWebWorker } from "./env/index.mjs";
|
|
10
10
|
import { Local, Session, base64StorageCodec, configureStorage, isStorageConfigured } from "./storage/index.mjs";
|
|
11
|
-
import { camelCase, decodeURIComponentRepeatedly, escapeHtml, generateUuidV4, isUuidV4, isValidJson, kebabCase, lowerFirst, normalizeWhitespace, parseQueryString, pascalCase,
|
|
11
|
+
import { camelCase, copy, decodeURIComponentRepeatedly, escapeHtml, generateUuidV4, isUuidV4, isValidJson, kebabCase, lowerFirst, normalizeWhitespace, parseQueryString, pascalCase, randomString, splitWords, truncateGraphemes, upperFirst } from "./string/index.mjs";
|
|
12
12
|
import { configureInstallationIdentity, getOrCreateInstallationId, installationIdentity } from "./identity/index.mjs";
|
|
13
13
|
import { createLogger, logger } from "./logger/index.mjs";
|
|
14
14
|
import { hasOwn, isPlainObject, mapValues, omit, pick, shallowEqual, toQueryString } from "./object/index.mjs";
|
|
@@ -20,4 +20,4 @@ import { definePropType, useProps } from "./vue/props.mjs";
|
|
|
20
20
|
import { useRender } from "./vue/render.mjs";
|
|
21
21
|
import { makeSlots } from "./vue/slots.mjs";
|
|
22
22
|
import { withDefineType } from "./vue/with.mjs";
|
|
23
|
-
export { AESDecrypt, AESDecryptAuthenticated, AESDecryptWithPassword, AESEncrypt, AESEncryptAuthenticated, AESEncryptWithPassword, DeriveECDHKeySHA256, DeriveECDHSecret, ECDSASign, ECDSAVerify, FixedTimeEquals, GenerateECDHKeyPair, GenerateECDSAKeyPair, GenerateRSAKeyPair, GenerateRandomBytes, HKDFSHA256, HMACSHA256Encrypt, HMACSHA384Encrypt, HMACSHA512Encrypt, HashPasswordPBKDF2SHA256, Local, MD5Encrypt, PBKDF2SHA256, RSADecryptOAEP, RSAEncryptOAEP, RSASignPSS, RSAVerifyPSS, SHA1Encrypt, SHA256Bytes, SHA256Encrypt, SHA384Bytes, SHA384Encrypt, SHA512Bytes, SHA512Encrypt, Session, VerifyPasswordPBKDF2SHA256, addCssUnit, addDays, addMonths, addYears, allEqualBy, average, base64StorageCodec, callOptionalFunction, camelCase, chunk, clamp, configureInstallationIdentity, configureStorage, contrastRatio, createDateRangeShortcuts, createDateShortcuts, createLogger, createOneMonthRangeFromToday, debounce, decodeBase64, decodeBase64Bytes, decodeBase64Url, decodeBase64UrlBytes, decodeLatin1Base64, decodeSecureBase64, decodeURIComponentRepeatedly, definePropType, detectRuntime, difference, encodeBase64, encodeBase64Bytes, encodeBase64Url, encodeBase64UrlBytes, encodeLatin1Base64, encodeSecureBase64, endOfDay, escapeHtml, formatBytes, formatChineseRelativeTime, formatHexColor, formatRelativeTime, generateUuidV4, getLocalDayBounds, getLocalTimeGreeting, getOrCreateInstallationId, getStartOfToday, groupBy, hasDuplicatesBy, hasOwn, hasWebCrypto, inRange, installationIdentity, intersection, isBrowser, isDateAfterNow, isFuture, isMobileUserAgent, isNode, isPlainObject, isSameDay, isStorageConfigured, isTabletUserAgent, isUniApp, isUuidV4, isValidDate, isValidJson, isWebWorker, isWithinInterval, kebabCase, lerp, logger, lowerFirst, makeSlots, mapConcurrent, mapValues, mixHexColorWithBlack, mixHexColorWithWhite, mixHexColors, normalizeWhitespace, omit, parseHexColor, parseQueryString, partition, pascalCase, pick, pickHigherContrastColor, relativeLuminance, removeNullishValues, retry, roundTo,
|
|
23
|
+
export { AESDecrypt, AESDecryptAuthenticated, AESDecryptWithPassword, AESEncrypt, AESEncryptAuthenticated, AESEncryptWithPassword, DeriveECDHKeySHA256, DeriveECDHSecret, ECDSASign, ECDSAVerify, FixedTimeEquals, GenerateECDHKeyPair, GenerateECDSAKeyPair, GenerateRSAKeyPair, GenerateRandomBytes, HKDFSHA256, HMACSHA256Encrypt, HMACSHA384Encrypt, HMACSHA512Encrypt, HashPasswordPBKDF2SHA256, Local, MD5Encrypt, PBKDF2SHA256, RSADecryptOAEP, RSAEncryptOAEP, RSASignPSS, RSAVerifyPSS, SHA1Encrypt, SHA256Bytes, SHA256Encrypt, SHA384Bytes, SHA384Encrypt, SHA512Bytes, SHA512Encrypt, Session, VerifyPasswordPBKDF2SHA256, addCssUnit, addDays, addMonths, addYears, allEqualBy, average, base64StorageCodec, callOptionalFunction, camelCase, chunk, clamp, configureInstallationIdentity, configureStorage, contrastRatio, copy, createDateRangeShortcuts, createDateShortcuts, createLogger, createOneMonthRangeFromToday, debounce, decodeBase64, decodeBase64Bytes, decodeBase64Url, decodeBase64UrlBytes, decodeLatin1Base64, decodeSecureBase64, decodeURIComponentRepeatedly, definePropType, detectRuntime, difference, encodeBase64, encodeBase64Bytes, encodeBase64Url, encodeBase64UrlBytes, encodeLatin1Base64, encodeSecureBase64, endOfDay, escapeHtml, formatBytes, formatChineseRelativeTime, formatHexColor, formatRelativeTime, generateUuidV4, getLocalDayBounds, getLocalTimeGreeting, getOrCreateInstallationId, getStartOfToday, groupBy, hasDuplicatesBy, hasOwn, hasWebCrypto, inRange, installationIdentity, intersection, isBrowser, isDateAfterNow, isFuture, isMobileUserAgent, isNode, isPlainObject, isSameDay, isStorageConfigured, isTabletUserAgent, isUniApp, isUuidV4, isValidDate, isValidJson, isWebWorker, isWithinInterval, kebabCase, lerp, logger, lowerFirst, makeSlots, mapConcurrent, mapValues, mixHexColorWithBlack, mixHexColorWithWhite, mixHexColors, normalizeWhitespace, omit, parseHexColor, parseQueryString, partition, pascalCase, pick, pickHigherContrastColor, randomInt, randomString, relativeLuminance, removeNullishValues, retry, roundTo, serializeStyle, shallowEqual, sleep, splitWords, startOfDay, sum, throttle, toDate, toQueryString, truncateGraphemes, unique, uniqueBy, upperFirst, useEmits, useExpose, useProps, useRender, withDefineType, withInstall, withInstallDirective, withNoopInstall, withTimeout };
|
package/dist/internal/text.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
//#region src/internal/text.ts
|
|
2
|
-
const runtimeEncodingGlobals = globalThis;
|
|
3
2
|
/**
|
|
4
3
|
* 延迟解析 UTF-8 TextDecoder,确保模块导入阶段不依赖 Encoding API。
|
|
5
4
|
*
|
|
@@ -7,8 +6,8 @@ const runtimeEncodingGlobals = globalThis;
|
|
|
7
6
|
* @throws `Error` 当当前平台没有提供 `TextDecoder`。
|
|
8
7
|
*/
|
|
9
8
|
const getTextDecoder = () => {
|
|
10
|
-
const TextDecoderConstructor =
|
|
11
|
-
if (typeof TextDecoderConstructor !== "function") throw new Error("TextDecoder
|
|
9
|
+
const TextDecoderConstructor = globalThis.TextDecoder;
|
|
10
|
+
if (typeof TextDecoderConstructor !== "function") throw new Error("当前运行环境不支持 TextDecoder。");
|
|
12
11
|
return new TextDecoderConstructor("utf-8", { fatal: true });
|
|
13
12
|
};
|
|
14
13
|
/**
|
|
@@ -18,8 +17,8 @@ const getTextDecoder = () => {
|
|
|
18
17
|
* @throws `Error` 当当前平台没有提供 `TextEncoder`。
|
|
19
18
|
*/
|
|
20
19
|
const getTextEncoder = () => {
|
|
21
|
-
const TextEncoderConstructor =
|
|
22
|
-
if (typeof TextEncoderConstructor !== "function") throw new Error("TextEncoder
|
|
20
|
+
const TextEncoderConstructor = globalThis.TextEncoder;
|
|
21
|
+
if (typeof TextEncoderConstructor !== "function") throw new Error("当前运行环境不支持 TextEncoder。");
|
|
23
22
|
return new TextEncoderConstructor();
|
|
24
23
|
};
|
|
25
24
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.mjs","names":[],"sources":["../../src/internal/text.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"text.mjs","names":[],"sources":["../../src/internal/text.ts"],"sourcesContent":["/**\n * 延迟解析 UTF-8 TextDecoder,确保模块导入阶段不依赖 Encoding API。\n *\n * @returns 启用 Fatal 模式的新解码器,非法 UTF-8 会直接失败。\n * @throws `Error` 当当前平台没有提供 `TextDecoder`。\n */\nexport const getTextDecoder = (): TextDecoder => {\n\tconst TextDecoderConstructor = globalThis.TextDecoder;\n\tif (typeof TextDecoderConstructor !== \"function\") {\n\t\tthrow new Error(\"当前运行环境不支持 TextDecoder。\");\n\t}\n\treturn new TextDecoderConstructor(\"utf-8\", { fatal: true });\n};\n\n/**\n * 延迟解析 TextEncoder,让纯字节 API 在缺少 Encoding API 的平台仍可导入。\n *\n * @returns 新建的 UTF-8 编码器。\n * @throws `Error` 当当前平台没有提供 `TextEncoder`。\n */\nexport const getTextEncoder = (): TextEncoder => {\n\tconst TextEncoderConstructor = globalThis.TextEncoder;\n\tif (typeof TextEncoderConstructor !== \"function\") {\n\t\tthrow new Error(\"当前运行环境不支持 TextEncoder。\");\n\t}\n\treturn new TextEncoderConstructor();\n};\n\n/**\n * 在确认平台能力后把 JavaScript 字符串编码为 UTF-8。\n *\n * @param value - 待编码文本。\n * @returns 使用独立 ArrayBuffer 的 UTF-8 字节数组。\n * @throws `Error` 当当前平台没有提供 `TextEncoder`。\n */\nexport const encodeUtf8 = (value: string): Uint8Array<ArrayBuffer> => getTextEncoder().encode(value);\n"],"mappings":";;;;;;;AAMA,MAAa,uBAAoC;CAChD,MAAM,yBAAyB,WAAW;CAC1C,IAAI,OAAO,2BAA2B,YACrC,MAAM,IAAI,MAAM,wBAAwB;CAEzC,OAAO,IAAI,uBAAuB,SAAS,EAAE,OAAO,KAAK,CAAC;AAC3D;;;;;;;AAQA,MAAa,uBAAoC;CAChD,MAAM,yBAAyB,WAAW;CAC1C,IAAI,OAAO,2BAA2B,YACrC,MAAM,IAAI,MAAM,wBAAwB;CAEzC,OAAO,IAAI,uBAAuB;AACnC;;;;;;;;AASA,MAAa,cAAc,UAA2C,eAAe,CAAC,CAAC,OAAO,KAAK"}
|
package/dist/logger/index.mjs
CHANGED
|
@@ -12,14 +12,13 @@ const levelPriority = {
|
|
|
12
12
|
* @returns 值是 `debug`、`info`、`warn` 或 `error` 时返回 `true`。
|
|
13
13
|
*/
|
|
14
14
|
const isLogLevel = (value) => typeof value === "string" && Object.hasOwn(levelPriority, value);
|
|
15
|
-
const runtimeLoggerGlobals = globalThis;
|
|
16
15
|
/**
|
|
17
16
|
* 检测 uni-app App-Plus 日志环境。
|
|
18
17
|
*
|
|
19
18
|
* @returns 全局 `uni` 与 `plus` 同时存在时返回 `true`。
|
|
20
19
|
*/
|
|
21
20
|
const isUniAppPlus = () => {
|
|
22
|
-
return
|
|
21
|
+
return Reflect.get(globalThis, "uni") !== void 0 && Reflect.get(globalThis, "plus") !== void 0;
|
|
23
22
|
};
|
|
24
23
|
/**
|
|
25
24
|
* 把日志附加值转换为适合 HBuilderX 单行输出的文本。
|
|
@@ -74,8 +73,8 @@ function createLogger(options = {}) {
|
|
|
74
73
|
const requestedLevel = options.level ?? "info";
|
|
75
74
|
const requestedPrefix = options.prefix ?? "Fast";
|
|
76
75
|
const sink = options.sink ?? defaultConsoleSink;
|
|
77
|
-
if (!isLogLevel(requestedLevel)) throw new RangeError(
|
|
78
|
-
if (typeof requestedPrefix !== "string" || requestedPrefix.length === 0) throw new RangeError("
|
|
76
|
+
if (!isLogLevel(requestedLevel)) throw new RangeError(`未知的日志级别:${String(requestedLevel)}。`);
|
|
77
|
+
if (typeof requestedPrefix !== "string" || requestedPrefix.length === 0) throw new RangeError("日志前缀必须是非空字符串。");
|
|
79
78
|
const level = requestedLevel;
|
|
80
79
|
const prefix = requestedPrefix;
|
|
81
80
|
const uniAppPlusSplit = options.uniAppPlusSplit ?? false;
|
|
@@ -89,8 +88,8 @@ function createLogger(options = {}) {
|
|
|
89
88
|
* @throws `RangeError` 当作用域不是非空字符串或包含外围空白。
|
|
90
89
|
*/
|
|
91
90
|
const write = (messageLevel, scope, message, data) => {
|
|
92
|
-
if (typeof scope !== "string") throw new TypeError("
|
|
93
|
-
if (scope.length === 0 || scope.trim() !== scope) throw new RangeError("
|
|
91
|
+
if (typeof scope !== "string") throw new TypeError("日志作用域必须是字符串。");
|
|
92
|
+
if (scope.length === 0 || scope.trim() !== scope) throw new RangeError("日志作用域必须是无外围空白的非空字符串。");
|
|
94
93
|
if (levelPriority[messageLevel] < levelPriority[level]) return;
|
|
95
94
|
const heading = `[${prefix}:${scope}]`;
|
|
96
95
|
const sinkMethod = messageLevel === "info" ? "log" : messageLevel;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/logger/index.ts"],"sourcesContent":["/** 日志严重级别,按从低到高排列。 */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/** 日志输出目标需要实现的最小控制台接口。 */\nexport interface LoggerSink {\n\t/**\n\t * 接收通过级别过滤后的调试参数。\n\t * @param data - 已格式化的品牌、作用域、消息以及保持原始类型的附加值。\n\t */\n\tdebug: (...data: unknown[]) => void;\n\t/**\n\t * 接收通过级别过滤后的普通信息参数;对应 Logger 的 `info` 级别。\n\t * @param data - 已格式化的品牌、作用域、消息以及保持原始类型的附加值。\n\t */\n\tlog: (...data: unknown[]) => void;\n\t/**\n\t * 接收通过级别过滤后的警告参数。\n\t * @param data - 已格式化的品牌、作用域、消息以及保持原始类型的附加值。\n\t */\n\twarn: (...data: unknown[]) => void;\n\t/**\n\t * 接收通过级别过滤后的错误参数。\n\t * @param data - 已格式化的品牌、作用域、消息以及保持原始类型的附加值。\n\t */\n\terror: (...data: unknown[]) => void;\n}\n\n/** {@link createLogger} 的不可变配置。 */\nexport interface LoggerOptions {\n\t/** 最低输出级别,默认 `info`;低于该优先级的消息不会传给 Sink。 */\n\tlevel?: LogLevel;\n\t/** 日志品牌前缀,默认 `Fast`;必须是无外围空白的非空字符串。 */\n\tprefix?: string;\n\t/** 可注入输出目标,默认当前运行时的 `console`;Logger 不会修改该对象。 */\n\tsink?: LoggerSink;\n\t/** uni-app App-Plus/HBuilderX 中把附加参数逐条转成单行文本输出,默认 `false`;其他平台忽略。 */\n\tuniAppPlusSplit?: boolean;\n}\n\n/** 配置隔离、无全局可变状态的轻量日志器。 */\nexport interface Logger {\n\t/**\n\t * 输出指定作用域的调试信息。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `TypeError` 或 `RangeError` 当作用域不是无外围空白的非空字符串。\n\t */\n\tdebug: (scope: string, message: string, ...data: unknown[]) => void;\n\t/**\n\t * 输出指定作用域的普通信息。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `TypeError` 或 `RangeError` 当作用域不是无外围空白的非空字符串。\n\t */\n\tinfo: (scope: string, message: string, ...data: unknown[]) => void;\n\t/**\n\t * 输出指定作用域的警告信息。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `TypeError` 或 `RangeError` 当作用域不是无外围空白的非空字符串。\n\t */\n\twarn: (scope: string, message: string, ...data: unknown[]) => void;\n\t/**\n\t * 输出指定作用域的错误信息。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `TypeError` 或 `RangeError` 当作用域不是无外围空白的非空字符串。\n\t */\n\terror: (scope: string, message: string, ...data: unknown[]) => void;\n}\n\nconst levelPriority: Readonly<Record<LogLevel, number>> = {\n\tdebug: 10,\n\tinfo: 20,\n\twarn: 30,\n\terror: 40,\n};\n\n/**\n * 判断未知值是否为受支持日志级别。\n *\n * @param value - 待检查配置值。\n * @returns 值是 `debug`、`info`、`warn` 或 `error` 时返回 `true`。\n */\nconst isLogLevel = (value: unknown): value is LogLevel => typeof value === \"string\" && Object.hasOwn(levelPriority, value);\n\n
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/logger/index.ts"],"sourcesContent":["/** 日志严重级别,按从低到高排列。 */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/** 日志输出目标需要实现的最小控制台接口。 */\nexport interface LoggerSink {\n\t/**\n\t * 接收通过级别过滤后的调试参数。\n\t * @param data - 已格式化的品牌、作用域、消息以及保持原始类型的附加值。\n\t */\n\tdebug: (...data: unknown[]) => void;\n\t/**\n\t * 接收通过级别过滤后的普通信息参数;对应 Logger 的 `info` 级别。\n\t * @param data - 已格式化的品牌、作用域、消息以及保持原始类型的附加值。\n\t */\n\tlog: (...data: unknown[]) => void;\n\t/**\n\t * 接收通过级别过滤后的警告参数。\n\t * @param data - 已格式化的品牌、作用域、消息以及保持原始类型的附加值。\n\t */\n\twarn: (...data: unknown[]) => void;\n\t/**\n\t * 接收通过级别过滤后的错误参数。\n\t * @param data - 已格式化的品牌、作用域、消息以及保持原始类型的附加值。\n\t */\n\terror: (...data: unknown[]) => void;\n}\n\n/** {@link createLogger} 的不可变配置。 */\nexport interface LoggerOptions {\n\t/** 最低输出级别,默认 `info`;低于该优先级的消息不会传给 Sink。 */\n\tlevel?: LogLevel;\n\t/** 日志品牌前缀,默认 `Fast`;必须是无外围空白的非空字符串。 */\n\tprefix?: string;\n\t/** 可注入输出目标,默认当前运行时的 `console`;Logger 不会修改该对象。 */\n\tsink?: LoggerSink;\n\t/** uni-app App-Plus/HBuilderX 中把附加参数逐条转成单行文本输出,默认 `false`;其他平台忽略。 */\n\tuniAppPlusSplit?: boolean;\n}\n\n/** 配置隔离、无全局可变状态的轻量日志器。 */\nexport interface Logger {\n\t/**\n\t * 输出指定作用域的调试信息。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `TypeError` 或 `RangeError` 当作用域不是无外围空白的非空字符串。\n\t */\n\tdebug: (scope: string, message: string, ...data: unknown[]) => void;\n\t/**\n\t * 输出指定作用域的普通信息。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `TypeError` 或 `RangeError` 当作用域不是无外围空白的非空字符串。\n\t */\n\tinfo: (scope: string, message: string, ...data: unknown[]) => void;\n\t/**\n\t * 输出指定作用域的警告信息。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `TypeError` 或 `RangeError` 当作用域不是无外围空白的非空字符串。\n\t */\n\twarn: (scope: string, message: string, ...data: unknown[]) => void;\n\t/**\n\t * 输出指定作用域的错误信息。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `TypeError` 或 `RangeError` 当作用域不是无外围空白的非空字符串。\n\t */\n\terror: (scope: string, message: string, ...data: unknown[]) => void;\n}\n\nconst levelPriority: Readonly<Record<LogLevel, number>> = {\n\tdebug: 10,\n\tinfo: 20,\n\twarn: 30,\n\terror: 40,\n};\n\n/**\n * 判断未知值是否为受支持日志级别。\n *\n * @param value - 待检查配置值。\n * @returns 值是 `debug`、`info`、`warn` 或 `error` 时返回 `true`。\n */\nconst isLogLevel = (value: unknown): value is LogLevel => typeof value === \"string\" && Object.hasOwn(levelPriority, value);\n\n/**\n * 检测 uni-app App-Plus 日志环境。\n *\n * @returns 全局 `uni` 与 `plus` 同时存在时返回 `true`。\n */\nconst isUniAppPlus = (): boolean => {\n\treturn Reflect.get(globalThis, \"uni\") !== undefined && Reflect.get(globalThis, \"plus\") !== undefined;\n};\n\n/**\n * 把日志附加值转换为适合 HBuilderX 单行输出的文本。\n *\n * @remarks 循环引用会替换为 `[Circular]`,BigInt 保留 `n` 后缀,Error 优先输出堆栈。\n * @param value - 任意日志附加值。\n * @returns 不会因 JSON 序列化失败而中断日志调用的文本。\n */\nconst formatSplitValue = (value: unknown): string => {\n\tif (typeof value === \"string\") return value;\n\tif (typeof value === \"bigint\") return `${value.toString()}n`;\n\tif (value instanceof Error) return value.stack ?? `${value.name}: ${value.message}`;\n\tconst visited = new WeakSet();\n\ttry {\n\t\tconst serialized: unknown = JSON.stringify(\n\t\t\tvalue,\n\t\t\t(_key, item: unknown): unknown => {\n\t\t\t\tif (typeof item === \"bigint\") return `${item.toString()}n`;\n\t\t\t\tif (typeof item !== \"object\" || item === null) return item;\n\t\t\t\tif (visited.has(item)) return \"[Circular]\";\n\t\t\t\tvisited.add(item);\n\t\t\t\treturn item;\n\t\t\t},\n\t\t\t2\n\t\t);\n\t\treturn typeof serialized === \"string\" ? serialized : String(value);\n\t} catch {\n\t\treturn String(value);\n\t}\n};\n\nconst defaultConsoleSink: LoggerSink = {\n\tdebug: (...data): void => {\n\t\tif (typeof console.debug === \"function\") console.debug(...data);\n\t\telse console.log(...data);\n\t},\n\tlog: (...data): void => {\n\t\tconsole.log(...data);\n\t},\n\twarn: (...data): void => {\n\t\tconsole.warn(...data);\n\t},\n\terror: (...data): void => {\n\t\tconsole.error(...data);\n\t},\n};\n\n/**\n * 创建独立日志器。\n *\n * @remarks 本库其他模块不会自动记录、吞掉或转换异常。日志内容可能进入持久化平台,\n * 调用方不得传入密码、令牌、密钥或完整个人数据。\n * @param options - 级别、前缀、输出目标和 uni-app App-Plus 拆分选项。\n * @returns 不会修改全局控制台或其他日志器配置的新实例。\n * @throws `RangeError` 当级别未知,或前缀、作用域不是有效的非空字符串。\n */\nexport function createLogger(options: LoggerOptions = {}): Logger {\n\tconst requestedLevel: unknown = options.level ?? \"info\";\n\tconst requestedPrefix: unknown = options.prefix ?? \"Fast\";\n\tconst sink = options.sink ?? defaultConsoleSink;\n\tif (!isLogLevel(requestedLevel)) throw new RangeError(`未知的日志级别:${String(requestedLevel)}。`);\n\tif (typeof requestedPrefix !== \"string\" || requestedPrefix.length === 0) {\n\t\tthrow new RangeError(\"日志前缀必须是非空字符串。\");\n\t}\n\tconst level = requestedLevel;\n\tconst prefix = requestedPrefix;\n\tconst uniAppPlusSplit = options.uniAppPlusSplit ?? false;\n\n\t/**\n\t * 应用级别过滤、标题格式和平台输出策略。\n\t *\n\t * @param messageLevel - 本条消息的严重级别。\n\t * @param scope - 模块、组件或业务来源名称。\n\t * @param message - 主消息文本。\n\t * @param data - 保持原始类型的附加值。\n\t * @throws `RangeError` 当作用域不是非空字符串或包含外围空白。\n\t */\n\tconst write = (messageLevel: LogLevel, scope: string, message: string, data: readonly unknown[]): void => {\n\t\tif (typeof scope !== \"string\") throw new TypeError(\"日志作用域必须是字符串。\");\n\t\tif (scope.length === 0 || scope.trim() !== scope) {\n\t\t\tthrow new RangeError(\"日志作用域必须是无外围空白的非空字符串。\");\n\t\t}\n\t\tif (levelPriority[messageLevel] < levelPriority[level]) return;\n\t\tconst heading = `[${prefix}:${scope}]`;\n\t\tconst sinkMethod: keyof LoggerSink = messageLevel === \"info\" ? \"log\" : messageLevel;\n\t\tif (uniAppPlusSplit && isUniAppPlus()) {\n\t\t\tsink[sinkMethod](`${heading} ${message}`);\n\t\t\tfor (const item of data) sink[sinkMethod](formatSplitValue(item));\n\t\t\treturn;\n\t\t}\n\t\tsink[sinkMethod](heading, message, ...data);\n\t};\n\n\treturn {\n\t\tdebug: (scope, message, ...data): void => {\n\t\t\twrite(\"debug\", scope, message, data);\n\t\t},\n\t\tinfo: (scope, message, ...data): void => {\n\t\t\twrite(\"info\", scope, message, data);\n\t\t},\n\t\twarn: (scope, message, ...data): void => {\n\t\t\twrite(\"warn\", scope, message, data);\n\t\t},\n\t\terror: (scope, message, ...data): void => {\n\t\t\twrite(\"error\", scope, message, data);\n\t\t},\n\t};\n}\n\n/** 默认使用 `Fast` 前缀和 `info` 级别的便捷日志器。 */\nexport const logger: Logger = createLogger();\n"],"mappings":"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"}
|
package/dist/number/index.d.mts
CHANGED
|
@@ -76,14 +76,15 @@ declare function lerp(start: number, end: number, amount: number): number;
|
|
|
76
76
|
*/
|
|
77
77
|
declare function formatBytes(bytes: number, options?: FormatBytesOptions): string;
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* 在半开区间内生成随机整数。
|
|
80
80
|
*
|
|
81
|
+
* @remarks 优先使用 Web Crypto;平台缺少安全随机能力时回退到 `Math.random()`。
|
|
81
82
|
* @param minimum - 包含的安全整数下界。
|
|
82
83
|
* @param maximumExclusive - 不包含的安全整数上界;区间宽度最大为 2^32。
|
|
83
|
-
* @returns
|
|
84
|
-
* @throws
|
|
84
|
+
* @returns 位于 `[minimum, maximumExclusive)` 的随机整数。
|
|
85
|
+
* @throws 参数非法时抛出 `RangeError`。
|
|
85
86
|
*/
|
|
86
|
-
declare function
|
|
87
|
+
declare function randomInt(minimum: number, maximumExclusive: number): number;
|
|
87
88
|
//#endregion
|
|
88
|
-
export { FormatBytesOptions, average, clamp, formatBytes, inRange, lerp,
|
|
89
|
+
export { FormatBytesOptions, average, clamp, formatBytes, inRange, lerp, randomInt, roundTo, sum };
|
|
89
90
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/number/index.mjs
CHANGED
|
@@ -21,7 +21,6 @@ const binaryByteUnits = [
|
|
|
21
21
|
"ZiB",
|
|
22
22
|
"YiB"
|
|
23
23
|
];
|
|
24
|
-
const runtimeNumberGlobals = globalThis;
|
|
25
24
|
/**
|
|
26
25
|
* 拒绝 NaN,同时允许具体 API 自行决定是否接受 Infinity。
|
|
27
26
|
*
|
|
@@ -30,7 +29,7 @@ const runtimeNumberGlobals = globalThis;
|
|
|
30
29
|
* @throws `RangeError` 当值为 NaN。
|
|
31
30
|
*/
|
|
32
31
|
const assertNotNaN = (value, name) => {
|
|
33
|
-
if (Number.isNaN(value)) throw new RangeError(
|
|
32
|
+
if (Number.isNaN(value)) throw new RangeError(`\`${name}\` 不能是 NaN。`);
|
|
34
33
|
};
|
|
35
34
|
/**
|
|
36
35
|
* 校验有限数值。
|
|
@@ -40,7 +39,7 @@ const assertNotNaN = (value, name) => {
|
|
|
40
39
|
* @throws `RangeError` 当值为 NaN 或无穷大。
|
|
41
40
|
*/
|
|
42
41
|
const assertFinite = (value, name) => {
|
|
43
|
-
if (!Number.isFinite(value)) throw new RangeError(
|
|
42
|
+
if (!Number.isFinite(value)) throw new RangeError(`\`${name}\` 必须是有限数。`);
|
|
44
43
|
};
|
|
45
44
|
/**
|
|
46
45
|
* 使用科学计数法移动十进制位。
|
|
@@ -68,7 +67,7 @@ function clamp(value, minimum, maximum) {
|
|
|
68
67
|
assertNotNaN(value, "value");
|
|
69
68
|
assertNotNaN(minimum, "minimum");
|
|
70
69
|
assertNotNaN(maximum, "maximum");
|
|
71
|
-
if (minimum > maximum) throw new RangeError("minimum
|
|
70
|
+
if (minimum > maximum) throw new RangeError("`minimum` 不能大于 `maximum`。");
|
|
72
71
|
return Math.min(Math.max(value, minimum), maximum);
|
|
73
72
|
}
|
|
74
73
|
/**
|
|
@@ -85,7 +84,7 @@ function inRange(value, minimum, maximum, includeMaximum = false) {
|
|
|
85
84
|
assertNotNaN(value, "value");
|
|
86
85
|
assertNotNaN(minimum, "minimum");
|
|
87
86
|
assertNotNaN(maximum, "maximum");
|
|
88
|
-
if (minimum > maximum) throw new RangeError("minimum
|
|
87
|
+
if (minimum > maximum) throw new RangeError("`minimum` 不能大于 `maximum`。");
|
|
89
88
|
return value >= minimum && (includeMaximum ? value <= maximum : value < maximum);
|
|
90
89
|
}
|
|
91
90
|
/**
|
|
@@ -99,7 +98,7 @@ function inRange(value, minimum, maximum, includeMaximum = false) {
|
|
|
99
98
|
*/
|
|
100
99
|
function roundTo(value, digits = 0) {
|
|
101
100
|
assertFinite(value, "value");
|
|
102
|
-
if (!Number.isSafeInteger(digits) || digits < -15 || digits > 15) throw new RangeError("digits
|
|
101
|
+
if (!Number.isSafeInteger(digits) || digits < -15 || digits > 15) throw new RangeError("`digits` 必须是 -15 到 15 之间的安全整数。");
|
|
103
102
|
const shifted = shiftDecimal(value, digits);
|
|
104
103
|
if (!Number.isFinite(shifted)) return value;
|
|
105
104
|
return shiftDecimal(Math.round(shifted), -digits);
|
|
@@ -118,7 +117,7 @@ function sum(values) {
|
|
|
118
117
|
assertFinite(value, "value");
|
|
119
118
|
const adjusted = value - compensation;
|
|
120
119
|
const next = total + adjusted;
|
|
121
|
-
if (!Number.isFinite(next)) throw new RangeError("
|
|
120
|
+
if (!Number.isFinite(next)) throw new RangeError("总和超出有限数范围。");
|
|
122
121
|
compensation = next - total - adjusted;
|
|
123
122
|
total = next;
|
|
124
123
|
});
|
|
@@ -156,7 +155,7 @@ function lerp(start, end, amount) {
|
|
|
156
155
|
assertFinite(end, "end");
|
|
157
156
|
assertFinite(amount, "amount");
|
|
158
157
|
const result = start * (1 - amount) + end * amount;
|
|
159
|
-
if (!Number.isFinite(result)) throw new RangeError("
|
|
158
|
+
if (!Number.isFinite(result)) throw new RangeError("插值结果超出有限数范围。");
|
|
160
159
|
return result;
|
|
161
160
|
}
|
|
162
161
|
/**
|
|
@@ -169,12 +168,12 @@ function lerp(start, end, amount) {
|
|
|
169
168
|
*/
|
|
170
169
|
function formatBytes(bytes, options = {}) {
|
|
171
170
|
assertFinite(bytes, "bytes");
|
|
172
|
-
if (bytes < 0) throw new RangeError("bytes
|
|
171
|
+
if (bytes < 0) throw new RangeError("`bytes` 不能为负数。");
|
|
173
172
|
const requestedBase = options.base ?? 1024;
|
|
174
|
-
if (requestedBase !== 1e3 && requestedBase !== 1024) throw new RangeError("base
|
|
173
|
+
if (requestedBase !== 1e3 && requestedBase !== 1024) throw new RangeError("`base` 必须是 1000 或 1024。");
|
|
175
174
|
const base = requestedBase;
|
|
176
175
|
const decimals = options.decimals ?? 2;
|
|
177
|
-
if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 20) throw new RangeError("decimals
|
|
176
|
+
if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 20) throw new RangeError("`decimals` 必须是 0 到 20 之间的安全整数。");
|
|
178
177
|
if (bytes === 0) return "0 B";
|
|
179
178
|
const units = base === 1024 ? binaryByteUnits : byteUnits;
|
|
180
179
|
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(base)), units.length - 1);
|
|
@@ -186,30 +185,32 @@ function formatBytes(bytes, options = {}) {
|
|
|
186
185
|
}).format(value)} ${units[exponent] ?? units.at(-1)}`;
|
|
187
186
|
}
|
|
188
187
|
/**
|
|
189
|
-
*
|
|
188
|
+
* 在半开区间内生成随机整数。
|
|
190
189
|
*
|
|
190
|
+
* @remarks 优先使用 Web Crypto;平台缺少安全随机能力时回退到 `Math.random()`。
|
|
191
191
|
* @param minimum - 包含的安全整数下界。
|
|
192
192
|
* @param maximumExclusive - 不包含的安全整数上界;区间宽度最大为 2^32。
|
|
193
|
-
* @returns
|
|
194
|
-
* @throws
|
|
193
|
+
* @returns 位于 `[minimum, maximumExclusive)` 的随机整数。
|
|
194
|
+
* @throws 参数非法时抛出 `RangeError`。
|
|
195
195
|
*/
|
|
196
|
-
function
|
|
197
|
-
if (!Number.isSafeInteger(minimum) || !Number.isSafeInteger(maximumExclusive)) throw new RangeError("minimum
|
|
196
|
+
function randomInt(minimum, maximumExclusive) {
|
|
197
|
+
if (!Number.isSafeInteger(minimum) || !Number.isSafeInteger(maximumExclusive)) throw new RangeError("`minimum` 和 `maximumExclusive` 必须是安全整数。");
|
|
198
198
|
const range = maximumExclusive - minimum;
|
|
199
199
|
const uint32Range = 4294967296;
|
|
200
|
-
if (range <= 0 || range > uint32Range) throw new RangeError("
|
|
201
|
-
const crypto =
|
|
202
|
-
|
|
200
|
+
if (range <= 0 || range > uint32Range) throw new RangeError("区间不能为空且宽度不能超过 2^32。");
|
|
201
|
+
const crypto = globalThis.crypto;
|
|
202
|
+
const hasWebCrypto = typeof crypto?.getRandomValues === "function";
|
|
203
203
|
const limit = Math.floor(uint32Range / range) * range;
|
|
204
204
|
const values = /* @__PURE__ */ new Uint32Array(1);
|
|
205
205
|
let sample;
|
|
206
206
|
do {
|
|
207
|
-
crypto.getRandomValues(values);
|
|
207
|
+
if (hasWebCrypto) crypto.getRandomValues(values);
|
|
208
|
+
else values[0] = Math.floor(Math.random() * uint32Range);
|
|
208
209
|
sample = values[0] ?? uint32Range;
|
|
209
210
|
} while (sample >= limit);
|
|
210
211
|
return minimum + sample % range;
|
|
211
212
|
}
|
|
212
213
|
//#endregion
|
|
213
|
-
export { average, clamp, formatBytes, inRange, lerp,
|
|
214
|
+
export { average, clamp, formatBytes, inRange, lerp, randomInt, roundTo, sum };
|
|
214
215
|
|
|
215
216
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/number/index.ts"],"sourcesContent":["const byteUnits = [\"B\", \"kB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"] as const;\nconst binaryByteUnits = [\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\", \"PiB\", \"EiB\", \"ZiB\", \"YiB\"] as const;\n\n/** 安全随机整数延迟读取的平台全局对象最小视图。 */\ninterface RuntimeNumberGlobals {\n\t/** 可选 Web Crypto 随机填充能力;缺失时安全随机整数明确失败。 */\n\tcrypto?: Partial<Pick<Crypto, \"getRandomValues\">>;\n}\n\nconst runtimeNumberGlobals = globalThis as unknown as RuntimeNumberGlobals;\n\n/** {@link formatBytes} 的格式化选项。 */\nexport interface FormatBytesOptions {\n\t/** 计量基数。`1000` 生成 SI 单位,`1024` 生成 IEC 单位;默认 `1024`。 */\n\tbase?: 1000 | 1024;\n\t/** 小数位数,范围 0 至 20;默认 `2`。 */\n\tdecimals?: number;\n\t/** `Intl.NumberFormat` 使用的语言;默认固定为 `en-US` 以保证输出稳定。 */\n\tlocale?: string | readonly string[];\n}\n\n/**\n * 拒绝 NaN,同时允许具体 API 自行决定是否接受 Infinity。\n *\n * @param value - 待校验数值。\n * @param name - 用于错误消息的参数名称。\n * @throws `RangeError` 当值为 NaN。\n */\nconst assertNotNaN = (value: number, name: string): void => {\n\tif (Number.isNaN(value)) throw new RangeError(`${name} cannot be NaN.`);\n};\n\n/**\n * 校验有限数值。\n *\n * @param value - 待校验数值。\n * @param name - 用于错误消息的参数名称。\n * @throws `RangeError` 当值为 NaN 或无穷大。\n */\nconst assertFinite = (value: number, name: string): void => {\n\tif (!Number.isFinite(value)) throw new RangeError(`${name} must be finite.`);\n};\n\n/**\n * 使用科学计数法移动十进制位。\n *\n * @remarks 该方式用于 `roundTo`,可减少直接乘除十次幂造成的额外二进制舍入误差。\n * @param value - 原始数值。\n * @param exponent - 十进制移动位数;正数向右移动。\n * @returns 移动后的数值。\n */\nconst shiftDecimal = (value: number, exponent: number): number => {\n\tif (Object.is(value, -0)) return -0;\n\tconst [coefficient = \"0\", currentExponent = \"0\"] = value.toString().split(\"e\");\n\treturn Number(`${coefficient}e${Number(currentExponent) + exponent}`);\n};\n\n/**\n * 把数字限制在闭区间内。\n *\n * @param value - 需要限制的数字。\n * @param minimum - 闭区间下界。\n * @param maximum - 闭区间上界。\n * @returns `minimum <= result <= maximum` 的值。\n * @throws `RangeError` 当参数为 `NaN` 或下界大于上界。\n */\nexport function clamp(value: number, minimum: number, maximum: number): number {\n\tassertNotNaN(value, \"value\");\n\tassertNotNaN(minimum, \"minimum\");\n\tassertNotNaN(maximum, \"maximum\");\n\tif (minimum > maximum) throw new RangeError(\"minimum cannot be greater than maximum.\");\n\treturn Math.min(Math.max(value, minimum), maximum);\n}\n\n/**\n * 判断数字是否位于指定区间。\n *\n * @param value - 待检查数字。\n * @param minimum - 包含的下界。\n * @param maximum - 上界。\n * @param includeMaximum - 是否包含上界;默认使用半开区间 `[minimum, maximum)`。\n * @returns 数字满足区间边界时返回 `true`。\n * @throws `RangeError` 当参数为 `NaN` 或下界大于上界。\n */\nexport function inRange(value: number, minimum: number, maximum: number, includeMaximum = false): boolean {\n\tassertNotNaN(value, \"value\");\n\tassertNotNaN(minimum, \"minimum\");\n\tassertNotNaN(maximum, \"maximum\");\n\tif (minimum > maximum) throw new RangeError(\"minimum cannot be greater than maximum.\");\n\treturn value >= minimum && (includeMaximum ? value <= maximum : value < maximum);\n}\n\n/**\n * 按十进制位数四舍五入。\n *\n * @remarks IEEE-754 浮点数仍可能存在不可表示误差;财务金额应使用十进制定点方案。\n * @param value - 有限数字。\n * @param digits - 小数位数;负数表示十位、百位等,范围 -15 至 15。\n * @returns 按 `Math.round` 语义舍入后的数字。\n * @throws `RangeError` 当值非有限或位数超出范围。\n */\nexport function roundTo(value: number, digits = 0): number {\n\tassertFinite(value, \"value\");\n\tif (!Number.isSafeInteger(digits) || digits < -15 || digits > 15) {\n\t\tthrow new RangeError(\"digits must be a safe integer between -15 and 15.\");\n\t}\n\tconst shifted = shiftDecimal(value, digits);\n\t// 对已经没有可表示小数的大数,乘以 10^digits 可能溢出;此时舍入不会改变值。\n\tif (!Number.isFinite(shifted)) return value;\n\treturn shiftDecimal(Math.round(shifted), -digits);\n}\n\n/**\n * 对有限数字求和。\n *\n * @param values - 不会被修改的数字数组。\n * @returns 算术和;空数组返回 `0`。\n * @throws `RangeError` 当任一值非有限或累计结果溢出。\n */\nexport function sum(values: readonly number[]): number {\n\tlet total = 0;\n\tlet compensation = 0;\n\tvalues.forEach((value) => {\n\t\tassertFinite(value, \"value\");\n\t\t// Kahan 补偿保存上一次浮点加法丢失的低位,减少大量小数累计误差。\n\t\tconst adjusted = value - compensation;\n\t\tconst next = total + adjusted;\n\t\tif (!Number.isFinite(next)) throw new RangeError(\"The sum exceeds the finite number range.\");\n\t\tcompensation = next - total - adjusted;\n\t\ttotal = next;\n\t});\n\treturn total;\n}\n\n/**\n * 计算有限数字的算术平均值。\n *\n * @param values - 不会被修改的数字数组。\n * @returns 空数组或只有稀疏空位的数组返回 `undefined`;空位不参与分母。\n * @throws `RangeError` 当任一值非有限。\n */\nexport function average(values: readonly number[]): number | undefined {\n\tlet count = 0;\n\tlet mean = 0;\n\tvalues.forEach((value) => {\n\t\tassertFinite(value, \"value\");\n\t\tcount += 1;\n\t\t// 加权增量形式避免先求和导致 MAX_VALUE + MAX_VALUE 溢出。\n\t\tmean = mean * ((count - 1) / count) + value / count;\n\t});\n\treturn count === 0 ? undefined : mean;\n}\n\n/**\n * 在两个数字间做线性插值。\n *\n * @remarks `amount` 不限制在 0 至 1;区间外的值会执行线性外推。\n * @param start - `amount = 0` 时的起点。\n * @param end - `amount = 1` 时的终点。\n * @param amount - 插值或外推比例。\n * @returns 线性计算结果。\n * @throws `RangeError` 当任一参数非有限或结果超出有限数字范围。\n */\nexport function lerp(start: number, end: number, amount: number): number {\n\tassertFinite(start, \"start\");\n\tassertFinite(end, \"end\");\n\tassertFinite(amount, \"amount\");\n\tconst result = start * (1 - amount) + end * amount;\n\tif (!Number.isFinite(result)) throw new RangeError(\"The interpolation result exceeds the finite number range.\");\n\treturn result;\n}\n\n/**\n * 将非负字节数格式化为 SI 或 IEC 单位。\n *\n * @param bytes - 非负有限字节数。\n * @param options - 基数、小数位和语言选项。\n * @returns 例如 `1.5 KiB`。\n * @throws `RangeError` 当字节数为负或非有限、基数不是 1000/1024、小数位非法,或 Locale 无效。\n */\nexport function formatBytes(bytes: number, options: FormatBytesOptions = {}): string {\n\tassertFinite(bytes, \"bytes\");\n\tif (bytes < 0) throw new RangeError(\"bytes cannot be negative.\");\n\tconst requestedBase: unknown = options.base ?? 1024;\n\tif (requestedBase !== 1000 && requestedBase !== 1024) throw new RangeError(\"base must be 1000 or 1024.\");\n\tconst base = requestedBase;\n\tconst decimals = options.decimals ?? 2;\n\tif (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 20) {\n\t\tthrow new RangeError(\"decimals must be a safe integer between 0 and 20.\");\n\t}\n\tif (bytes === 0) return \"0 B\";\n\n\tconst units = base === 1024 ? binaryByteUnits : byteUnits;\n\tconst exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(base)), units.length - 1);\n\tconst value = bytes / base ** exponent;\n\tconst formatted = new Intl.NumberFormat(options.locale ?? \"en-US\", {\n\t\tmaximumFractionDigits: decimals,\n\t\tminimumFractionDigits: 0,\n\t\tuseGrouping: false,\n\t}).format(value);\n\treturn `${formatted} ${units[exponent] ?? units.at(-1)}`;\n}\n\n/**\n * 使用 Web Crypto 在半开区间内生成无偏安全随机整数。\n *\n * @param minimum - 包含的安全整数下界。\n * @param maximumExclusive - 不包含的安全整数上界;区间宽度最大为 2^32。\n * @returns 均匀分布在 `[minimum, maximumExclusive)` 的安全整数。\n * @throws 缺少 Web Crypto 时抛出 `Error`;参数非法时抛出 `RangeError`。\n */\nexport function secureRandomInt(minimum: number, maximumExclusive: number): number {\n\tif (!Number.isSafeInteger(minimum) || !Number.isSafeInteger(maximumExclusive)) {\n\t\tthrow new RangeError(\"minimum and maximumExclusive must be safe integers.\");\n\t}\n\tconst range = maximumExclusive - minimum;\n\tconst uint32Range = 0x1_0000_0000;\n\tif (range <= 0 || range > uint32Range) {\n\t\tthrow new RangeError(\"The interval must be non-empty and no wider than 2^32.\");\n\t}\n\tconst crypto = runtimeNumberGlobals.crypto;\n\tif (typeof crypto?.getRandomValues !== \"function\") {\n\t\tthrow new Error(\"Web Crypto random generation is unavailable in the current runtime.\");\n\t}\n\n\t// 只接受可以被区间宽度整除的最大 2^32 前缀,消除取模偏差。\n\tconst limit = Math.floor(uint32Range / range) * range;\n\tconst values = new Uint32Array(1);\n\tlet sample: number;\n\tdo {\n\t\tcrypto.getRandomValues(values);\n\t\tsample = values[0] ?? uint32Range;\n\t} while (sample >= limit);\n\treturn minimum + (sample % range);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/number/index.ts"],"sourcesContent":["const byteUnits = [\"B\", \"kB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"] as const;\nconst binaryByteUnits = [\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\", \"PiB\", \"EiB\", \"ZiB\", \"YiB\"] as const;\n\n/** {@link formatBytes} 的格式化选项。 */\nexport interface FormatBytesOptions {\n\t/** 计量基数。`1000` 生成 SI 单位,`1024` 生成 IEC 单位;默认 `1024`。 */\n\tbase?: 1000 | 1024;\n\t/** 小数位数,范围 0 至 20;默认 `2`。 */\n\tdecimals?: number;\n\t/** `Intl.NumberFormat` 使用的语言;默认固定为 `en-US` 以保证输出稳定。 */\n\tlocale?: string | readonly string[];\n}\n\n/**\n * 拒绝 NaN,同时允许具体 API 自行决定是否接受 Infinity。\n *\n * @param value - 待校验数值。\n * @param name - 用于错误消息的参数名称。\n * @throws `RangeError` 当值为 NaN。\n */\nconst assertNotNaN = (value: number, name: string): void => {\n\tif (Number.isNaN(value)) throw new RangeError(`\\`${name}\\` 不能是 NaN。`);\n};\n\n/**\n * 校验有限数值。\n *\n * @param value - 待校验数值。\n * @param name - 用于错误消息的参数名称。\n * @throws `RangeError` 当值为 NaN 或无穷大。\n */\nconst assertFinite = (value: number, name: string): void => {\n\tif (!Number.isFinite(value)) throw new RangeError(`\\`${name}\\` 必须是有限数。`);\n};\n\n/**\n * 使用科学计数法移动十进制位。\n *\n * @remarks 该方式用于 `roundTo`,可减少直接乘除十次幂造成的额外二进制舍入误差。\n * @param value - 原始数值。\n * @param exponent - 十进制移动位数;正数向右移动。\n * @returns 移动后的数值。\n */\nconst shiftDecimal = (value: number, exponent: number): number => {\n\tif (Object.is(value, -0)) return -0;\n\tconst [coefficient = \"0\", currentExponent = \"0\"] = value.toString().split(\"e\");\n\treturn Number(`${coefficient}e${Number(currentExponent) + exponent}`);\n};\n\n/**\n * 把数字限制在闭区间内。\n *\n * @param value - 需要限制的数字。\n * @param minimum - 闭区间下界。\n * @param maximum - 闭区间上界。\n * @returns `minimum <= result <= maximum` 的值。\n * @throws `RangeError` 当参数为 `NaN` 或下界大于上界。\n */\nexport function clamp(value: number, minimum: number, maximum: number): number {\n\tassertNotNaN(value, \"value\");\n\tassertNotNaN(minimum, \"minimum\");\n\tassertNotNaN(maximum, \"maximum\");\n\tif (minimum > maximum) throw new RangeError(\"`minimum` 不能大于 `maximum`。\");\n\treturn Math.min(Math.max(value, minimum), maximum);\n}\n\n/**\n * 判断数字是否位于指定区间。\n *\n * @param value - 待检查数字。\n * @param minimum - 包含的下界。\n * @param maximum - 上界。\n * @param includeMaximum - 是否包含上界;默认使用半开区间 `[minimum, maximum)`。\n * @returns 数字满足区间边界时返回 `true`。\n * @throws `RangeError` 当参数为 `NaN` 或下界大于上界。\n */\nexport function inRange(value: number, minimum: number, maximum: number, includeMaximum = false): boolean {\n\tassertNotNaN(value, \"value\");\n\tassertNotNaN(minimum, \"minimum\");\n\tassertNotNaN(maximum, \"maximum\");\n\tif (minimum > maximum) throw new RangeError(\"`minimum` 不能大于 `maximum`。\");\n\treturn value >= minimum && (includeMaximum ? value <= maximum : value < maximum);\n}\n\n/**\n * 按十进制位数四舍五入。\n *\n * @remarks IEEE-754 浮点数仍可能存在不可表示误差;财务金额应使用十进制定点方案。\n * @param value - 有限数字。\n * @param digits - 小数位数;负数表示十位、百位等,范围 -15 至 15。\n * @returns 按 `Math.round` 语义舍入后的数字。\n * @throws `RangeError` 当值非有限或位数超出范围。\n */\nexport function roundTo(value: number, digits = 0): number {\n\tassertFinite(value, \"value\");\n\tif (!Number.isSafeInteger(digits) || digits < -15 || digits > 15) {\n\t\tthrow new RangeError(\"`digits` 必须是 -15 到 15 之间的安全整数。\");\n\t}\n\tconst shifted = shiftDecimal(value, digits);\n\t// 对已经没有可表示小数的大数,乘以 10^digits 可能溢出;此时舍入不会改变值。\n\tif (!Number.isFinite(shifted)) return value;\n\treturn shiftDecimal(Math.round(shifted), -digits);\n}\n\n/**\n * 对有限数字求和。\n *\n * @param values - 不会被修改的数字数组。\n * @returns 算术和;空数组返回 `0`。\n * @throws `RangeError` 当任一值非有限或累计结果溢出。\n */\nexport function sum(values: readonly number[]): number {\n\tlet total = 0;\n\tlet compensation = 0;\n\tvalues.forEach((value) => {\n\t\tassertFinite(value, \"value\");\n\t\t// Kahan 补偿保存上一次浮点加法丢失的低位,减少大量小数累计误差。\n\t\tconst adjusted = value - compensation;\n\t\tconst next = total + adjusted;\n\t\tif (!Number.isFinite(next)) throw new RangeError(\"总和超出有限数范围。\");\n\t\tcompensation = next - total - adjusted;\n\t\ttotal = next;\n\t});\n\treturn total;\n}\n\n/**\n * 计算有限数字的算术平均值。\n *\n * @param values - 不会被修改的数字数组。\n * @returns 空数组或只有稀疏空位的数组返回 `undefined`;空位不参与分母。\n * @throws `RangeError` 当任一值非有限。\n */\nexport function average(values: readonly number[]): number | undefined {\n\tlet count = 0;\n\tlet mean = 0;\n\tvalues.forEach((value) => {\n\t\tassertFinite(value, \"value\");\n\t\tcount += 1;\n\t\t// 加权增量形式避免先求和导致 MAX_VALUE + MAX_VALUE 溢出。\n\t\tmean = mean * ((count - 1) / count) + value / count;\n\t});\n\treturn count === 0 ? undefined : mean;\n}\n\n/**\n * 在两个数字间做线性插值。\n *\n * @remarks `amount` 不限制在 0 至 1;区间外的值会执行线性外推。\n * @param start - `amount = 0` 时的起点。\n * @param end - `amount = 1` 时的终点。\n * @param amount - 插值或外推比例。\n * @returns 线性计算结果。\n * @throws `RangeError` 当任一参数非有限或结果超出有限数字范围。\n */\nexport function lerp(start: number, end: number, amount: number): number {\n\tassertFinite(start, \"start\");\n\tassertFinite(end, \"end\");\n\tassertFinite(amount, \"amount\");\n\tconst result = start * (1 - amount) + end * amount;\n\tif (!Number.isFinite(result)) throw new RangeError(\"插值结果超出有限数范围。\");\n\treturn result;\n}\n\n/**\n * 将非负字节数格式化为 SI 或 IEC 单位。\n *\n * @param bytes - 非负有限字节数。\n * @param options - 基数、小数位和语言选项。\n * @returns 例如 `1.5 KiB`。\n * @throws `RangeError` 当字节数为负或非有限、基数不是 1000/1024、小数位非法,或 Locale 无效。\n */\nexport function formatBytes(bytes: number, options: FormatBytesOptions = {}): string {\n\tassertFinite(bytes, \"bytes\");\n\tif (bytes < 0) throw new RangeError(\"`bytes` 不能为负数。\");\n\tconst requestedBase: unknown = options.base ?? 1024;\n\tif (requestedBase !== 1000 && requestedBase !== 1024) throw new RangeError(\"`base` 必须是 1000 或 1024。\");\n\tconst base = requestedBase;\n\tconst decimals = options.decimals ?? 2;\n\tif (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 20) {\n\t\tthrow new RangeError(\"`decimals` 必须是 0 到 20 之间的安全整数。\");\n\t}\n\tif (bytes === 0) return \"0 B\";\n\n\tconst units = base === 1024 ? binaryByteUnits : byteUnits;\n\tconst exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(base)), units.length - 1);\n\tconst value = bytes / base ** exponent;\n\tconst formatted = new Intl.NumberFormat(options.locale ?? \"en-US\", {\n\t\tmaximumFractionDigits: decimals,\n\t\tminimumFractionDigits: 0,\n\t\tuseGrouping: false,\n\t}).format(value);\n\treturn `${formatted} ${units[exponent] ?? units.at(-1)}`;\n}\n\n/**\n * 在半开区间内生成随机整数。\n *\n * @remarks 优先使用 Web Crypto;平台缺少安全随机能力时回退到 `Math.random()`。\n * @param minimum - 包含的安全整数下界。\n * @param maximumExclusive - 不包含的安全整数上界;区间宽度最大为 2^32。\n * @returns 位于 `[minimum, maximumExclusive)` 的随机整数。\n * @throws 参数非法时抛出 `RangeError`。\n */\nexport function randomInt(minimum: number, maximumExclusive: number): number {\n\tif (!Number.isSafeInteger(minimum) || !Number.isSafeInteger(maximumExclusive)) {\n\t\tthrow new RangeError(\"`minimum` 和 `maximumExclusive` 必须是安全整数。\");\n\t}\n\tconst range = maximumExclusive - minimum;\n\tconst uint32Range = 0x1_0000_0000;\n\tif (range <= 0 || range > uint32Range) {\n\t\tthrow new RangeError(\"区间不能为空且宽度不能超过 2^32。\");\n\t}\n\tconst crypto = globalThis.crypto;\n\tconst hasWebCrypto = typeof crypto?.getRandomValues === \"function\";\n\n\t// 只接受可以被区间宽度整除的最大 2^32 前缀,消除取模偏差。\n\tconst limit = Math.floor(uint32Range / range) * range;\n\tconst values = new Uint32Array(1);\n\tlet sample: number;\n\tdo {\n\t\tif (hasWebCrypto) crypto.getRandomValues(values);\n\t\telse values[0] = Math.floor(Math.random() * uint32Range);\n\t\tsample = values[0] ?? uint32Range;\n\t} while (sample >= limit);\n\treturn minimum + (sample % range);\n}\n"],"mappings":"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"}
|
package/dist/object/index.mjs
CHANGED
|
@@ -104,7 +104,7 @@ function shallowEqual(left, right) {
|
|
|
104
104
|
* @throws `RangeError` 当数字不是有限值。
|
|
105
105
|
*/
|
|
106
106
|
const serializeQueryValue = (value) => {
|
|
107
|
-
if (typeof value === "number" && !Number.isFinite(value)) throw new RangeError("
|
|
107
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new RangeError("查询参数中的数字必须是有限数。");
|
|
108
108
|
return String(value);
|
|
109
109
|
};
|
|
110
110
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/object/index.ts"],"sourcesContent":["/** URL 查询参数支持的单值类型。 */\nexport type QueryPrimitive = bigint | boolean | number | string | null | undefined;\n\n/** URL 查询参数值;数组使用重复键表示。 */\nexport type QueryValue = QueryPrimitive | readonly QueryPrimitive[];\n\n/**\n * 判断 Query Value 是否为重复参数数组。\n *\n * @param value - 单值或数组形式的 Query Value。\n * @returns 是只读原始值数组时返回 `true`。\n */\nconst isQueryPrimitiveArray = (value: QueryValue): value is readonly QueryPrimitive[] => Array.isArray(value);\n\n/** {@link toQueryString} 的序列化选项。 */\nexport interface QueryStringOptions {\n\t/** 返回非空结果时是否添加 `?`;默认 `false`。 */\n\tprefixQuestionMark?: boolean;\n\t/** 是否按键的 UTF-16 码元顺序稳定排序;默认保留对象枚举顺序。 */\n\tsort?: boolean;\n\t/** 空格编码方式;默认遵循表单编码并输出 `+`。 */\n\tspace?: \"percent\" | \"plus\";\n}\n\n/**\n * 安全写入结果对象的自有可枚举属性。\n *\n * @remarks 使用 `defineProperty` 避免 `__proto__` 触发 Setter,并显式拒绝三个原型污染键。\n * @param target - 要写入的结果对象。\n * @param key - 自有属性键。\n * @param value - 属性值。\n * @throws `TypeError` 当键为 `__proto__`、`prototype` 或 `constructor`。\n */\nconst defineEnumerableProperty = (target: object, key: PropertyKey, value: unknown): void => {\n\tObject.defineProperty(target, key, { configurable: true, enumerable: true, value, writable: true });\n};\n\n/**\n * 判断值是否是普通对象。\n *\n * @param value - 任意待检查值。\n * @returns 原型为 `Object.prototype` 或 `null` 时返回 `true`。\n */\nexport function isPlainObject(value: unknown): value is Record<PropertyKey, unknown> {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst prototype = Object.getPrototypeOf(value) as object | null;\n\treturn prototype === null || prototype === Object.prototype;\n}\n\n/**\n * 安全判断对象是否拥有自己的属性。\n *\n * @remarks 不调用可能被对象覆盖的 `hasOwnProperty`。\n * @param value - 待检查对象。\n * @param key - 字符串、数字或 Symbol 属性键。\n * @returns 属性为对象自有属性时返回 `true`,并收窄键类型。\n */\nexport function hasOwn<ObjectType extends object, Key extends PropertyKey>(value: ObjectType, key: Key): key is Key & keyof ObjectType {\n\treturn Object.hasOwn(value, key);\n}\n\n/**\n * 从对象中选择指定自有可枚举属性。\n *\n * @param source - 不会被修改的源对象。\n * @param keys - 需要保留的键;不存在的键被忽略。\n * @returns 新对象,保持 `keys` 的遍历顺序。\n */\nexport function pick<Source extends object, Keys extends readonly (keyof Source)[]>(source: Source, keys: Keys): Pick<Source, Keys[number]> {\n\tconst result = {} as Pick<Source, Keys[number]>;\n\tfor (const key of keys) {\n\t\tif (Object.prototype.propertyIsEnumerable.call(source, key)) defineEnumerableProperty(result, key, source[key]);\n\t}\n\treturn result;\n}\n\n/**\n * 浅复制对象并删除指定属性。\n *\n * @param source - 不会被修改的源对象。\n * @param keys - 需要排除的键。\n * @returns 包含其余自有可枚举字符串与 Symbol 属性的新对象。\n */\nexport function omit<Source extends object, Keys extends readonly (keyof Source)[]>(source: Source, keys: Keys): Omit<Source, Keys[number]> {\n\tconst result = { ...source };\n\tfor (const key of keys) Reflect.deleteProperty(result, key);\n\treturn result;\n}\n\n/**\n * 映射对象的自有可枚举属性值。\n *\n * @param source - 不会被修改的源对象。\n * @param mapper - 接收值、键和源对象的映射函数。\n * @returns 保留原键的新对象。\n */\nexport function mapValues<Source extends object, Result>(\n\tsource: Source,\n\tmapper: (value: Source[keyof Source], key: keyof Source, source: Source) => Result\n): { [Key in keyof Source]: Result } {\n\tconst result = {} as { [Key in keyof Source]: Result };\n\tfor (const key of Reflect.ownKeys(source) as (keyof Source)[]) {\n\t\tif (Object.prototype.propertyIsEnumerable.call(source, key)) defineEnumerableProperty(result, key, mapper(source[key], key, source));\n\t}\n\treturn result;\n}\n\n/**\n * 对自有可枚举属性执行 SameValue 浅比较。\n *\n * @remarks 嵌套对象只比较引用;`NaN` 相等,`0` 与 `-0` 不相等。\n * @param left - 第一对象。\n * @param right - 第二对象。\n * @returns 自有可枚举键集合与对应值均满足 SameValue 时返回 `true`。\n */\nexport function shallowEqual(left: object, right: object): boolean {\n\tif (Object.is(left, right)) return true;\n\tconst leftKeys = Reflect.ownKeys(left).filter((key) => Object.prototype.propertyIsEnumerable.call(left, key));\n\tconst rightKeys = Reflect.ownKeys(right).filter((key) => Object.prototype.propertyIsEnumerable.call(right, key));\n\tif (leftKeys.length !== rightKeys.length) return false;\n\treturn leftKeys.every((key) => Object.hasOwn(right, key) && Object.is(Reflect.get(left, key), Reflect.get(right, key)));\n}\n\n/**\n * 把 Query 原始值规范化为文本。\n *\n * @param value - 已排除空值的字符串、数字、布尔值或 BigInt。\n * @returns 与 URLSearchParams 兼容的文本值。\n * @throws `RangeError` 当数字不是有限值。\n */\nconst serializeQueryValue = (value: Exclude<QueryPrimitive, null | undefined>): string => {\n\tif (typeof value === \"number\" && !Number.isFinite(value)) {\n\t\tthrow new RangeError(\"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/object/index.ts"],"sourcesContent":["/** URL 查询参数支持的单值类型。 */\nexport type QueryPrimitive = bigint | boolean | number | string | null | undefined;\n\n/** URL 查询参数值;数组使用重复键表示。 */\nexport type QueryValue = QueryPrimitive | readonly QueryPrimitive[];\n\n/**\n * 判断 Query Value 是否为重复参数数组。\n *\n * @param value - 单值或数组形式的 Query Value。\n * @returns 是只读原始值数组时返回 `true`。\n */\nconst isQueryPrimitiveArray = (value: QueryValue): value is readonly QueryPrimitive[] => Array.isArray(value);\n\n/** {@link toQueryString} 的序列化选项。 */\nexport interface QueryStringOptions {\n\t/** 返回非空结果时是否添加 `?`;默认 `false`。 */\n\tprefixQuestionMark?: boolean;\n\t/** 是否按键的 UTF-16 码元顺序稳定排序;默认保留对象枚举顺序。 */\n\tsort?: boolean;\n\t/** 空格编码方式;默认遵循表单编码并输出 `+`。 */\n\tspace?: \"percent\" | \"plus\";\n}\n\n/**\n * 安全写入结果对象的自有可枚举属性。\n *\n * @remarks 使用 `defineProperty` 避免 `__proto__` 触发 Setter,并显式拒绝三个原型污染键。\n * @param target - 要写入的结果对象。\n * @param key - 自有属性键。\n * @param value - 属性值。\n * @throws `TypeError` 当键为 `__proto__`、`prototype` 或 `constructor`。\n */\nconst defineEnumerableProperty = (target: object, key: PropertyKey, value: unknown): void => {\n\tObject.defineProperty(target, key, { configurable: true, enumerable: true, value, writable: true });\n};\n\n/**\n * 判断值是否是普通对象。\n *\n * @param value - 任意待检查值。\n * @returns 原型为 `Object.prototype` 或 `null` 时返回 `true`。\n */\nexport function isPlainObject(value: unknown): value is Record<PropertyKey, unknown> {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst prototype = Object.getPrototypeOf(value) as object | null;\n\treturn prototype === null || prototype === Object.prototype;\n}\n\n/**\n * 安全判断对象是否拥有自己的属性。\n *\n * @remarks 不调用可能被对象覆盖的 `hasOwnProperty`。\n * @param value - 待检查对象。\n * @param key - 字符串、数字或 Symbol 属性键。\n * @returns 属性为对象自有属性时返回 `true`,并收窄键类型。\n */\nexport function hasOwn<ObjectType extends object, Key extends PropertyKey>(value: ObjectType, key: Key): key is Key & keyof ObjectType {\n\treturn Object.hasOwn(value, key);\n}\n\n/**\n * 从对象中选择指定自有可枚举属性。\n *\n * @param source - 不会被修改的源对象。\n * @param keys - 需要保留的键;不存在的键被忽略。\n * @returns 新对象,保持 `keys` 的遍历顺序。\n */\nexport function pick<Source extends object, Keys extends readonly (keyof Source)[]>(source: Source, keys: Keys): Pick<Source, Keys[number]> {\n\tconst result = {} as Pick<Source, Keys[number]>;\n\tfor (const key of keys) {\n\t\tif (Object.prototype.propertyIsEnumerable.call(source, key)) defineEnumerableProperty(result, key, source[key]);\n\t}\n\treturn result;\n}\n\n/**\n * 浅复制对象并删除指定属性。\n *\n * @param source - 不会被修改的源对象。\n * @param keys - 需要排除的键。\n * @returns 包含其余自有可枚举字符串与 Symbol 属性的新对象。\n */\nexport function omit<Source extends object, Keys extends readonly (keyof Source)[]>(source: Source, keys: Keys): Omit<Source, Keys[number]> {\n\tconst result = { ...source };\n\tfor (const key of keys) Reflect.deleteProperty(result, key);\n\treturn result;\n}\n\n/**\n * 映射对象的自有可枚举属性值。\n *\n * @param source - 不会被修改的源对象。\n * @param mapper - 接收值、键和源对象的映射函数。\n * @returns 保留原键的新对象。\n */\nexport function mapValues<Source extends object, Result>(\n\tsource: Source,\n\tmapper: (value: Source[keyof Source], key: keyof Source, source: Source) => Result\n): { [Key in keyof Source]: Result } {\n\tconst result = {} as { [Key in keyof Source]: Result };\n\tfor (const key of Reflect.ownKeys(source) as (keyof Source)[]) {\n\t\tif (Object.prototype.propertyIsEnumerable.call(source, key)) defineEnumerableProperty(result, key, mapper(source[key], key, source));\n\t}\n\treturn result;\n}\n\n/**\n * 对自有可枚举属性执行 SameValue 浅比较。\n *\n * @remarks 嵌套对象只比较引用;`NaN` 相等,`0` 与 `-0` 不相等。\n * @param left - 第一对象。\n * @param right - 第二对象。\n * @returns 自有可枚举键集合与对应值均满足 SameValue 时返回 `true`。\n */\nexport function shallowEqual(left: object, right: object): boolean {\n\tif (Object.is(left, right)) return true;\n\tconst leftKeys = Reflect.ownKeys(left).filter((key) => Object.prototype.propertyIsEnumerable.call(left, key));\n\tconst rightKeys = Reflect.ownKeys(right).filter((key) => Object.prototype.propertyIsEnumerable.call(right, key));\n\tif (leftKeys.length !== rightKeys.length) return false;\n\treturn leftKeys.every((key) => Object.hasOwn(right, key) && Object.is(Reflect.get(left, key), Reflect.get(right, key)));\n}\n\n/**\n * 把 Query 原始值规范化为文本。\n *\n * @param value - 已排除空值的字符串、数字、布尔值或 BigInt。\n * @returns 与 URLSearchParams 兼容的文本值。\n * @throws `RangeError` 当数字不是有限值。\n */\nconst serializeQueryValue = (value: Exclude<QueryPrimitive, null | undefined>): string => {\n\tif (typeof value === \"number\" && !Number.isFinite(value)) {\n\t\tthrow new RangeError(\"查询参数中的数字必须是有限数。\");\n\t}\n\treturn String(value);\n};\n\n/**\n * 将对象序列化为标准 URL 查询字符串。\n *\n * @remarks `null` 与 `undefined` 被跳过;数组使用重复键;返回值不会修改输入。\n * @param value - 查询参数对象。\n * @param options - 排序、空格和问号前缀选项。\n * @returns URL 编码后的查询字符串;没有参数时始终返回空字符串。\n * @throws `RangeError` 当参数包含 `NaN` 或无穷数字。\n */\nexport function toQueryString(value: Readonly<Record<string, QueryValue>>, options: QueryStringOptions = {}): string {\n\tconst entries = Object.entries(value);\n\tif (options.sort) entries.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));\n\tconst parameters = new URLSearchParams();\n\tfor (const [key, rawValue] of entries) {\n\t\tconst values = isQueryPrimitiveArray(rawValue) ? rawValue : [rawValue];\n\t\tfor (const item of values) {\n\t\t\tif (item !== null && item !== undefined) parameters.append(key, serializeQueryValue(item));\n\t\t}\n\t}\n\tlet result = parameters.toString();\n\tif (options.space === \"percent\") result = result.replace(/\\+/gu, \"%20\");\n\treturn result && options.prefixQuestionMark ? `?${result}` : result;\n}\n"],"mappings":";;;;;;;AAYA,MAAM,yBAAyB,UAA0D,MAAM,QAAQ,KAAK;;;;;;;;;;AAqB5G,MAAM,4BAA4B,QAAgB,KAAkB,UAAyB;CAC5F,OAAO,eAAe,QAAQ,KAAK;EAAE,cAAc;EAAM,YAAY;EAAM;EAAO,UAAU;CAAK,CAAC;AACnG;;;;;;;AAQA,SAAgB,cAAc,OAAuD;CACpF,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,OAAO,cAAc,QAAQ,cAAc,OAAO;AACnD;;;;;;;;;AAUA,SAAgB,OAA2D,OAAmB,KAAyC;CACtI,OAAO,OAAO,OAAO,OAAO,GAAG;AAChC;;;;;;;;AASA,SAAgB,KAAoE,QAAgB,MAAwC;CAC3I,MAAM,SAAS,CAAC;CAChB,KAAK,MAAM,OAAO,MACjB,IAAI,OAAO,UAAU,qBAAqB,KAAK,QAAQ,GAAG,GAAG,yBAAyB,QAAQ,KAAK,OAAO,IAAI;CAE/G,OAAO;AACR;;;;;;;;AASA,SAAgB,KAAoE,QAAgB,MAAwC;CAC3I,MAAM,SAAS,EAAE,GAAG,OAAO;CAC3B,KAAK,MAAM,OAAO,MAAM,QAAQ,eAAe,QAAQ,GAAG;CAC1D,OAAO;AACR;;;;;;;;AASA,SAAgB,UACf,QACA,QACoC;CACpC,MAAM,SAAS,CAAC;CAChB,KAAK,MAAM,OAAO,QAAQ,QAAQ,MAAM,GACvC,IAAI,OAAO,UAAU,qBAAqB,KAAK,QAAQ,GAAG,GAAG,yBAAyB,QAAQ,KAAK,OAAO,OAAO,MAAM,KAAK,MAAM,CAAC;CAEpI,OAAO;AACR;;;;;;;;;AAUA,SAAgB,aAAa,MAAc,OAAwB;CAClE,IAAI,OAAO,GAAG,MAAM,KAAK,GAAG,OAAO;CACnC,MAAM,WAAW,QAAQ,QAAQ,IAAI,CAAC,CAAC,QAAQ,QAAQ,OAAO,UAAU,qBAAqB,KAAK,MAAM,GAAG,CAAC;CAC5G,MAAM,YAAY,QAAQ,QAAQ,KAAK,CAAC,CAAC,QAAQ,QAAQ,OAAO,UAAU,qBAAqB,KAAK,OAAO,GAAG,CAAC;CAC/G,IAAI,SAAS,WAAW,UAAU,QAAQ,OAAO;CACjD,OAAO,SAAS,OAAO,QAAQ,OAAO,OAAO,OAAO,GAAG,KAAK,OAAO,GAAG,QAAQ,IAAI,MAAM,GAAG,GAAG,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AACvH;;;;;;;;AASA,MAAM,uBAAuB,UAA6D;CACzF,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,GACtD,MAAM,IAAI,WAAW,iBAAiB;CAEvC,OAAO,OAAO,KAAK;AACpB;;;;;;;;;;AAWA,SAAgB,cAAc,OAA6C,UAA8B,CAAC,GAAW;CACpH,MAAM,UAAU,OAAO,QAAQ,KAAK;CACpC,IAAI,QAAQ,MAAM,QAAQ,MAAM,CAAC,OAAO,CAAC,WAAY,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAE;CAC9F,MAAM,aAAa,IAAI,gBAAgB;CACvC,KAAK,MAAM,CAAC,KAAK,aAAa,SAAS;EACtC,MAAM,SAAS,sBAAsB,QAAQ,IAAI,WAAW,CAAC,QAAQ;EACrE,KAAK,MAAM,QAAQ,QAClB,IAAI,SAAS,QAAQ,SAAS,KAAA,GAAW,WAAW,OAAO,KAAK,oBAAoB,IAAI,CAAC;CAE3F;CACA,IAAI,SAAS,WAAW,SAAS;CACjC,IAAI,QAAQ,UAAU,WAAW,SAAS,OAAO,QAAQ,QAAQ,KAAK;CACtE,OAAO,UAAU,QAAQ,qBAAqB,IAAI,WAAW;AAC9D"}
|
package/dist/storage/index.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { decodeSecureBase64, encodeSecureBase64 } from "../base64/index.mjs";
|
|
2
2
|
//#region src/storage/index.ts
|
|
3
|
-
const runtimeStorageGlobals = globalThis;
|
|
4
3
|
/**
|
|
5
4
|
* 读取并校验当前运行时的全局 uni-app 同步 Storage。
|
|
6
5
|
*
|
|
@@ -8,11 +7,11 @@ const runtimeStorageGlobals = globalThis;
|
|
|
8
7
|
* @throws `TypeError` 当全局 `uni` 存在但缺少本库需要的同步 Storage 方法。
|
|
9
8
|
*/
|
|
10
9
|
const getGlobalUniStorage = () => {
|
|
11
|
-
const value =
|
|
10
|
+
const value = Reflect.get(globalThis, "uni");
|
|
12
11
|
if (value === void 0) return void 0;
|
|
13
|
-
if (typeof value !== "object" && typeof value !== "function" || value === null) throw new TypeError("
|
|
12
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null) throw new TypeError("全局 uni 对象未提供同步 Storage API。");
|
|
14
13
|
const storage = value;
|
|
15
|
-
if (typeof storage.getStorageSync !== "function" || typeof storage.getStorageInfoSync !== "function" || typeof storage.removeStorageSync !== "function" || typeof storage.setStorageSync !== "function") throw new TypeError("
|
|
14
|
+
if (typeof storage.getStorageSync !== "function" || typeof storage.getStorageInfoSync !== "function" || typeof storage.removeStorageSync !== "function" || typeof storage.setStorageSync !== "function") throw new TypeError("全局 uni 对象未提供同步 Storage API。");
|
|
16
15
|
return storage;
|
|
17
16
|
};
|
|
18
17
|
/** 默认 JSON Codec;显式拒绝会被 JSON.stringify 静默丢弃的顶层值。 */
|
|
@@ -20,7 +19,7 @@ const jsonCodec = {
|
|
|
20
19
|
decode: (value) => JSON.parse(value),
|
|
21
20
|
encode: (value) => {
|
|
22
21
|
const encoded = JSON.stringify(value);
|
|
23
|
-
if (typeof encoded !== "string") throw new TypeError("
|
|
22
|
+
if (typeof encoded !== "string") throw new TypeError("存储值无法序列化为 JSON。");
|
|
24
23
|
return encoded;
|
|
25
24
|
}
|
|
26
25
|
};
|
|
@@ -29,7 +28,7 @@ const base64StorageCodec = {
|
|
|
29
28
|
decode: (value) => JSON.parse(decodeSecureBase64(value)),
|
|
30
29
|
encode: (value) => {
|
|
31
30
|
const encoded = JSON.stringify(value);
|
|
32
|
-
if (typeof encoded !== "string") throw new TypeError("
|
|
31
|
+
if (typeof encoded !== "string") throw new TypeError("存储值无法序列化为 JSON。");
|
|
33
32
|
return encodeSecureBase64(encoded);
|
|
34
33
|
}
|
|
35
34
|
};
|
|
@@ -49,7 +48,7 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
|
|
|
49
48
|
* @throws `TypeError` 当值不是非空字符串。
|
|
50
49
|
*/
|
|
51
50
|
const assertKey = (key) => {
|
|
52
|
-
if (typeof key !== "string" || key.length === 0) throw new TypeError("Storage
|
|
51
|
+
if (typeof key !== "string" || key.length === 0) throw new TypeError("Storage 键必须是非空字符串。");
|
|
53
52
|
};
|
|
54
53
|
/**
|
|
55
54
|
* 创建浏览器 Storage 后端。
|
|
@@ -60,8 +59,8 @@ const assertKey = (key) => {
|
|
|
60
59
|
* @throws `Error` 当所选 Storage 在当前环境不可用。
|
|
61
60
|
*/
|
|
62
61
|
const createWebStorageBackend = (kind) => {
|
|
63
|
-
const storage = kind === "local" ?
|
|
64
|
-
if (storage === void 0) throw new Error(
|
|
62
|
+
const storage = kind === "local" ? globalThis.localStorage : globalThis.sessionStorage;
|
|
63
|
+
if (storage === void 0) throw new Error(`当前运行环境不支持 ${kind}Storage。`);
|
|
65
64
|
return {
|
|
66
65
|
getItem: (key) => storage.getItem(key),
|
|
67
66
|
keys: () => {
|
|
@@ -110,17 +109,17 @@ const createUniStorageBackend = (storage) => ({
|
|
|
110
109
|
* @throws `TypeError` 当原始值不是字符串、JSON 损坏、版本不支持或字段类型非法。
|
|
111
110
|
*/
|
|
112
111
|
const parseStoredEnvelope = (rawValue, key) => {
|
|
113
|
-
if (typeof rawValue !== "string") throw new TypeError(`Storage
|
|
112
|
+
if (typeof rawValue !== "string") throw new TypeError(`Storage 条目“${key}”不是字符串。`);
|
|
114
113
|
try {
|
|
115
114
|
const parsed = JSON.parse(rawValue);
|
|
116
|
-
if (!isRecord(parsed) || parsed["version"] !== 3 || typeof parsed["data"] !== "string" || !(parsed["expiresAt"] === null || typeof parsed["expiresAt"] === "number" && Number.isFinite(parsed["expiresAt"]))) throw new TypeError("
|
|
115
|
+
if (!isRecord(parsed) || parsed["version"] !== 3 || typeof parsed["data"] !== "string" || !(parsed["expiresAt"] === null || typeof parsed["expiresAt"] === "number" && Number.isFinite(parsed["expiresAt"]))) throw new TypeError("不支持该存储包络。");
|
|
117
116
|
return {
|
|
118
117
|
data: parsed["data"],
|
|
119
118
|
expiresAt: parsed["expiresAt"],
|
|
120
119
|
version: 3
|
|
121
120
|
};
|
|
122
121
|
} catch (cause) {
|
|
123
|
-
throw new TypeError(`Storage
|
|
122
|
+
throw new TypeError(`Storage 条目“${key}”已损坏或不受支持。`, { cause });
|
|
124
123
|
}
|
|
125
124
|
};
|
|
126
125
|
/**
|
|
@@ -149,7 +148,7 @@ const createStorageArea = (backendFactory, prefix, codec, now) => {
|
|
|
149
148
|
*/
|
|
150
149
|
const listBusinessKeys = (backend) => {
|
|
151
150
|
const keys = backend.keys();
|
|
152
|
-
if (!Array.isArray(keys) || !keys.every((key) => typeof key === "string")) throw new TypeError("Storage
|
|
151
|
+
if (!Array.isArray(keys) || !keys.every((key) => typeof key === "string")) throw new TypeError("Storage 后端返回的键必须是字符串。");
|
|
153
152
|
return [...new Set(keys.filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length)))].sort();
|
|
154
153
|
};
|
|
155
154
|
/**
|
|
@@ -169,7 +168,7 @@ const createStorageArea = (backendFactory, prefix, codec, now) => {
|
|
|
169
168
|
const envelope = parseStoredEnvelope(rawValue, storageKey);
|
|
170
169
|
if (envelope.expiresAt === null) return envelope;
|
|
171
170
|
const timestamp = now();
|
|
172
|
-
if (!Number.isFinite(timestamp)) throw new RangeError("Storage
|
|
171
|
+
if (!Number.isFinite(timestamp)) throw new RangeError("Storage 时钟必须返回有限时间戳。");
|
|
173
172
|
if (timestamp < envelope.expiresAt) return envelope;
|
|
174
173
|
backend.removeItem(storageKey);
|
|
175
174
|
};
|
|
@@ -185,7 +184,7 @@ const createStorageArea = (backendFactory, prefix, codec, now) => {
|
|
|
185
184
|
try {
|
|
186
185
|
return codec.decode(envelope.data);
|
|
187
186
|
} catch (cause) {
|
|
188
|
-
throw new TypeError(
|
|
187
|
+
throw new TypeError(`无法解码 Storage 条目“${toStorageKey(key)}”。`, { cause });
|
|
189
188
|
}
|
|
190
189
|
},
|
|
191
190
|
has: (key) => readStoredEnvelope(backendFactory(), key) !== void 0,
|
|
@@ -210,20 +209,20 @@ const createStorageArea = (backendFactory, prefix, codec, now) => {
|
|
|
210
209
|
},
|
|
211
210
|
set(key, value, options = {}) {
|
|
212
211
|
assertKey(key);
|
|
213
|
-
if (value === void 0) throw new TypeError("
|
|
212
|
+
if (value === void 0) throw new TypeError("不能存储顶层 `undefined`,请改为移除对应的键。");
|
|
214
213
|
let expiresAt = null;
|
|
215
214
|
if (options.ttlMs !== void 0) {
|
|
216
|
-
if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) throw new RangeError("ttlMs
|
|
215
|
+
if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) throw new RangeError("`ttlMs` 必须是大于 0 的有限数。");
|
|
217
216
|
const timestamp = now();
|
|
218
|
-
if (!Number.isFinite(timestamp) || !Number.isFinite(timestamp + options.ttlMs)) throw new RangeError("Storage
|
|
217
|
+
if (!Number.isFinite(timestamp) || !Number.isFinite(timestamp + options.ttlMs)) throw new RangeError("Storage 过期时间超出支持的时间戳范围。");
|
|
219
218
|
expiresAt = timestamp + options.ttlMs;
|
|
220
219
|
}
|
|
221
220
|
let data;
|
|
222
221
|
try {
|
|
223
222
|
data = codec.encode(value);
|
|
224
|
-
if (typeof data !== "string") throw new TypeError("Storage
|
|
223
|
+
if (typeof data !== "string") throw new TypeError("Storage Codec 必须返回字符串。");
|
|
225
224
|
} catch (cause) {
|
|
226
|
-
throw new TypeError("
|
|
225
|
+
throw new TypeError("无法编码存储值。", { cause });
|
|
227
226
|
}
|
|
228
227
|
backendFactory().setItem(toStorageKey(key), JSON.stringify({
|
|
229
228
|
data,
|
|
@@ -240,7 +239,7 @@ const createStorageArea = (backendFactory, prefix, codec, now) => {
|
|
|
240
239
|
*/
|
|
241
240
|
const requireStorageConfiguration = () => {
|
|
242
241
|
if (activeConfiguration === void 0) configureStorage();
|
|
243
|
-
if (activeConfiguration === void 0) throw new Error("Storage
|
|
242
|
+
if (activeConfiguration === void 0) throw new Error("无法初始化 Storage 配置。");
|
|
244
243
|
return activeConfiguration;
|
|
245
244
|
};
|
|
246
245
|
/**
|
|
@@ -259,7 +258,7 @@ const createStorageAreaProxy = (select, name) => {
|
|
|
259
258
|
*/
|
|
260
259
|
const getArea = () => {
|
|
261
260
|
const area = select(requireStorageConfiguration());
|
|
262
|
-
if (area === void 0) throw new Error(
|
|
261
|
+
if (area === void 0) throw new Error(`uni-app 中不支持 ${name}。`);
|
|
263
262
|
return area;
|
|
264
263
|
};
|
|
265
264
|
return {
|
|
@@ -299,13 +298,13 @@ const Session = createStorageAreaProxy((configuration) => configuration.session,
|
|
|
299
298
|
*/
|
|
300
299
|
function configureStorage(options = {}) {
|
|
301
300
|
const prefix = options.prefix ?? "fast__";
|
|
302
|
-
if (typeof prefix !== "string" || prefix.length === 0) throw new TypeError("Storage
|
|
303
|
-
if (options.codec !== void 0 && options.crypto === true) throw new TypeError("Storage
|
|
301
|
+
if (typeof prefix !== "string" || prefix.length === 0) throw new TypeError("Storage 前缀必须是非空字符串。");
|
|
302
|
+
if (options.codec !== void 0 && options.crypto === true) throw new TypeError("Storage 的 Codec 和加密选项不能同时使用。");
|
|
304
303
|
const codec = options.codec ?? (options.crypto === true ? base64StorageCodec : jsonCodec);
|
|
305
304
|
const now = options.now ?? Date.now;
|
|
306
305
|
if (activeConfiguration !== void 0) {
|
|
307
306
|
if (activeConfiguration.prefix === prefix && activeConfiguration.codec === codec && activeConfiguration.now === now) return;
|
|
308
|
-
throw new Error("Storage
|
|
307
|
+
throw new Error("Storage 已使用其他选项完成配置。");
|
|
309
308
|
}
|
|
310
309
|
const uni = getGlobalUniStorage();
|
|
311
310
|
const configuration = {
|