@devthing/utils 0.0.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/package.json +13 -0
- package/scripts/generate-exports.ts +44 -0
- package/src/array-set.ts +28 -0
- package/src/create-singleton.ts +43 -0
- package/src/datetime-formatter.ts +12 -0
- package/src/route-matcher.ts +64 -0
- package/src/string-hash.ts +6 -0
- package/src/timestamp-convert.ts +80 -0
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devthing/utils",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "./index.ts",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"registry": "https://registry.npmjs.org",
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "bun scripts/generate-exports"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {readdir, writeFile} from 'node:fs/promises'
|
|
2
|
+
import {resolve} from 'node:path'
|
|
3
|
+
|
|
4
|
+
const packageRoot = resolve(import.meta.dir, '..')
|
|
5
|
+
const srcDir = resolve(packageRoot, 'src')
|
|
6
|
+
const outputFile = resolve(packageRoot, 'index.ts')
|
|
7
|
+
|
|
8
|
+
const walk = async (dir: string, baseDir = dir): Promise<string[]> => {
|
|
9
|
+
const entries = await readdir(dir, {withFileTypes: true})
|
|
10
|
+
const files: string[] = []
|
|
11
|
+
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const fullPath = resolve(dir, entry.name)
|
|
14
|
+
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
files.push(...(await walk(fullPath, baseDir)))
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!entry.isFile()) {
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!entry.name.endsWith('.ts') || entry.name.endsWith('.d.ts')) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const relativePath = fullPath.slice(baseDir.length + 1).replaceAll('\\', '/')
|
|
29
|
+
files.push(relativePath)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return files
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sourceFiles = (await walk(srcDir))
|
|
36
|
+
.filter((file) => file !== 'index.ts')
|
|
37
|
+
.sort((a, b) => a.localeCompare(b))
|
|
38
|
+
|
|
39
|
+
const exportLines = sourceFiles.map((file) => {
|
|
40
|
+
return `export * from './src/${file.replace(/\.ts$/, '')}'`
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const content = exportLines.length > 0 ? `${exportLines.join('\n')}\n` : ''
|
|
44
|
+
await writeFile(outputFile, content, 'utf8')
|
package/src/array-set.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const useArraySet = () => {
|
|
2
|
+
// [1,2,3,4] [3,4,5,6] => [3,4]
|
|
3
|
+
const intersection = <T>(arr1: T[], arr2: T[]): T[] => {
|
|
4
|
+
return arr1.filter((item) => arr2.includes(item))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// [1,2,3,4] [3,4,5,6] => [1,2,3,4,5,6]
|
|
8
|
+
const union = <T>(arr1: T[], arr2: T[]): T[] => {
|
|
9
|
+
return [...new Set([...arr1, ...arr2])]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// [1,2,3,4] [3,4,5,6] => [1,2]
|
|
13
|
+
const complement = <T>(arr1: T[], arr2: T[]): T[] => {
|
|
14
|
+
return arr1.filter((item) => !arr2.includes(item))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// [1,2,3,4] [3,4,5,6] => [1,2,5,6]
|
|
18
|
+
const difference = <T>(arr1: T[], arr2: T[]): T[] => {
|
|
19
|
+
return [...complement(arr1, arr2), ...complement(arr2, arr1)]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
intersection,
|
|
24
|
+
union,
|
|
25
|
+
complement,
|
|
26
|
+
difference,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function createSingleton<T, Args extends any[]>(
|
|
2
|
+
createInstanceFn: (...args: Args) => T | Promise<T>,
|
|
3
|
+
options: {
|
|
4
|
+
isValid?: (instance: T, ...args: Args) => boolean
|
|
5
|
+
} = {},
|
|
6
|
+
): ((...args: Args) => Promise<T>) & {reset: () => void; getInstance: () => T | null} {
|
|
7
|
+
let instance: T | null = null
|
|
8
|
+
let promise: Promise<T> | null = null
|
|
9
|
+
|
|
10
|
+
const get = async (...args: Args): Promise<T> => {
|
|
11
|
+
if (instance && (!options.isValid || options.isValid(instance, ...args))) {
|
|
12
|
+
return instance
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (promise) {
|
|
16
|
+
const result = await promise
|
|
17
|
+
if (!options.isValid || options.isValid(result, ...args)) {
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
promise = (async () => {
|
|
23
|
+
try {
|
|
24
|
+
const newInstance = await createInstanceFn(...args)
|
|
25
|
+
instance = newInstance
|
|
26
|
+
return newInstance
|
|
27
|
+
} finally {
|
|
28
|
+
promise = null
|
|
29
|
+
}
|
|
30
|
+
})()
|
|
31
|
+
|
|
32
|
+
return promise
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const reset = () => {
|
|
36
|
+
instance = null
|
|
37
|
+
promise = null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getInstance = () => instance
|
|
41
|
+
|
|
42
|
+
return Object.assign(get, {reset, getInstance})
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const formatDateTime = (datetime = Date.now()) => {
|
|
2
|
+
const UTC8DateTime = new Date(datetime + 1000 * 60 * 60 * 8)
|
|
3
|
+
|
|
4
|
+
const year = UTC8DateTime.getUTCFullYear()
|
|
5
|
+
const month = String(UTC8DateTime.getUTCMonth() + 1).padStart(2, '0')
|
|
6
|
+
const day = String(UTC8DateTime.getUTCDate()).padStart(2, '0')
|
|
7
|
+
const hour = String(UTC8DateTime.getUTCHours()).padStart(2, '0')
|
|
8
|
+
const minute = String(UTC8DateTime.getUTCMinutes()).padStart(2, '0')
|
|
9
|
+
const second = String(UTC8DateTime.getUTCSeconds()).padStart(2, '0')
|
|
10
|
+
|
|
11
|
+
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
|
12
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
interface MatchResult {
|
|
2
|
+
rule: string
|
|
3
|
+
type: 'allow' | 'block'
|
|
4
|
+
length: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isPathMatch(pathname: string, rule: string): boolean {
|
|
8
|
+
let normalizedRule = rule.startsWith('/') ? rule : `/${rule}`
|
|
9
|
+
|
|
10
|
+
if (normalizedRule.length > 1 && normalizedRule.endsWith('/')) {
|
|
11
|
+
normalizedRule = normalizedRule.slice(0, -1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (normalizedRule === '/') {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (pathname === normalizedRule || pathname === `${normalizedRule}/`) {
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (pathname.startsWith(normalizedRule)) {
|
|
23
|
+
const nextChar = pathname[normalizedRule.length]
|
|
24
|
+
return nextChar === '/'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isRouteAllowed(
|
|
31
|
+
pathname: string,
|
|
32
|
+
publicRoutes: string[],
|
|
33
|
+
privateRoutes: string[],
|
|
34
|
+
): boolean {
|
|
35
|
+
const matches: MatchResult[] = []
|
|
36
|
+
|
|
37
|
+
publicRoutes.forEach((rule) => {
|
|
38
|
+
if (isPathMatch(pathname, rule)) {
|
|
39
|
+
matches.push({rule, type: 'allow', length: rule.length})
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
privateRoutes.forEach((rule) => {
|
|
44
|
+
if (isPathMatch(pathname, rule)) {
|
|
45
|
+
matches.push({rule, type: 'block', length: rule.length})
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
if (matches.length === 0) {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
matches.sort((a, b) => {
|
|
54
|
+
if (b.length !== a.length) {
|
|
55
|
+
return b.length - a.length
|
|
56
|
+
}
|
|
57
|
+
if (a.type === 'block' && b.type === 'allow') return -1
|
|
58
|
+
if (a.type === 'allow' && b.type === 'block') return 1
|
|
59
|
+
return 0
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const bestMatch = matches[0]
|
|
63
|
+
return bestMatch?.type === 'allow'
|
|
64
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const hashSHA256 = async (key: string) => {
|
|
2
|
+
const msgBuffer = new TextEncoder().encode(key)
|
|
3
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
|
|
4
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
5
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
6
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type TimePrecision = 'ms' | 's' | 'm' | 'h' | 'd'
|
|
2
|
+
export type TimePrecisionISO = TimePrecision | 'iso' | (string & {})
|
|
3
|
+
|
|
4
|
+
export function pt(timeStr: string): number
|
|
5
|
+
export function pt(timeStr: string, precision: TimePrecision): number
|
|
6
|
+
export function pt(timeStr: string, precision: `iso${string}`): string
|
|
7
|
+
export function pt(timeStr: string, precision: TimePrecisionISO): number | string
|
|
8
|
+
export function pt(timeStr: string, precision: TimePrecisionISO = 'ms'): number | string {
|
|
9
|
+
if (!timeStr) {
|
|
10
|
+
return 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const match = timeStr.trim().match(/^(\d+(?:\.\d+)?)\s*([smhdwMy])(?:\s+(.+))?$/)
|
|
14
|
+
|
|
15
|
+
if (!match) {
|
|
16
|
+
return 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const value = parseFloat(match[1]!)
|
|
20
|
+
const unit = match[2]
|
|
21
|
+
const suffix = match[3]?.toLowerCase()
|
|
22
|
+
|
|
23
|
+
const unitToMs: Record<string, number> = {
|
|
24
|
+
s: 1000,
|
|
25
|
+
m: 1000 * 60,
|
|
26
|
+
h: 1000 * 60 * 60,
|
|
27
|
+
d: 1000 * 60 * 60 * 24,
|
|
28
|
+
w: 1000 * 60 * 60 * 24 * 7,
|
|
29
|
+
M: 1000 * 60 * 60 * 24 * 31,
|
|
30
|
+
y: 1000 * 60 * 60 * 24 * 366,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const durationMs = value * (unitToMs[unit as keyof typeof unitToMs] ?? 0)
|
|
34
|
+
|
|
35
|
+
let resultMs = durationMs
|
|
36
|
+
|
|
37
|
+
if (suffix) {
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
if (['ago', 'before'].includes(suffix)) {
|
|
40
|
+
resultMs = now - durationMs
|
|
41
|
+
} else if (['later', 'after'].includes(suffix)) {
|
|
42
|
+
resultMs = now + durationMs
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (precision.startsWith('iso')) {
|
|
47
|
+
if (precision === 'iso') {
|
|
48
|
+
return new Date(resultMs).toISOString()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isoMatch = precision.match(/^iso([+-])(\d+(?:\.\d+)?)$/)
|
|
52
|
+
if (isoMatch) {
|
|
53
|
+
const sign = isoMatch[1] === '+' ? 1 : -1
|
|
54
|
+
const hours = parseFloat(isoMatch[2]!)
|
|
55
|
+
const offsetMs = sign * hours * 3600000
|
|
56
|
+
const localMs = resultMs + offsetMs
|
|
57
|
+
|
|
58
|
+
const absOffset = Math.abs(hours)
|
|
59
|
+
const h = Math.floor(absOffset)
|
|
60
|
+
const m = Math.floor((absOffset - h) * 60)
|
|
61
|
+
const tz = `${sign > 0 ? '+' : '-'}${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
|
|
62
|
+
|
|
63
|
+
return new Date(localMs).toISOString().replace('Z', tz)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
switch (precision) {
|
|
68
|
+
case 'd':
|
|
69
|
+
return Math.round(resultMs / 86400000)
|
|
70
|
+
case 'h':
|
|
71
|
+
return Math.round(resultMs / 3600000)
|
|
72
|
+
case 'm':
|
|
73
|
+
return Math.round(resultMs / 60000)
|
|
74
|
+
case 's':
|
|
75
|
+
return Math.round(resultMs / 1000)
|
|
76
|
+
case 'ms':
|
|
77
|
+
default:
|
|
78
|
+
return Math.round(resultMs)
|
|
79
|
+
}
|
|
80
|
+
}
|