tooling overhaul

This commit is contained in:
itsMapleLeaf
2023-08-16 19:32:28 -05:00
parent 7ac1a9cdce
commit e9e5a1617b
111 changed files with 6758 additions and 6156 deletions

View File

@@ -1,42 +1,42 @@
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
import type {
CamelCasedPropertiesDeep,
SnakeCasedPropertiesDeep,
CamelCasedPropertiesDeep,
SnakeCasedPropertiesDeep,
} from "type-fest"
import { expect, test } from "vitest"
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
test("camelCaseDeep", () => {
const input = {
some_prop: {
some_deep_prop: "some_deep_value",
},
someOtherProp: "someOtherValue",
}
const input = {
some_prop: {
some_deep_prop: "some_deep_value",
},
someOtherProp: "someOtherValue",
}
const expected: CamelCasedPropertiesDeep<typeof input> = {
someProp: {
someDeepProp: "some_deep_value",
},
someOtherProp: "someOtherValue",
}
const expected: CamelCasedPropertiesDeep<typeof input> = {
someProp: {
someDeepProp: "some_deep_value",
},
someOtherProp: "someOtherValue",
}
expect(camelCaseDeep(input)).toEqual(expected)
expect(camelCaseDeep(input)).toEqual(expected)
})
test("snakeCaseDeep", () => {
const input = {
someProp: {
someDeepProp: "someDeepValue",
},
some_other_prop: "someOtherValue",
}
const input = {
someProp: {
someDeepProp: "someDeepValue",
},
some_other_prop: "someOtherValue",
}
const expected: SnakeCasedPropertiesDeep<typeof input> = {
some_prop: {
some_deep_prop: "someDeepValue",
},
some_other_prop: "someOtherValue",
}
const expected: SnakeCasedPropertiesDeep<typeof input> = {
some_prop: {
some_deep_prop: "someDeepValue",
},
some_other_prop: "someOtherValue",
}
expect(snakeCaseDeep(input)).toEqual(expected)
expect(snakeCaseDeep(input)).toEqual(expected)
})

View File

@@ -1,34 +1,35 @@
import { camelCase, isObject, snakeCase } from "lodash-es"
import type {
CamelCasedPropertiesDeep,
SnakeCasedPropertiesDeep,
CamelCasedPropertiesDeep,
SnakeCasedPropertiesDeep,
UnknownRecord,
} from "type-fest"
function convertKeyCaseDeep<Input, Output>(
input: Input,
convertKey: (key: string) => string,
input: Input,
convertKey: (key: string) => string,
): Output {
if (!isObject(input)) {
return input as unknown as Output
}
if (!isObject(input)) {
return input as unknown as Output
}
if (Array.isArray(input)) {
return input.map((item) =>
convertKeyCaseDeep(item, convertKey),
) as unknown as Output
}
if (Array.isArray(input)) {
return input.map((item) =>
convertKeyCaseDeep(item, convertKey),
) as unknown as Output
}
const output: any = {}
for (const [key, value] of Object.entries(input)) {
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
}
return output
const output = {} as UnknownRecord
for (const [key, value] of Object.entries(input)) {
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
}
return output as Output
}
export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> {
return convertKeyCaseDeep(input, camelCase)
return convertKeyCaseDeep(input, camelCase)
}
export function snakeCaseDeep<T>(input: T): SnakeCasedPropertiesDeep<T> {
return convertKeyCaseDeep(input, snakeCase)
return convertKeyCaseDeep(input, snakeCase)
}

View File

@@ -1,5 +1,5 @@
import { raise } from "./raise.js"
export function getEnvironmentValue(name: string) {
return process.env[name] ?? raise(`Missing environment variable: ${name}`)
return process.env[name] ?? raise(`Missing environment variable: ${name}`)
}

View File

@@ -1,7 +1,7 @@
/**
* for narrowing instance types with array.filter
*/
/** For narrowing instance types with array.filter */
export const isInstanceOf =
<T>(Constructor: new (...args: any[]) => T) =>
(value: unknown): value is T =>
value instanceof Constructor
<Instance, Args extends unknown[]>(
constructor: new (...args: Args) => Instance,
) =>
(value: unknown): value is Instance =>
value instanceof constructor

View File

@@ -1,7 +1,3 @@
export function isObject<T>(
value: T,
): value is Exclude<T, Primitive | AnyFunction> {
return typeof value === "object" && value !== null
export function isObject(value: unknown): value is object {
return typeof value === "object" && value !== null
}
type Primitive = string | number | boolean | undefined | null
type AnyFunction = (...args: any[]) => any

7
packages/helpers/json.ts Normal file
View File

@@ -0,0 +1,7 @@
export function safeJsonStringify(value: unknown): string {
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}

View File

@@ -1,3 +1,3 @@
export function last<T>(array: T[]): T | undefined {
return array[array.length - 1]
return array[array.length - 1]
}

View File

@@ -1,11 +1,11 @@
import { inspect } from "node:util"
export function logPretty(value: unknown) {
console.info(
inspect(value, {
// depth: Number.POSITIVE_INFINITY,
depth: 10,
colors: true,
}),
)
console.info(
inspect(value, {
// depth: Number.POSITIVE_INFINITY,
depth: 10,
colors: true,
}),
)
}

View File

@@ -0,0 +1,7 @@
import { expect, test } from "vitest"
import { omit } from "./omit.ts"
test("omit", () => {
const subject = { a: 1, b: true }
expect(omit(subject, ["a"])).toStrictEqual({ b: true })
})

View File

@@ -0,0 +1,3 @@
import { omit } from "./omit.ts"
omit({ a: 1, b: true }, ["a"]) satisfies { b: boolean }

View File

@@ -1,13 +1,10 @@
export function omit<Subject extends object, Key extends PropertyKey>(
subject: Subject,
keys: Key[],
// hack: using a conditional type preserves union types
): Subject extends any ? Omit<Subject, Key> : never {
const result: any = {}
for (const key in subject) {
if (!keys.includes(key as unknown as Key)) {
result[key] = subject[key]
}
}
return result
subject: Subject,
keys: Key[],
) {
const keySet = new Set<PropertyKey>(keys)
return Object.fromEntries(
Object.entries(subject).filter(([key]) => !keySet.has(key)),
// hack: conditional type preserves unions
) as Subject extends unknown ? Omit<Subject, Key> : never
}

View File

@@ -1,11 +1,11 @@
{
"name": "@reacord/helpers",
"version": "0.0.0",
"private": true,
"dependencies": {
"@types/lodash-es": "^4.17.6",
"lodash-es": "^4.17.21",
"type-fest": "^2.17.0",
"vitest": "^0.18.1"
}
"name": "@reacord/helpers",
"version": "0.0.0",
"private": true,
"dependencies": {
"@types/lodash-es": "^4.17.8",
"lodash-es": "^4.17.21",
"type-fest": "^4.2.0",
"vitest": "^0.34.1"
}
}

View File

@@ -1,15 +1,11 @@
import type { LoosePick, UnknownRecord } from "./types"
import type { LoosePick } from "./types"
export function pick<T, K extends keyof T | PropertyKey>(
object: T,
keys: K[],
): LoosePick<T, K> {
const result: any = {}
for (const key of keys) {
const value = (object as UnknownRecord)[key]
if (value !== undefined) {
result[key] = value
}
}
return result
export function pick<T extends object, K extends keyof T | PropertyKey>(
object: T,
keys: K[],
) {
const keySet = new Set<PropertyKey>(keys)
return Object.fromEntries(
Object.entries(object).filter(([key]) => keySet.has(key)),
) as LoosePick<T, K>
}

View File

@@ -3,33 +3,32 @@ import type { PruneNullishValues } from "./prune-nullish-values"
import { pruneNullishValues } from "./prune-nullish-values"
test("pruneNullishValues", () => {
type InputType = {
a: string
b: string | null | undefined
c?: string
d: {
a: string
b: string | undefined
}
}
interface InputType {
a: string
b: string | null | undefined
c?: string
d: {
a: string
b: string | undefined
}
}
const input: InputType = {
a: "a",
// eslint-disable-next-line unicorn/no-null
b: null,
c: undefined,
d: {
a: "a",
b: undefined,
},
}
const input: InputType = {
a: "a",
b: null,
c: undefined,
d: {
a: "a",
b: undefined,
},
}
const output: PruneNullishValues<InputType> = {
a: "a",
d: {
a: "a",
},
}
const output: PruneNullishValues<InputType> = {
a: "a",
d: {
a: "a",
},
}
expect(pruneNullishValues(input)).toEqual(output)
expect(pruneNullishValues(input)).toEqual(output)
})

View File

@@ -1,42 +1,44 @@
import { isObject } from "./is-object"
export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
if (Array.isArray(input)) {
return input.filter(Boolean).map((item) => pruneNullishValues(item)) as any
}
if (!isObject(input)) {
return input as PruneNullishValues<T>
}
if (!isObject(input)) {
return input as any
}
if (Array.isArray(input)) {
return input
.filter(Boolean)
.map((item) => pruneNullishValues(item)) as PruneNullishValues<T>
}
const result: any = {}
for (const [key, value] of Object.entries(input as any)) {
if (value != undefined) {
result[key] = pruneNullishValues(value)
}
}
return result
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(input)) {
if (value != undefined) {
result[key] = pruneNullishValues(value)
}
}
return result as PruneNullishValues<T>
}
export type PruneNullishValues<Input> = Input extends object
? OptionalKeys<
{ [Key in keyof Input]: NonNullable<PruneNullishValues<Input[Key]>> },
KeysWithNullishValues<Input>
>
: Input
? OptionalKeys<
{ [Key in keyof Input]: NonNullable<PruneNullishValues<Input[Key]>> },
KeysWithNullishValues<Input>
>
: Input
type OptionalKeys<Input, Keys extends keyof Input> = Omit<Input, Keys> & {
[Key in Keys]?: Input[Key]
[Key in Keys]?: Input[Key]
}
type KeysWithNullishValues<Input> = NonNullable<
Values<{
[Key in keyof Input]: null extends Input[Key]
? Key
: undefined extends Input[Key]
? Key
: never
}>
Values<{
[Key in keyof Input]: null extends Input[Key]
? Key
: undefined extends Input[Key]
? Key
: never
}>
>
type Values<Input> = Input[keyof Input]

View File

@@ -1,5 +1,5 @@
import { toError } from "./to-error.js"
export function raise(error: unknown): never {
throw toError(error)
throw toError(error)
}

View File

@@ -1,10 +1,10 @@
import { setTimeout } from "node:timers/promises"
import { toError } from "./to-error.js"
import { setTimeout } from "node:timers/promises"
export async function rejectAfter(
timeMs: number,
error: unknown = `rejected after ${timeMs}ms`,
timeMs: number,
error: unknown = `rejected after ${timeMs}ms`,
): Promise<never> {
await setTimeout(timeMs)
throw toError(error)
await setTimeout(timeMs)
throw toError(error)
}

View File

@@ -4,18 +4,18 @@ const maxTime = 500
const waitPeriod = 50
export async function retryWithTimeout<T>(
callback: () => Promise<T> | T,
callback: () => Promise<T> | T,
): Promise<T> {
const startTime = Date.now()
// eslint-disable-next-line no-constant-condition
while (true) {
try {
return await callback()
} catch (error) {
if (Date.now() - startTime > maxTime) {
throw error
}
await setTimeout(waitPeriod)
}
}
const startTime = Date.now()
// eslint-disable-next-line no-constant-condition
while (true) {
try {
return await callback()
} catch (error) {
if (Date.now() - startTime > maxTime) {
throw error
}
await setTimeout(waitPeriod)
}
}
}

View File

@@ -1,3 +1,3 @@
export function toError(value: unknown) {
return value instanceof Error ? value : new Error(String(value))
return value instanceof Error ? value : new Error(String(value))
}

View File

@@ -1,4 +1,4 @@
/** A typesafe version of toUpperCase */
export function toUpper<S extends string>(string: S) {
return string.toUpperCase() as Uppercase<S>
return string.toUpperCase() as Uppercase<S>
}

View File

@@ -1,3 +1,3 @@
{
"extends": "@itsmapleleaf/configs/tsconfig.base"
"extends": "../../tsconfig.base.json"
}

View File

@@ -0,0 +1,4 @@
import { LooseOmit, LoosePick, typeEquals } from "./types.ts"
typeEquals<LoosePick<{ a: 1; b: 2 }, "a">, { a: 1 }>(true)
typeEquals<LooseOmit<{ a: 1; b: 2 }, "a">, { b: 2 }>(true)

View File

@@ -1,11 +1,21 @@
export type MaybePromise<T> = T | Promise<T>
import { raise } from "./raise.ts"
export type ValueOf<Type> = Type extends ReadonlyArray<infer Value>
? Value
: Type[keyof Type]
export type MaybePromise<T> = T | PromiseLike<T>
export type UnknownRecord = Record<PropertyKey, unknown>
export type ValueOf<Type> = Type extends readonly (infer Value)[]
? Value
: Type[keyof Type]
export type LoosePick<Shape, Keys extends PropertyKey> = {
[Key in Keys]: Shape extends Record<Key, infer Value> ? Value : never
}
export type LoosePick<Shape, Keys extends PropertyKey> = Simplify<{
[Key in Extract<Keys, keyof Shape>]: Shape[Key]
}>
export type LooseOmit<Shape, Keys extends PropertyKey> = Simplify<{
[Key in Exclude<keyof Shape, Keys>]: Shape[Key]
}>
export type Simplify<T> = { [Key in keyof T]: T[Key] } & NonNullable<unknown>
export const typeEquals = <A, B>(
_result: A extends B ? (B extends A ? true : false) : false,
) => raise("typeEquals() should not be called at runtime")

View File

@@ -1,21 +1,22 @@
import { setTimeout } from "node:timers/promises"
import { MaybePromise } from "./types.ts"
const maxTime = 1000
export async function waitFor<Result>(
predicate: () => Result,
predicate: () => MaybePromise<Result>,
): Promise<Awaited<Result>> {
const startTime = Date.now()
let lastError: unknown
const startTime = Date.now()
let lastError: unknown
while (Date.now() - startTime < maxTime) {
try {
return await predicate()
} catch (error) {
lastError = error
await setTimeout(50)
}
}
while (Date.now() - startTime < maxTime) {
try {
return await predicate()
} catch (error) {
lastError = error
await setTimeout(50)
}
}
throw lastError ?? new Error("Timeout")
throw lastError ?? new Error("Timeout")
}

View File

@@ -1,24 +1,24 @@
import { inspect } from "node:util"
export function withLoggedMethodCalls<T extends object>(value: T) {
return new Proxy(value as Record<string | symbol, unknown>, {
get(target, property) {
const value = target[property]
if (typeof value !== "function") {
return value
}
return (...values: any[]) => {
console.info(
`${String(property)}(${values
.map((value) =>
typeof value === "object" && value !== null
? value.constructor.name
: inspect(value, { colors: true }),
)
.join(", ")})`,
)
return value.apply(target, values)
}
},
}) as T
return new Proxy(value as Record<string | symbol, unknown>, {
get(target, property) {
const value = target[property]
if (typeof value !== "function") {
return value
}
return (...values: unknown[]) => {
console.info(
`${String(property)}(${values
.map((value) =>
typeof value === "object" && value !== null
? value.constructor.name
: inspect(value, { colors: true }),
)
.join(", ")})`,
)
return value.apply(target, values)
}
},
}) as T
}