@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.
- package/.eslintrc.cjs +6 -0
- package/README.md +1 -0
- package/build-cleanup.cjs +10 -0
- package/env-browser/device.d.ts +24 -0
- package/env-browser/device.js +49 -0
- package/env-browser/global.d.ts +11 -0
- package/env-browser/global.js +14 -0
- package/env-browser/load-script.d.ts +5 -0
- package/env-browser/load-script.js +13 -0
- package/env-browser/logging.d.ts +18 -0
- package/env-browser/logging.js +49 -0
- package/env-browser/manage-vconsole.d.ts +16 -0
- package/env-browser/manage-vconsole.js +38 -0
- package/env-node/index.d.ts +17 -0
- package/env-node/index.js +45 -0
- package/env-node/logging.d.ts +64 -0
- package/env-node/logging.js +159 -0
- package/index.d.ts +3 -0
- package/index.js +3 -0
- package/init-dayjs.d.ts +2 -0
- package/init-dayjs.js +7 -0
- package/lang/async.d.ts +19 -0
- package/lang/async.js +34 -0
- package/lang/index.d.ts +5 -0
- package/lang/index.js +5 -0
- package/lang/object.d.ts +5 -0
- package/lang/object.js +31 -0
- package/lang/string.d.ts +17 -0
- package/lang/string.js +59 -0
- package/lang/time.d.ts +10 -0
- package/lang/time.js +18 -0
- package/lang/types.d.ts +98 -0
- package/lang/types.js +41 -0
- package/logging/adapt.d.ts +5 -0
- package/logging/adapt.js +38 -0
- package/logging/formatters.d.ts +10 -0
- package/logging/formatters.js +22 -0
- package/logging/index.d.ts +43 -0
- package/logging/index.js +72 -0
- package/package.json +43 -0
- package/src/.eslintrc.cjs +8 -0
- package/src/env-browser/.eslintrc.cjs +6 -0
- package/src/env-browser/device.ts +60 -0
- package/src/env-browser/global.ts +20 -0
- package/src/env-browser/load-script.ts +13 -0
- package/src/env-browser/logging.ts +58 -0
- package/src/env-browser/manage-vconsole.ts +54 -0
- package/src/env-node/.eslintrc.cjs +4 -0
- package/src/env-node/index.ts +46 -0
- package/src/env-node/logging.ts +202 -0
- package/src/index.ts +3 -0
- package/src/init-dayjs.ts +8 -0
- package/src/lang/async.ts +47 -0
- package/src/lang/index.ts +5 -0
- package/src/lang/object.ts +28 -0
- package/src/lang/string.ts +62 -0
- package/src/lang/time.ts +19 -0
- package/src/lang/types.ts +138 -0
- package/src/logging/adapt.ts +40 -0
- package/src/logging/formatters.ts +23 -0
- package/src/logging/index.ts +87 -0
- package/src/url.ts +153 -0
- package/url.d.ts +28 -0
- package/url.js +116 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 针对浏览器环境定制 logging
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
logger as defaultLogger,
|
|
6
|
+
type Logger,
|
|
7
|
+
type LogInfo,
|
|
8
|
+
LogLevel,
|
|
9
|
+
LogHandler,
|
|
10
|
+
formatters,
|
|
11
|
+
} from '../logging/index.js'
|
|
12
|
+
|
|
13
|
+
export * from '../logging/index.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 实现向浏览器 console 输出日志
|
|
17
|
+
*/
|
|
18
|
+
export class ConsoleHandler extends LogHandler {
|
|
19
|
+
log(info: LogInfo) {
|
|
20
|
+
const color = (value = 'black') => `color: ${value};`
|
|
21
|
+
const prefix =
|
|
22
|
+
'%c' + [formatters.time(info), info.logger].map((v: string) => (v ? `[${v}]` : '')).join('')
|
|
23
|
+
const prefixColor = info.logger ? color(ConsoleHandler.getColor(info.logger)) : color()
|
|
24
|
+
|
|
25
|
+
const values = [prefix, prefixColor, ...info.args]
|
|
26
|
+
if (info.level === LogLevel.Debug) console.debug(...values)
|
|
27
|
+
else if (info.level === LogLevel.Info) console.log(...values)
|
|
28
|
+
else if (info.level === LogLevel.Warning) console.warn(...values)
|
|
29
|
+
else console.error(...values)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 按顺序给各主题分配颜色(取自 http://chriskempson.com/projects/base16/)
|
|
33
|
+
private static readonly colors = [
|
|
34
|
+
'#dc9656',
|
|
35
|
+
'#7cafc2',
|
|
36
|
+
'#ba8baf',
|
|
37
|
+
'#a16946',
|
|
38
|
+
'#ab4642',
|
|
39
|
+
'#86c1b9',
|
|
40
|
+
'#a1b56c',
|
|
41
|
+
'#f7ca88',
|
|
42
|
+
]
|
|
43
|
+
private static readonly colorMap = new Map<string, string>()
|
|
44
|
+
static getColor(name: string) {
|
|
45
|
+
if (!ConsoleHandler.colorMap.has(name)) {
|
|
46
|
+
const nextIndex = ConsoleHandler.colorMap.size % ConsoleHandler.colors.length
|
|
47
|
+
ConsoleHandler.colorMap.set(name, ConsoleHandler.colors[nextIndex]!)
|
|
48
|
+
}
|
|
49
|
+
return ConsoleHandler.colorMap.get(name)!
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 预设的初始化行为
|
|
55
|
+
*/
|
|
56
|
+
export function initLogger(logger: Logger = defaultLogger) {
|
|
57
|
+
logger.addHandler(new ConsoleHandler())
|
|
58
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type default as VConsole } from 'vconsole'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 注册 VConsole 代码
|
|
5
|
+
* 此类库自己并不加载 VConsole,需使用者自行通过依赖或者 CDN 加载 VConsole 然后传给此类库
|
|
6
|
+
*/
|
|
7
|
+
let VConsoleLib: (new () => VConsole) | undefined
|
|
8
|
+
|
|
9
|
+
export function registerVConsoleLib(lib: new () => VConsole) {
|
|
10
|
+
VConsoleLib = lib
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 管理 VConsole 实例
|
|
15
|
+
*/
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
VConsole: VConsole | null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
window.VConsole = null
|
|
23
|
+
|
|
24
|
+
export function isVConsoleEnabled() {
|
|
25
|
+
return localStorage.getItem('vconsole') === '1'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function detectVConsole() {
|
|
29
|
+
if (isVConsoleEnabled()) {
|
|
30
|
+
runVConsole()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function enableVConsole() {
|
|
35
|
+
localStorage.setItem('vconsole', '1')
|
|
36
|
+
runVConsole()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function disableVConsole() {
|
|
40
|
+
localStorage.setItem('vconsole', '0')
|
|
41
|
+
destoryVConsole()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function runVConsole() {
|
|
45
|
+
if (window.VConsole !== null) return
|
|
46
|
+
if (VConsoleLib === undefined) return void console.warn('尚未传入 VConsole 对象,无法启动')
|
|
47
|
+
window.VConsole = new VConsoleLib()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function destoryVConsole() {
|
|
51
|
+
if (!window.VConsole) return
|
|
52
|
+
window.VConsole.destroy()
|
|
53
|
+
window.VConsole = null
|
|
54
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node 环境下的工具函数
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'node:fs'
|
|
5
|
+
import * as path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 确认一个路径是否存在且是文件
|
|
10
|
+
*/
|
|
11
|
+
export async function isFileExists(filepath: string) {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fs.promises.stat(filepath)
|
|
14
|
+
return res.isFile()
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 确认一个路径是否存在且是文件夹
|
|
22
|
+
*/
|
|
23
|
+
export async function isDirectoryExists(dirpath: string) {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fs.promises.stat(dirpath)
|
|
26
|
+
return res.isDirectory()
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 返回当前文件的绝对路径
|
|
34
|
+
* 需要传入当前文件的 ImportMeta 对象(可通过 import.meta 取得)
|
|
35
|
+
*/
|
|
36
|
+
export function getFilePath(fileUrl: string | ImportMeta) {
|
|
37
|
+
if (typeof fileUrl !== 'string') fileUrl = fileUrl.url
|
|
38
|
+
return fileURLToPath(new URL(fileUrl))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 返回文件所处目录的绝对路径
|
|
43
|
+
*/
|
|
44
|
+
export function getFileDir(fileUrl: string | ImportMeta) {
|
|
45
|
+
return path.dirname(getFilePath(fileUrl))
|
|
46
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 针对 Node.js 环境定制 logging
|
|
3
|
+
* 注意:使用此模块需要 chalk 依赖
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import chalk from 'chalk'
|
|
9
|
+
import dayjs from 'dayjs'
|
|
10
|
+
import {
|
|
11
|
+
logger as defaultLogger,
|
|
12
|
+
type Logger,
|
|
13
|
+
type LogInfo,
|
|
14
|
+
LogLevel,
|
|
15
|
+
LogHandler,
|
|
16
|
+
formatters,
|
|
17
|
+
} from '../logging/index.js'
|
|
18
|
+
|
|
19
|
+
export * from '../logging/index.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 向 console 输出日志
|
|
23
|
+
*/
|
|
24
|
+
export class ConsoleHandler extends LogHandler {
|
|
25
|
+
log(info: LogInfo) {
|
|
26
|
+
const { logger, level, args } = info
|
|
27
|
+
const method = ConsoleHandler.consoleMethods[level]
|
|
28
|
+
const levelColor = ConsoleHandler.levelColors[level]
|
|
29
|
+
const levelName = formatters.level(info)
|
|
30
|
+
const loggerColor = chalk[ConsoleHandler.getLoggerColor(logger)]
|
|
31
|
+
const prefix = [
|
|
32
|
+
chalk.white(`[${formatters.time(info)}]`),
|
|
33
|
+
levelColor(`[${levelName}]`),
|
|
34
|
+
loggerColor(`[${logger}]`),
|
|
35
|
+
].join('')
|
|
36
|
+
method(prefix, ...args)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static readonly consoleMethods = {
|
|
40
|
+
[LogLevel.Debug]: console.debug.bind(console),
|
|
41
|
+
[LogLevel.Info]: console.info.bind(console),
|
|
42
|
+
[LogLevel.Warning]: console.warn.bind(console),
|
|
43
|
+
[LogLevel.Error]: console.error.bind(console),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static readonly levelColors = {
|
|
47
|
+
[LogLevel.Debug]: chalk.whiteBright,
|
|
48
|
+
[LogLevel.Info]: chalk.white,
|
|
49
|
+
[LogLevel.Warning]: chalk.yellowBright,
|
|
50
|
+
[LogLevel.Error]: chalk.redBright,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 可供 logger 选择的颜色
|
|
54
|
+
private static readonly loggerColors = [
|
|
55
|
+
'green',
|
|
56
|
+
'yellow',
|
|
57
|
+
'blue',
|
|
58
|
+
'magenta',
|
|
59
|
+
'cyan',
|
|
60
|
+
'greenBright',
|
|
61
|
+
'yellowBright',
|
|
62
|
+
'blueBright',
|
|
63
|
+
'magentaBright',
|
|
64
|
+
'cyanBright',
|
|
65
|
+
] as const
|
|
66
|
+
private static readonly loggerColorMap = new Map<string, (typeof this.loggerColors)[number]>()
|
|
67
|
+
static getLoggerColor(logger: string) {
|
|
68
|
+
if (!ConsoleHandler.loggerColorMap.has(logger)) {
|
|
69
|
+
const color =
|
|
70
|
+
ConsoleHandler.loggerColors[
|
|
71
|
+
ConsoleHandler.loggerColorMap.size % ConsoleHandler.loggerColors.length
|
|
72
|
+
]!
|
|
73
|
+
ConsoleHandler.loggerColorMap.set(logger, color)
|
|
74
|
+
}
|
|
75
|
+
return ConsoleHandler.loggerColorMap.get(logger)!
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 写入文件日志
|
|
81
|
+
*/
|
|
82
|
+
export interface FileHandlerOptions {
|
|
83
|
+
dir: string // 日志存放目录 Logs directory
|
|
84
|
+
filePrefix: string // 日志文件名前缀 Prefix of the log file name
|
|
85
|
+
maxLength: number // 单条日志最长多少字符 Maximum length of a single log message
|
|
86
|
+
flushLength: number // 触发写入的缓存字符串数 Length of buffered strings that trigger a write operation
|
|
87
|
+
flushInterval: number // 缓存定时写入间隔(单位:ms),为 0 则所有日志立刻写入 Buffered strings write interval, 0 means all logs are written immediately.
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class FileHandler extends LogHandler {
|
|
91
|
+
readonly options: FileHandlerOptions
|
|
92
|
+
|
|
93
|
+
constructor(options?: Partial<FileHandlerOptions>) {
|
|
94
|
+
super()
|
|
95
|
+
|
|
96
|
+
const dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
|
|
97
|
+
this.options = {
|
|
98
|
+
dir: path.resolve(dirname, 'logs'),
|
|
99
|
+
filePrefix: '',
|
|
100
|
+
maxLength: 10000,
|
|
101
|
+
flushLength: 100000,
|
|
102
|
+
flushInterval: 1000,
|
|
103
|
+
...(options ?? {}),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.initLogDir()
|
|
107
|
+
this.initFlush()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Format log content
|
|
111
|
+
log(info: LogInfo) {
|
|
112
|
+
const { logger, args } = info
|
|
113
|
+
const prefix =
|
|
114
|
+
[
|
|
115
|
+
`[${formatters.datetime(info)}]`,
|
|
116
|
+
`[${formatters.level(info)}]`,
|
|
117
|
+
logger ? `[${logger}]` : '',
|
|
118
|
+
].join('') + ' '
|
|
119
|
+
|
|
120
|
+
const itemStrings: string[] = []
|
|
121
|
+
let totalLength = prefix.length
|
|
122
|
+
for (const item of args) {
|
|
123
|
+
const itemString = this.stringifyDataItem(item)
|
|
124
|
+
|
|
125
|
+
// 截断过长的日志内容 Truncate overly long log messages
|
|
126
|
+
if (totalLength + itemString.length < this.options.maxLength) {
|
|
127
|
+
itemStrings.push((totalLength === prefix.length ? '' : ' ') + itemString)
|
|
128
|
+
totalLength += itemString.length
|
|
129
|
+
} else {
|
|
130
|
+
itemStrings.push(
|
|
131
|
+
itemString.slice(0, this.options.maxLength - totalLength) + ' [too long, sliced]',
|
|
132
|
+
)
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.pushBuffer(prefix, ...itemStrings, '\n')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected stringifyDataItem(item: unknown) {
|
|
141
|
+
if (typeof item === 'string') return item
|
|
142
|
+
if (item instanceof Error) return item.stack ?? String(item)
|
|
143
|
+
try {
|
|
144
|
+
const json = JSON.stringify(item) as string | undefined
|
|
145
|
+
return json ?? String(json)
|
|
146
|
+
} catch (e: unknown) {
|
|
147
|
+
return String(item)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle buffer
|
|
152
|
+
private buffer: string[] = []
|
|
153
|
+
private bufferSize = 0
|
|
154
|
+
|
|
155
|
+
protected pushBuffer(...strings: string[]) {
|
|
156
|
+
this.buffer.push(...strings)
|
|
157
|
+
this.bufferSize = strings.reduce((sum, v) => sum + v.length, this.bufferSize)
|
|
158
|
+
if (this.options.flushInterval === 0 || this.bufferSize >= this.options.flushLength)
|
|
159
|
+
this.flush()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
protected flush() {
|
|
163
|
+
if (this.buffer.length) {
|
|
164
|
+
const content = this.buffer.join('')
|
|
165
|
+
this.buffer = []
|
|
166
|
+
this.bufferSize = 0
|
|
167
|
+
this.write(content)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected initFlush() {
|
|
172
|
+
if (this.options.flushInterval !== 0) {
|
|
173
|
+
setInterval(() => this.flush(), this.options.flushInterval)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 文件系统交互 File system interaction
|
|
178
|
+
get filepath() {
|
|
179
|
+
const { dir, filePrefix } = this.options
|
|
180
|
+
return path.join(
|
|
181
|
+
dir,
|
|
182
|
+
`${filePrefix ? `${filePrefix}-` : ''}${dayjs().format('YYYY-MM-DD')}.log`,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
protected initLogDir() {
|
|
187
|
+
if (!fs.existsSync(this.options.dir)) fs.mkdirSync(this.options.dir)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
protected write(content: string) {
|
|
191
|
+
fs.appendFile(this.filepath, content, error => {
|
|
192
|
+
if (error) console.error('[logger] write failed: ' + String(error))
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 预设的初始化行为
|
|
199
|
+
*/
|
|
200
|
+
export function initLogger(logger: Logger = defaultLogger) {
|
|
201
|
+
logger.addHandler(new ConsoleHandler())
|
|
202
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 异步行为相关函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 返回一个指定毫秒后 resolve 的 Promise
|
|
7
|
+
* 若指定了 resolveValue,则 Promise 最终会解析出此值
|
|
8
|
+
*/
|
|
9
|
+
async function sleep(ms: number): Promise<void>
|
|
10
|
+
async function sleep<T>(ms: number, resolveValue: T): Promise<T>
|
|
11
|
+
async function sleep<T>(ms: number, resolveValue?: T) {
|
|
12
|
+
return new Promise<T | void>(resolve => {
|
|
13
|
+
setTimeout(() => resolve(resolveValue), ms)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
export { sleep }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 给 Promise 增加时限。
|
|
20
|
+
* - 若 silent 为 false,超时时 reject 一个 TimeoutError
|
|
21
|
+
* - 若 silent 为 true,则超时后,promise 永远不会 resolve 或 reject
|
|
22
|
+
*/
|
|
23
|
+
export async function timeout<T>(
|
|
24
|
+
promise: Promise<T>,
|
|
25
|
+
timeoutMS: number,
|
|
26
|
+
silent = false,
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let isTimeout = false
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
isTimeout = true
|
|
32
|
+
if (!silent) reject(new TimeoutError('timeout'))
|
|
33
|
+
}, timeoutMS)
|
|
34
|
+
|
|
35
|
+
promise.then(
|
|
36
|
+
result => {
|
|
37
|
+
if (!isTimeout) resolve(result)
|
|
38
|
+
},
|
|
39
|
+
error => {
|
|
40
|
+
if (!isTimeout) reject(error)
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
export class TimeoutError extends Error {
|
|
46
|
+
isTimeout = true
|
|
47
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对两个对象进行浅比较
|
|
3
|
+
* 代码取自 react-addons-shallow-compare 包
|
|
4
|
+
*/
|
|
5
|
+
export function shallowEqual(objA: unknown, objB: unknown) {
|
|
6
|
+
if (is(objA, objB)) return true
|
|
7
|
+
if (!isObject(objA) || !isObject(objB)) return false
|
|
8
|
+
|
|
9
|
+
const keysA = Object.keys(objA)
|
|
10
|
+
const keysB = Object.keys(objB)
|
|
11
|
+
if (keysA.length !== keysB.length) return false
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
14
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
15
|
+
const key = keysA[i]!
|
|
16
|
+
// eslint-disable-next-line prefer-object-has-own
|
|
17
|
+
if (!Object.prototype.hasOwnProperty.call(objB, key) || !is(objA[key], objB[key])) return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
function is(x: unknown, y: unknown) {
|
|
23
|
+
if (x === y) return x !== 0 || y !== 0 || 1 / x === 1 / y
|
|
24
|
+
else return x !== x && y !== y // eslint-disable-line no-self-compare
|
|
25
|
+
}
|
|
26
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === 'object' && value !== null
|
|
28
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字符串处理相关函数
|
|
3
|
+
*/
|
|
4
|
+
import escapeRegExp from 'lodash/escapeRegExp.js'
|
|
5
|
+
import padStart from 'lodash/padStart.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 将数字字符串化,并在左侧填充 0 直到达到 length 参数指定的长度
|
|
9
|
+
* 若数字本身达到或超过此长度,则不填充
|
|
10
|
+
*/
|
|
11
|
+
export function zfill(num: number, length = 2) {
|
|
12
|
+
return padStart(String(num), length, '0')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 执行关键词匹配
|
|
17
|
+
* 成功返回 true;失败返回 false
|
|
18
|
+
*/
|
|
19
|
+
const kwCache: { [kw: string]: RegExp } = {} // 避免大量重复构建正则表达式影响性能
|
|
20
|
+
export function keywordCompare(keyword: string, target: string) {
|
|
21
|
+
if (!keyword) return true
|
|
22
|
+
if (!(keyword in kwCache)) {
|
|
23
|
+
const regStr = keyword.split('').map(escapeRegExp).join('.*')
|
|
24
|
+
kwCache[keyword] = new RegExp(regStr, 'i')
|
|
25
|
+
}
|
|
26
|
+
return kwCache[keyword]!.test(target)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 对两个字符串进行排序,支持处理逻辑上的数字。
|
|
31
|
+
* 两个字符串都完全由数字组成,且都不以 0 开头时,以数字大小排序;否则以字符串形式排序。
|
|
32
|
+
*
|
|
33
|
+
* 以 0 开头是特例,例如 019 和 12 两个字符串,按直觉还是应该 019 在前面,毕竟本质还是字符串排序。
|
|
34
|
+
*
|
|
35
|
+
*
|
|
36
|
+
* "123" "456" 数字排序
|
|
37
|
+
* "123你好" "133我好" 字符串排序
|
|
38
|
+
* "019" "12" 字符串排序
|
|
39
|
+
*/
|
|
40
|
+
const _pattern = /^[1-9][0-9]*$/
|
|
41
|
+
export function numericCompare(a: string, b: string) {
|
|
42
|
+
if (_pattern.exec(a) && _pattern.exec(b)) return parseInt(a, 10) - parseInt(b, 10)
|
|
43
|
+
return a.localeCompare(b)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 字符串解析成整数,补充安全措施:
|
|
48
|
+
* 1. 默认设置 radix 为 10,无需再手动指定
|
|
49
|
+
* 2. 支持指定 fallback,当解析出来的数字是 NaN 时返回这个值
|
|
50
|
+
*/
|
|
51
|
+
export function safeParseInt(value: string | number, fallback?: number, redix = 10) {
|
|
52
|
+
const raw = parseInt(String(value), redix)
|
|
53
|
+
return isFinite(raw) ? raw : fallback ?? raw
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 字符串解析成浮点数;若解析结果是 NaN,返回 fallback
|
|
58
|
+
*/
|
|
59
|
+
export function safeParseFloat(value: string | number, fallback: number) {
|
|
60
|
+
const raw = parseFloat(String(value))
|
|
61
|
+
return isFinite(raw) ? raw : fallback
|
|
62
|
+
}
|
package/src/lang/time.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 时间处理相关函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 返回指定时间长度的 毫秒数
|
|
7
|
+
*/
|
|
8
|
+
export function daysMS(n: number) {
|
|
9
|
+
return hoursMS(n * 24)
|
|
10
|
+
}
|
|
11
|
+
export function hoursMS(n: number) {
|
|
12
|
+
return minutesMS(n * 60)
|
|
13
|
+
}
|
|
14
|
+
export function minutesMS(n: number) {
|
|
15
|
+
return secondsMS(n * 60)
|
|
16
|
+
}
|
|
17
|
+
export function secondsMS(n: number) {
|
|
18
|
+
return n * 1000
|
|
19
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对类型系统的辅助、补充
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 解决 TypeScript 中,数组字面量 [1, 'a'] 无法自动识别为 tuple 的问题
|
|
7
|
+
*
|
|
8
|
+
* 使用方法:
|
|
9
|
+
* const t = tuple(1, 'a') // 类型会识别为 [1, 'a'] 而不是 (number | string)[]
|
|
10
|
+
*
|
|
11
|
+
* 待官方提供语言层面的支持
|
|
12
|
+
* https://github.com/microsoft/TypeScript/issues/27179
|
|
13
|
+
* https://github.com/microsoft/TypeScript/issues/16656
|
|
14
|
+
*/
|
|
15
|
+
export function tuple<T extends unknown[]>(...elements: T) {
|
|
16
|
+
return elements
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 将一个对象中的指定 key 设为必须的
|
|
21
|
+
*/
|
|
22
|
+
export type RequiredFields<T, K extends keyof T> = Omit<T, K> & Pick<Required<T>, K>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 将一个对象中的指定 key 设为非必须的
|
|
26
|
+
*/
|
|
27
|
+
export type OptionalFields<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 用 ReplaceT 中的字段定义代替 T 中的
|
|
31
|
+
*/
|
|
32
|
+
export type ReplaceFields<T, ReplaceT> = Omit<T, keyof ReplaceT> & ReplaceT
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 获取一个对象所有 value 的集合
|
|
36
|
+
*/
|
|
37
|
+
export type ValueOf<T> = T extends { [_ in keyof T]: infer U } ? U : never
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 获取指定类型的 key 的集合
|
|
41
|
+
* 可以解决 keyof 无法正确获取继承了 plain object interface 的类型的 key 的问题
|
|
42
|
+
*
|
|
43
|
+
* interface Base { [key: string]: number }
|
|
44
|
+
* interface MyType extends Base { a: 1, b: 2 }
|
|
45
|
+
* keyof MyType // string | number
|
|
46
|
+
* KnownKeys<MyType> // 'a' | 'b'
|
|
47
|
+
*/
|
|
48
|
+
export type KnownKeys<T> = ValueOf<{
|
|
49
|
+
[K in keyof T]: string extends K ? never : number extends K ? never : K
|
|
50
|
+
}>
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 排除对象中指定类型的项目
|
|
54
|
+
*/
|
|
55
|
+
export type ExcludePropertiesOfType<T, ExcludeValueT> = Pick<
|
|
56
|
+
T,
|
|
57
|
+
{ [K in keyof T]: T[K] extends ExcludeValueT ? never : K }[keyof T]
|
|
58
|
+
>
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 排除对象的方法(仅保留属性)
|
|
62
|
+
*/
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
64
|
+
export type ExcluceMethods<T> = ExcludePropertiesOfType<T, Function>
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 所有“可能失败”的操作都可使用此类型作为返回值
|
|
68
|
+
*/
|
|
69
|
+
export interface Success<T = void> {
|
|
70
|
+
success: true
|
|
71
|
+
data: T
|
|
72
|
+
}
|
|
73
|
+
export interface Failed<ET = string> {
|
|
74
|
+
success: false
|
|
75
|
+
error: ET
|
|
76
|
+
}
|
|
77
|
+
export type MaySuccess<T = void, ET = string> = Success<T> | Failed<ET>
|
|
78
|
+
|
|
79
|
+
function success(): Success
|
|
80
|
+
function success<T>(data: T): Success<T>
|
|
81
|
+
function success<T = void>(data?: T) {
|
|
82
|
+
return { success: true, data }
|
|
83
|
+
}
|
|
84
|
+
export { success }
|
|
85
|
+
|
|
86
|
+
export function failed<ET>(error: ET): Failed<ET> {
|
|
87
|
+
return { success: false, error }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 若传入值为 success,格式化其 data;否则原样返回错误
|
|
92
|
+
* 支持传入会返回 MaySuccess 的 Promise
|
|
93
|
+
*/
|
|
94
|
+
function formatSuccess<T, ET, FT>(
|
|
95
|
+
item: MaySuccess<T, ET>,
|
|
96
|
+
formatter: (data: T) => FT,
|
|
97
|
+
): MaySuccess<FT, ET>
|
|
98
|
+
function formatSuccess<T, ET, FT>(
|
|
99
|
+
item: Promise<MaySuccess<T, ET>>,
|
|
100
|
+
formatter: (data: T) => FT,
|
|
101
|
+
): Promise<MaySuccess<FT, ET>>
|
|
102
|
+
function formatSuccess<T, ET, FT>(
|
|
103
|
+
item: MaySuccess<T, ET> | Promise<MaySuccess<T, ET>>,
|
|
104
|
+
formatter: (data: T) => FT,
|
|
105
|
+
) {
|
|
106
|
+
if ('then' in item) return item.then(finalItem => formatSuccess(finalItem, formatter))
|
|
107
|
+
return item.success ? { ...item, data: formatter(item.data) } : item
|
|
108
|
+
}
|
|
109
|
+
export { formatSuccess }
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 确认变量是否有值
|
|
113
|
+
* 注意:空字符串和数字 0 也会判定为没有值
|
|
114
|
+
*/
|
|
115
|
+
function truthy(
|
|
116
|
+
value: string | number | boolean | null | undefined,
|
|
117
|
+
): value is string | number | true
|
|
118
|
+
function truthy<T>(
|
|
119
|
+
value: T | string | number | boolean | null | undefined,
|
|
120
|
+
): value is T | string | number | true
|
|
121
|
+
function truthy<T>(value: T | string | number | boolean | null | undefined) {
|
|
122
|
+
return value !== null && value !== undefined && value !== '' && value !== 0 && value !== false
|
|
123
|
+
}
|
|
124
|
+
export { truthy }
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 定义 JSON 数据
|
|
128
|
+
*/
|
|
129
|
+
export type JSONData = number | boolean | string | null | JSONData[] | { [key: string]: JSONData }
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 有些场景不适合用 undefined 判断一个变量是否被赋值(例如它允许被赋值成 undefined)
|
|
133
|
+
* 此时可以用这个 symbol 来做判断。
|
|
134
|
+
*
|
|
135
|
+
* if (value === noValue) console.log('未赋值')
|
|
136
|
+
* else console.log('已赋值', value)
|
|
137
|
+
*/
|
|
138
|
+
export const noValue = Symbol('no-value')
|