@anjianshi/utils 1.0.0

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.
Files changed (64) hide show
  1. package/.eslintrc.cjs +6 -0
  2. package/README.md +1 -0
  3. package/build-cleanup.cjs +10 -0
  4. package/env-browser/device.d.ts +24 -0
  5. package/env-browser/device.js +49 -0
  6. package/env-browser/global.d.ts +11 -0
  7. package/env-browser/global.js +14 -0
  8. package/env-browser/load-script.d.ts +5 -0
  9. package/env-browser/load-script.js +13 -0
  10. package/env-browser/logging.d.ts +18 -0
  11. package/env-browser/logging.js +49 -0
  12. package/env-browser/manage-vconsole.d.ts +16 -0
  13. package/env-browser/manage-vconsole.js +38 -0
  14. package/env-node/index.d.ts +17 -0
  15. package/env-node/index.js +45 -0
  16. package/env-node/logging.d.ts +64 -0
  17. package/env-node/logging.js +159 -0
  18. package/index.d.ts +3 -0
  19. package/index.js +3 -0
  20. package/init-dayjs.d.ts +2 -0
  21. package/init-dayjs.js +7 -0
  22. package/lang/async.d.ts +19 -0
  23. package/lang/async.js +34 -0
  24. package/lang/index.d.ts +5 -0
  25. package/lang/index.js +5 -0
  26. package/lang/object.d.ts +5 -0
  27. package/lang/object.js +31 -0
  28. package/lang/string.d.ts +17 -0
  29. package/lang/string.js +59 -0
  30. package/lang/time.d.ts +10 -0
  31. package/lang/time.js +18 -0
  32. package/lang/types.d.ts +98 -0
  33. package/lang/types.js +41 -0
  34. package/logging/adapt.d.ts +5 -0
  35. package/logging/adapt.js +38 -0
  36. package/logging/formatters.d.ts +10 -0
  37. package/logging/formatters.js +22 -0
  38. package/logging/index.d.ts +43 -0
  39. package/logging/index.js +72 -0
  40. package/package.json +43 -0
  41. package/src/.eslintrc.cjs +8 -0
  42. package/src/env-browser/.eslintrc.cjs +6 -0
  43. package/src/env-browser/device.ts +60 -0
  44. package/src/env-browser/global.ts +20 -0
  45. package/src/env-browser/load-script.ts +13 -0
  46. package/src/env-browser/logging.ts +58 -0
  47. package/src/env-browser/manage-vconsole.ts +54 -0
  48. package/src/env-node/.eslintrc.cjs +4 -0
  49. package/src/env-node/index.ts +46 -0
  50. package/src/env-node/logging.ts +202 -0
  51. package/src/index.ts +3 -0
  52. package/src/init-dayjs.ts +8 -0
  53. package/src/lang/async.ts +47 -0
  54. package/src/lang/index.ts +5 -0
  55. package/src/lang/object.ts +28 -0
  56. package/src/lang/string.ts +62 -0
  57. package/src/lang/time.ts +19 -0
  58. package/src/lang/types.ts +138 -0
  59. package/src/logging/adapt.ts +40 -0
  60. package/src/logging/formatters.ts +23 -0
  61. package/src/logging/index.ts +87 -0
  62. package/src/url.ts +153 -0
  63. package/url.d.ts +28 -0
  64. package/url.js +116 -0
@@ -0,0 +1,40 @@
1
+ import type { Debug } from 'debug'
2
+ import { getLogger } from './index.js'
3
+
4
+ /**
5
+ * 适配 debug package
6
+ */
7
+ export function adaptDebugLib(debugLib: Debug, enable = '') {
8
+ // 不在 localStorage 里记录 debugLib enable 状态,
9
+ // 以解决 web worker 里读不到 localStorage 而无法启用 debugLib 日志的问题
10
+ const emulate = {
11
+ storage: {
12
+ data: {} as Record<string, string>,
13
+ getItem(name: string) {
14
+ return emulate.storage.data[name]
15
+ },
16
+ setItem(name: string, value: string) {
17
+ emulate.storage.data[name] = value
18
+ },
19
+ removeItem(name: string) {
20
+ delete emulate.storage.data[name]
21
+ },
22
+ },
23
+ save(namespaces: string) {
24
+ if (namespaces) emulate.storage.setItem('debug', namespaces)
25
+ else emulate.storage.removeItem('debug')
26
+ },
27
+ load() {
28
+ return emulate.storage.getItem('debug')
29
+ },
30
+ }
31
+ Object.assign(debugLib, emulate)
32
+
33
+ // 将 debugLib 日志转发给 logger
34
+ const logger = getLogger('3rd-library')
35
+ debugLib.log = logger.debug.bind(logger)
36
+
37
+ if (enable) {
38
+ debugLib.enable(enable)
39
+ }
40
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 日志常用数据的格式化函数
3
+ */
4
+ import { type LogInfo, LogLevel } from './index.js'
5
+
6
+ const formatters = {
7
+ time(info: LogInfo) {
8
+ return info.time.format('HH:mm:ss.SSS')
9
+ },
10
+ datetime(info: LogInfo) {
11
+ return info.time.format('YY-MM-DD HH:mm:ss.SSS')
12
+ },
13
+ level(info: LogInfo) {
14
+ const map = {
15
+ [LogLevel.Debug]: 'debug',
16
+ [LogLevel.Info]: 'info',
17
+ [LogLevel.Warning]: 'warn',
18
+ [LogLevel.Error]: 'error',
19
+ }
20
+ return map[info.level]
21
+ },
22
+ }
23
+ export default formatters
@@ -0,0 +1,87 @@
1
+ export { default as formatters } from './formatters.js'
2
+ export * from './adapt.js'
3
+ import dayjs, { type Dayjs } from 'dayjs'
4
+ import { initDayJs } from '../init-dayjs.js'
5
+
6
+ // 引入 logging 库会自动初始化 dayjs
7
+ initDayJs()
8
+
9
+ export enum LogLevel {
10
+ Debug = 1,
11
+ Info = 2,
12
+ Warning = 3,
13
+ Error = 4,
14
+ }
15
+
16
+ export interface LogInfo {
17
+ logger: string // logger name;有多级 logger 的情况下,这是最初的 logger 名称
18
+ level: LogLevel
19
+ time: Dayjs
20
+ args: unknown[] // log content
21
+ }
22
+
23
+ export class LogHandler {
24
+ log(info: LogInfo) {} // eslint-disable-line @typescript-eslint/no-unused-vars
25
+ }
26
+
27
+ export class Logger {
28
+ level = LogLevel.Info
29
+ handlers = new Set<LogHandler>()
30
+
31
+ constructor(
32
+ public name: string = '',
33
+ public base: Logger | null = null, // 指定上级 logger,当前 logger 记录的日志也会传递给上级
34
+ ) {}
35
+
36
+ setLevel(level: LogLevel) {
37
+ this.level = level
38
+ }
39
+
40
+ addHandler(handler: LogHandler) {
41
+ this.handlers.add(handler)
42
+ }
43
+
44
+ /**
45
+ * 创建一个以当前 logger 为 base 的 child logger
46
+ */
47
+ getChild(name: string) {
48
+ const fullname = this.name ? `${this.name}/${name}` : name
49
+ type Constructor = new (...args: ConstructorParameters<typeof Logger>) => Logger
50
+ // 这里加上 `as this` 才能让 TypeScript 判定,对继承了 Logger 的类调用此方法时,返回的是那个类而不是原始的 Logger 类的实例
51
+ return new (this.constructor as Constructor)(fullname, this) as this
52
+ }
53
+
54
+ log(level: LogLevel, args: unknown[]) {
55
+ this.logByInfo({ logger: this.name, level, time: dayjs(), args })
56
+ }
57
+ protected logByInfo(info: LogInfo) {
58
+ if (this.base) this.base.logByInfo(info)
59
+ if (this.level > info.level) return
60
+ for (const handler of this.handlers) {
61
+ handler.log(info)
62
+ }
63
+ }
64
+
65
+ debug(...args: any[]) {
66
+ this.log(LogLevel.Debug, args)
67
+ }
68
+ info(...args: any[]) {
69
+ this.log(LogLevel.Info, args)
70
+ }
71
+ warn(...args: any[]) {
72
+ this.log(LogLevel.Warning, args)
73
+ }
74
+ error(...args: any[]) {
75
+ this.log(LogLevel.Error, args)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 提供一套默认配置好的 logger
81
+ */
82
+ const defaultLogger = new Logger()
83
+ export { defaultLogger as logger }
84
+
85
+ export function getLogger(name: string) {
86
+ return defaultLogger.getChild(name)
87
+ }
package/src/url.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * URL 工具函数
3
+ * 部分灵感来自:https://www.npmjs.com/package/qs
4
+ *
5
+ * [名词定义]
6
+ * query: 指 a=1&b=2 格式的“查询字符串”或此类字符串的解析结果。
7
+ * search: URL 中的 search 部分,与 location.search 一致,空字符串或以 '?' 开头。
8
+ * hash: URL 中的 hash 部分,与 location.hash 一直,空字符串或以 '#' 开头。
9
+ */
10
+ import isPlainObject from 'lodash/isPlainObject.js'
11
+
12
+ /*
13
+ 将 URL 里的 query 内容解析成对象
14
+
15
+ array:
16
+ - 若开启,支持解析 a[]=1&a[]=2 格式的参数,会解析成一个数组 { a: ['1', '2'] }
17
+ - 否则把 `a[]` 整体当做一个参数名 { 'a[]': '2' }
18
+
19
+ strict:
20
+ 是否开启“严格模式”(默认不开启)。在非严格模式下,会做很多兼容处理:
21
+ 1. 支持直接传入 query string(a=1&b=2);严格模式下,则需在开头补充一个 ?(?a=1&b=2)
22
+ 2. hash 里的内容也会被解析,以兼容拼接错误的 URL(把 query 拼到了 hash 后面)。
23
+ 3. 出现多个 ? 符号时,会把 ? 也当做 & 分隔符(index.html?a=1&b=2?c=3)
24
+
25
+ 小技巧:
26
+ 如果明确只想解析 hash 或 search 里的内容,可以传入 location.search / hash 而不是传入整个 location.href
27
+
28
+ 此函数不会对 query 值进行 decode,需自定处理
29
+ */
30
+ function parseQuery(url: string, options?: { array?: false, strict?: boolean }): Record<string, string> // prettier-ignore
31
+ function parseQuery(url: string, options: { array: true, strict?: boolean }): Record<string, string | string[]> // prettier-ignore
32
+ function parseQuery(
33
+ url: string,
34
+ options?: { array?: boolean; strict?: boolean },
35
+ ): Record<string, string | string[]> {
36
+ if (!url) return {}
37
+ const { array = false, strict = false } = options ?? {}
38
+
39
+ const queryString = strict
40
+ ? (/^[^?#]*\?(.+?)(\?|#|$)/.exec(url) ?? ['', ''])[1] // 排除 hash、重复的 ? 符号
41
+ : url
42
+
43
+ const query: { [name: string]: string | string[] } = {}
44
+ const reg = /([^#?&]*)=([^#?&]*)/g
45
+ let re = reg.exec(queryString)
46
+ while (re) {
47
+ const [name, value] = [re[1]!, re[2]!]
48
+ if (name.endsWith('[]') && array) {
49
+ const realName = name.slice(0, -2)
50
+ if (Array.isArray(query[realName])) (query[realName] as string[]).push(value)
51
+ else query[realName] = [value]
52
+ } else {
53
+ query[name] = value
54
+ }
55
+ re = reg.exec(queryString)
56
+ }
57
+ return query
58
+ }
59
+ export { parseQuery }
60
+
61
+ /*
62
+ 取 query 中指定参数的值
63
+ - 参数存在,返回参数值(可能是空字符串);不存在返回 null
64
+ - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
65
+ - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
66
+ */
67
+ export function getQueryParam(name: string, url: string): string | null {
68
+ const query = parseQuery(url)
69
+ return typeof query[name] === 'string' ? query[name]! : null
70
+ }
71
+
72
+ /*
73
+ 把对象合并成 query string。
74
+ 支持字符串、数值、布尔值(不建议,考虑用 0 和 1 代替),数组会转换成 name[]=value 的格式
75
+ */
76
+ type StringifyVal = string | number | boolean
77
+ export function stringifyQuery(obj: { [key: string]: StringifyVal | StringifyVal[] | undefined }) {
78
+ if (!isPlainObject(obj)) return ''
79
+ return (
80
+ Object.entries(obj)
81
+ // 过滤值为 undefined 的项目,使其完全不出现在最终的 query 中
82
+ .filter((entry): entry is [string, StringifyVal | StringifyVal[]] => entry[1] !== undefined)
83
+ .map(([name, value]) => stringifyQueryItem(name, value))
84
+ .join('&')
85
+ )
86
+ }
87
+ function stringifyQueryItem(name: string, value: StringifyVal | StringifyVal[]): string {
88
+ if (Array.isArray(value))
89
+ return value.map(subValue => stringifyQueryItem(`${name}[]`, subValue)).join('&')
90
+ return `${name}=${value}`
91
+ }
92
+
93
+ /**
94
+ * 拆分 URL 的各个部分(search 不带 '?',hash 不带 '#')
95
+ */
96
+ export function splitUrl(url: string): { base: string; search: string; hash: string } {
97
+ let hashIndex = url.indexOf('#')
98
+ if (hashIndex === -1) hashIndex = url.length
99
+
100
+ let searchIndex = url.slice(0, hashIndex).indexOf('?')
101
+ if (searchIndex === -1) searchIndex = hashIndex
102
+
103
+ return {
104
+ base: url.slice(0, searchIndex),
105
+ search: url.slice(searchIndex + 1, hashIndex),
106
+ hash: url.slice(hashIndex + 1),
107
+ }
108
+ }
109
+
110
+ /*
111
+ 把指定 search 和 hash 内容合并到 url 上
112
+
113
+ search object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
114
+ hash string 不带 # 号;若 url 已有 hash,会用此值代替
115
+ */
116
+ export function combineUrl(
117
+ origUrl: string,
118
+ search?: Record<string, string | string[]>,
119
+ hash?: string,
120
+ ) {
121
+ search = search ?? {}
122
+ hash = hash ?? ''
123
+
124
+ // 拆分原 url 的 search、hash
125
+ const { base, search: origSearch, hash: origHash } = splitUrl(origUrl)
126
+
127
+ // 拼接新 URL
128
+ let newUrl = base
129
+ const newSearch = stringifyQuery({ ...parseQuery(origSearch), ...search })
130
+ const newHash = hash || origHash
131
+ if (newSearch) newUrl += `?${newSearch}`
132
+ if (newHash) newUrl += `#${newHash}`
133
+ return newUrl
134
+ }
135
+
136
+ /*
137
+ decodeURIComponent() 对于不规范编码的字符串可能会报错(例如字符串里出现了“%”)
138
+ 用此函数代替可避免此问题
139
+ */
140
+ export function safeDecode(content: string) {
141
+ try {
142
+ return decodeURIComponent(content)
143
+ } catch (e) {
144
+ return content
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 将 URL 中的 http:// 协议改成 https://
150
+ */
151
+ export function ensureHttps(url: string | undefined) {
152
+ return url?.replace(/http:\/\//g, 'https://')
153
+ }
package/url.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ declare function parseQuery(url: string, options?: {
2
+ array?: false;
3
+ strict?: boolean;
4
+ }): Record<string, string>;
5
+ declare function parseQuery(url: string, options: {
6
+ array: true;
7
+ strict?: boolean;
8
+ }): Record<string, string | string[]>;
9
+ export { parseQuery };
10
+ export declare function getQueryParam(name: string, url: string): string | null;
11
+ type StringifyVal = string | number | boolean;
12
+ export declare function stringifyQuery(obj: {
13
+ [key: string]: StringifyVal | StringifyVal[] | undefined;
14
+ }): string;
15
+ /**
16
+ * 拆分 URL 的各个部分(search 不带 '?',hash 不带 '#')
17
+ */
18
+ export declare function splitUrl(url: string): {
19
+ base: string;
20
+ search: string;
21
+ hash: string;
22
+ };
23
+ export declare function combineUrl(origUrl: string, search?: Record<string, string | string[]>, hash?: string): string;
24
+ export declare function safeDecode(content: string): string;
25
+ /**
26
+ * 将 URL 中的 http:// 协议改成 https://
27
+ */
28
+ export declare function ensureHttps(url: string | undefined): string | undefined;
package/url.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * URL 工具函数
3
+ * 部分灵感来自:https://www.npmjs.com/package/qs
4
+ *
5
+ * [名词定义]
6
+ * query: 指 a=1&b=2 格式的“查询字符串”或此类字符串的解析结果。
7
+ * search: URL 中的 search 部分,与 location.search 一致,空字符串或以 '?' 开头。
8
+ * hash: URL 中的 hash 部分,与 location.hash 一直,空字符串或以 '#' 开头。
9
+ */
10
+ import isPlainObject from 'lodash/isPlainObject.js';
11
+ function parseQuery(url, options) {
12
+ if (!url)
13
+ return {};
14
+ const { array = false, strict = false } = options ?? {};
15
+ const queryString = strict
16
+ ? (/^[^?#]*\?(.+?)(\?|#|$)/.exec(url) ?? ['', ''])[1] // 排除 hash、重复的 ? 符号
17
+ : url;
18
+ const query = {};
19
+ const reg = /([^#?&]*)=([^#?&]*)/g;
20
+ let re = reg.exec(queryString);
21
+ while (re) {
22
+ const [name, value] = [re[1], re[2]];
23
+ if (name.endsWith('[]') && array) {
24
+ const realName = name.slice(0, -2);
25
+ if (Array.isArray(query[realName]))
26
+ query[realName].push(value);
27
+ else
28
+ query[realName] = [value];
29
+ }
30
+ else {
31
+ query[name] = value;
32
+ }
33
+ re = reg.exec(queryString);
34
+ }
35
+ return query;
36
+ }
37
+ export { parseQuery };
38
+ /*
39
+ 取 query 中指定参数的值
40
+ - 参数存在,返回参数值(可能是空字符串);不存在返回 null
41
+ - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
42
+ - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
43
+ */
44
+ export function getQueryParam(name, url) {
45
+ const query = parseQuery(url);
46
+ return typeof query[name] === 'string' ? query[name] : null;
47
+ }
48
+ export function stringifyQuery(obj) {
49
+ if (!isPlainObject(obj))
50
+ return '';
51
+ return (Object.entries(obj)
52
+ // 过滤值为 undefined 的项目,使其完全不出现在最终的 query 中
53
+ .filter((entry) => entry[1] !== undefined)
54
+ .map(([name, value]) => stringifyQueryItem(name, value))
55
+ .join('&'));
56
+ }
57
+ function stringifyQueryItem(name, value) {
58
+ if (Array.isArray(value))
59
+ return value.map(subValue => stringifyQueryItem(`${name}[]`, subValue)).join('&');
60
+ return `${name}=${value}`;
61
+ }
62
+ /**
63
+ * 拆分 URL 的各个部分(search 不带 '?',hash 不带 '#')
64
+ */
65
+ export function splitUrl(url) {
66
+ let hashIndex = url.indexOf('#');
67
+ if (hashIndex === -1)
68
+ hashIndex = url.length;
69
+ let searchIndex = url.slice(0, hashIndex).indexOf('?');
70
+ if (searchIndex === -1)
71
+ searchIndex = hashIndex;
72
+ return {
73
+ base: url.slice(0, searchIndex),
74
+ search: url.slice(searchIndex + 1, hashIndex),
75
+ hash: url.slice(hashIndex + 1),
76
+ };
77
+ }
78
+ /*
79
+ 把指定 search 和 hash 内容合并到 url 上
80
+
81
+ search object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
82
+ hash string 不带 # 号;若 url 已有 hash,会用此值代替
83
+ */
84
+ export function combineUrl(origUrl, search, hash) {
85
+ search = search ?? {};
86
+ hash = hash ?? '';
87
+ // 拆分原 url 的 search、hash
88
+ const { base, search: origSearch, hash: origHash } = splitUrl(origUrl);
89
+ // 拼接新 URL
90
+ let newUrl = base;
91
+ const newSearch = stringifyQuery({ ...parseQuery(origSearch), ...search });
92
+ const newHash = hash || origHash;
93
+ if (newSearch)
94
+ newUrl += `?${newSearch}`;
95
+ if (newHash)
96
+ newUrl += `#${newHash}`;
97
+ return newUrl;
98
+ }
99
+ /*
100
+ decodeURIComponent() 对于不规范编码的字符串可能会报错(例如字符串里出现了“%”)
101
+ 用此函数代替可避免此问题
102
+ */
103
+ export function safeDecode(content) {
104
+ try {
105
+ return decodeURIComponent(content);
106
+ }
107
+ catch (e) {
108
+ return content;
109
+ }
110
+ }
111
+ /**
112
+ * 将 URL 中的 http:// 协议改成 https://
113
+ */
114
+ export function ensureHttps(url) {
115
+ return url?.replace(/http:\/\//g, 'https://');
116
+ }