Add initial tools for managing derivations and React compatibility (#202)
Co-authored-by: Cole Lawrence <cole@colelawrence.com> Co-authored-by: Elliot <key.draw@gmail.com> Co-authored-by: Aria <aria.minaei@gmail.com>
This commit is contained in:
parent
bebf281517
commit
f1844952ea
12 changed files with 320 additions and 30 deletions
|
@ -201,7 +201,9 @@ export default class Atom<State extends {}>
|
||||||
|
|
||||||
private _checkUpdates(scope: Scope, oldState: unknown, newState: unknown) {
|
private _checkUpdates(scope: Scope, oldState: unknown, newState: unknown) {
|
||||||
if (oldState === newState) return
|
if (oldState === newState) return
|
||||||
scope.identityChangeListeners.forEach((cb) => cb(newState))
|
for (const cb of scope.identityChangeListeners) {
|
||||||
|
cb(newState)
|
||||||
|
}
|
||||||
|
|
||||||
if (scope.children.size === 0) return
|
if (scope.children.size === 0) return
|
||||||
|
|
||||||
|
@ -212,11 +214,11 @@ export default class Atom<State extends {}>
|
||||||
if (oldValueType === ValueTypes.Other && oldValueType === newValueType)
|
if (oldValueType === ValueTypes.Other && oldValueType === newValueType)
|
||||||
return
|
return
|
||||||
|
|
||||||
scope.children.forEach((childScope, childKey) => {
|
for (const [childKey, childScope] of scope.children) {
|
||||||
const oldChildVal = getKeyOfValue(oldState, childKey, oldValueType)
|
const oldChildVal = getKeyOfValue(oldState, childKey, oldValueType)
|
||||||
const newChildVal = getKeyOfValue(newState, childKey, newValueType)
|
const newChildVal = getKeyOfValue(newState, childKey, newValueType)
|
||||||
this._checkUpdates(childScope, oldChildVal, newChildVal)
|
this._checkUpdates(childScope, oldChildVal, newChildVal)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getOrCreateScopeForPath(path: (string | number)[]): Scope {
|
private _getOrCreateScopeForPath(path: (string | number)[]): Scope {
|
||||||
|
|
|
@ -121,9 +121,10 @@ export default class Ticker {
|
||||||
tick(t: number = performance.now()) {
|
tick(t: number = performance.now()) {
|
||||||
this._ticking = true
|
this._ticking = true
|
||||||
this._timeAtCurrentTick = t
|
this._timeAtCurrentTick = t
|
||||||
this._scheduledForNextTick.forEach((v) =>
|
for (const v of this._scheduledForNextTick) {
|
||||||
this._scheduledForThisOrNextTick.add(v),
|
this._scheduledForThisOrNextTick.add(v)
|
||||||
)
|
}
|
||||||
|
|
||||||
this._scheduledForNextTick.clear()
|
this._scheduledForNextTick.clear()
|
||||||
this._tick(0)
|
this._tick(0)
|
||||||
this._ticking = false
|
this._ticking = false
|
||||||
|
@ -142,9 +143,9 @@ export default class Ticker {
|
||||||
|
|
||||||
const oldSet = this._scheduledForThisOrNextTick
|
const oldSet = this._scheduledForThisOrNextTick
|
||||||
this._scheduledForThisOrNextTick = new Set()
|
this._scheduledForThisOrNextTick = new Set()
|
||||||
oldSet.forEach((fn) => {
|
for (const fn of oldSet) {
|
||||||
fn(time)
|
fn(time)
|
||||||
})
|
}
|
||||||
|
|
||||||
if (this._scheduledForThisOrNextTick.size > 0) {
|
if (this._scheduledForThisOrNextTick.size > 0) {
|
||||||
return this._tick(iterationNumber + 1)
|
return this._tick(iterationNumber + 1)
|
||||||
|
|
|
@ -170,9 +170,9 @@ export default abstract class AbstractDerivation<V> implements IDerivation<V> {
|
||||||
this._didMarkDependentsAsStale = true
|
this._didMarkDependentsAsStale = true
|
||||||
this._isFresh = false
|
this._isFresh = false
|
||||||
|
|
||||||
this._dependents.forEach((dependent) => {
|
for (const dependent of this._dependents) {
|
||||||
dependent(this)
|
dependent(this)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -228,14 +228,14 @@ export default abstract class AbstractDerivation<V> implements IDerivation<V> {
|
||||||
this._didMarkDependentsAsStale = false
|
this._didMarkDependentsAsStale = false
|
||||||
this._isFresh = false
|
this._isFresh = false
|
||||||
if (shouldBecomeHot) {
|
if (shouldBecomeHot) {
|
||||||
this._dependencies.forEach((d) => {
|
for (const d of this._dependencies) {
|
||||||
d.addDependent(this._internal_markAsStale)
|
d.addDependent(this._internal_markAsStale)
|
||||||
})
|
}
|
||||||
this._keepHot()
|
this._keepHot()
|
||||||
} else {
|
} else {
|
||||||
this._dependencies.forEach((d) => {
|
for (const d of this._dependencies) {
|
||||||
d.removeDependent(this._internal_markAsStale)
|
d.removeDependent(this._internal_markAsStale)
|
||||||
})
|
}
|
||||||
this._becomeCold()
|
this._becomeCold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,18 +70,18 @@ export class PrismDerivation<V> extends AbstractDerivation<V> {
|
||||||
|
|
||||||
popCollector(collector)
|
popCollector(collector)
|
||||||
|
|
||||||
this._dependencies.forEach((dep) => {
|
for (const dep of this._dependencies) {
|
||||||
if (!newDeps.has(dep)) {
|
if (!newDeps.has(dep)) {
|
||||||
this._removeDependency(dep)
|
this._removeDependency(dep)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
this._dependencies = newDeps
|
this._dependencies = newDeps
|
||||||
|
|
||||||
startIgnoringDependencies()
|
startIgnoringDependencies()
|
||||||
newDeps.forEach((dep) => {
|
for (const dep of newDeps) {
|
||||||
this._cacheOfDendencyValues.set(dep, dep.getValue())
|
this._cacheOfDendencyValues.set(dep, dep.getValue())
|
||||||
})
|
}
|
||||||
stopIgnoringDependencies()
|
stopIgnoringDependencies()
|
||||||
|
|
||||||
return value!
|
return value!
|
||||||
|
|
|
@ -20,17 +20,14 @@ export default class Emitter<V> {
|
||||||
this._lastTapperId = 0
|
this._lastTapperId = 0
|
||||||
this._tappers = new Map()
|
this._tappers = new Map()
|
||||||
this.tappable = new Tappable({
|
this.tappable = new Tappable({
|
||||||
tapToSource: (cb: Tapper<V>) => {
|
tapToSource: (cb: Tapper<V>) => this._tap(cb),
|
||||||
return this._tap(cb)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_tap(cb: Tapper<V>): Untap {
|
_tap(cb: Tapper<V>): Untap {
|
||||||
const tapperId = this._lastTapperId++
|
const tapperId = this._lastTapperId++
|
||||||
this._tappers.set(tapperId, cb)
|
this._tappers.set(tapperId, cb)
|
||||||
this._onNumberOfTappersChangeListener &&
|
this._onNumberOfTappersChangeListener?.(this._tappers.size)
|
||||||
this._onNumberOfTappersChangeListener(this._tappers.size)
|
|
||||||
return () => {
|
return () => {
|
||||||
this._removeTapperById(tapperId)
|
this._removeTapperById(tapperId)
|
||||||
}
|
}
|
||||||
|
@ -41,8 +38,7 @@ export default class Emitter<V> {
|
||||||
this._tappers.delete(id)
|
this._tappers.delete(id)
|
||||||
const newSize = this._tappers.size
|
const newSize = this._tappers.size
|
||||||
if (oldSize !== newSize) {
|
if (oldSize !== newSize) {
|
||||||
this._onNumberOfTappersChangeListener &&
|
this._onNumberOfTappersChangeListener?.(newSize)
|
||||||
this._onNumberOfTappersChangeListener(this._tappers.size)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,9 +48,9 @@ export default class Emitter<V> {
|
||||||
* @param payload - The value to be emitted.
|
* @param payload - The value to be emitted.
|
||||||
*/
|
*/
|
||||||
emit(payload: V) {
|
emit(payload: V) {
|
||||||
this._tappers.forEach((cb) => {
|
for (const cb of this._tappers.values()) {
|
||||||
cb(payload)
|
cb(payload)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -53,9 +53,9 @@ export default class Tappable<V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _cb: any = (arg: any): void => {
|
private _cb: any = (arg: any): void => {
|
||||||
this._tappers.forEach((cb) => {
|
for (const cb of this._tappers.values()) {
|
||||||
cb(arg)
|
cb(arg)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -179,7 +179,7 @@ function queueIfNeeded() {
|
||||||
*
|
*
|
||||||
* I'm happy with how little bookkeeping we ended up doing here.
|
* I'm happy with how little bookkeeping we ended up doing here.
|
||||||
*/
|
*/
|
||||||
function useDerivation<T>(der: IDerivation<T>, debugLabel?: string): T {
|
export function useDerivation<T>(der: IDerivation<T>, debugLabel?: string): T {
|
||||||
const _forceUpdate = useForceUpdate(debugLabel)
|
const _forceUpdate = useForceUpdate(debugLabel)
|
||||||
|
|
||||||
const refs = useRef<{queueItem: QueueItem; unmounted: boolean}>(
|
const refs = useRef<{queueItem: QueueItem; unmounted: boolean}>(
|
||||||
|
|
116
theatre/studio/src/utils/derive-utils.tsx
Normal file
116
theatre/studio/src/utils/derive-utils.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import {isDerivation, prism, val} from '@theatre/dataverse'
|
||||||
|
import type {IDerivation, Pointer} from '@theatre/dataverse'
|
||||||
|
import {useDerivation} from '@theatre/react'
|
||||||
|
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||||
|
import React, {useMemo, useRef} from 'react'
|
||||||
|
import {invariant} from './invariant'
|
||||||
|
import {emptyArray} from '@theatre/shared/utils'
|
||||||
|
|
||||||
|
type DeriveAll<T> = IDerivation<
|
||||||
|
{
|
||||||
|
[P in keyof T]: T[P] extends $<infer R> ? R : never
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
export type $<T> = IDerivation<T> | Pointer<T>
|
||||||
|
|
||||||
|
function deriveAllD<T extends [...$<any>[]]>(obj: T): DeriveAll<T>
|
||||||
|
function deriveAllD<T extends Record<string, $<any>>>(obj: T): DeriveAll<T>
|
||||||
|
function deriveAllD<T extends Record<string, $<any>> | $<any>[]>(
|
||||||
|
obj: T,
|
||||||
|
): DeriveAll<T> {
|
||||||
|
return prism(() => {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
const values = new Array(obj.length)
|
||||||
|
for (let i = 0; i < obj.length; i++) {
|
||||||
|
values[i] = obj[i].getValue()
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
} else {
|
||||||
|
const values: $IntentionalAny = {}
|
||||||
|
for (const k in obj) {
|
||||||
|
values[k] = val((obj as Record<string, $<any>>)[k])
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
}) as $IntentionalAny
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReactPrism(
|
||||||
|
fn: () => React.ReactNode,
|
||||||
|
deps: readonly any[] = emptyArray,
|
||||||
|
): React.ReactElement {
|
||||||
|
const derivation = useMemo(() => prism(fn), deps)
|
||||||
|
return <DeriveElement der={derivation} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reactPrism(fn: () => React.ReactNode): React.ReactElement {
|
||||||
|
return <DeriveElement der={prism(fn)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeriveElement(props: {der: IDerivation<React.ReactNode>}) {
|
||||||
|
const node = useDerivation(props.der)
|
||||||
|
return <>{node}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This is only used for type checking to make sure the APIs are used properly */
|
||||||
|
interface TSErrors<M> extends Error {}
|
||||||
|
|
||||||
|
type ReactDeriver<Props extends {}> = (
|
||||||
|
props: {
|
||||||
|
[P in keyof Props]: Props[P] extends IDerivation<infer _>
|
||||||
|
? TSErrors<"Can't both use Derivation properties while wrapping with deriver">
|
||||||
|
: Props[P] | IDerivation<Props[P]>
|
||||||
|
},
|
||||||
|
) => React.ReactElement | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap up the component to enable it to take derivable properties.
|
||||||
|
* Invoked similarly to `React.memo`.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This is an experimental interface for wrapping components in a version
|
||||||
|
* which allows you to pass in derivations for any of the properties that
|
||||||
|
* previously took only values.
|
||||||
|
*/
|
||||||
|
export function deriver<Props extends {}>(
|
||||||
|
Component: React.ComponentType<Props>,
|
||||||
|
): ReactDeriver<Props> {
|
||||||
|
return React.forwardRef(function deriverRender(
|
||||||
|
props: Record<string, $IntentionalAny>,
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
let observableArr = []
|
||||||
|
const observables: Record<string, IDerivation<$IntentionalAny>> = {}
|
||||||
|
const normalProps: Record<string, $IntentionalAny> = {
|
||||||
|
ref,
|
||||||
|
}
|
||||||
|
for (const key in props) {
|
||||||
|
const value = props[key]
|
||||||
|
if (isDerivation(value)) {
|
||||||
|
observableArr.push(value)
|
||||||
|
observables[key] = value
|
||||||
|
} else {
|
||||||
|
normalProps[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialCount = useRef(observableArr.length)
|
||||||
|
invariant(
|
||||||
|
initialCount.current === observableArr.length,
|
||||||
|
`expect same number of observable props on every invocation of deriver wrapped component.`,
|
||||||
|
{initial: initialCount.current, count: observableArr.length},
|
||||||
|
)
|
||||||
|
|
||||||
|
const allD = useMemo(() => deriveAllD(observables), observableArr)
|
||||||
|
const observedPropState = useDerivation(allD)
|
||||||
|
|
||||||
|
return (
|
||||||
|
observedPropState &&
|
||||||
|
React.createElement(Component, {
|
||||||
|
...normalProps,
|
||||||
|
...observedPropState,
|
||||||
|
} as Props)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
20
theatre/studio/src/utils/devStringify.ts
Normal file
20
theatre/studio/src/utils/devStringify.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {tightJsonStringify} from './tightJsonStringify'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stringifies any value given. If an object is given and `indentJSON` is true,
|
||||||
|
* then a developer-readable, command line friendly (not too spaced out, but with
|
||||||
|
* enough whitespace to be readable).
|
||||||
|
*/
|
||||||
|
export function devStringify(input: any, indentJSON: boolean = true): string {
|
||||||
|
try {
|
||||||
|
return typeof input === 'string'
|
||||||
|
? input
|
||||||
|
: typeof input === 'function' || input instanceof Error
|
||||||
|
? input.toString()
|
||||||
|
: indentJSON
|
||||||
|
? tightJsonStringify(input)
|
||||||
|
: JSON.stringify(input)
|
||||||
|
} catch (err) {
|
||||||
|
return input?.name || String(input)
|
||||||
|
}
|
||||||
|
}
|
98
theatre/studio/src/utils/invariant.ts
Normal file
98
theatre/studio/src/utils/invariant.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import {devStringify} from './devStringify'
|
||||||
|
|
||||||
|
type AllowedMessageTypes = string | number | object
|
||||||
|
|
||||||
|
/**
|
||||||
|
* invariants are like `expect` from jest or another testing library but
|
||||||
|
* for use in implementations and not just tests. If the `condition` passed
|
||||||
|
* to `invariant` is falsy then `message`, and optionally `found`, are thrown as a
|
||||||
|
* {@link InvariantError} which has a developer-readable and command line friendly
|
||||||
|
* stack trace and error message.
|
||||||
|
*/
|
||||||
|
export function invariant(
|
||||||
|
shouldBeTruthy: any,
|
||||||
|
message: (() => AllowedMessageTypes) | AllowedMessageTypes,
|
||||||
|
butFoundInstead?: any,
|
||||||
|
): asserts shouldBeTruthy {
|
||||||
|
if (!shouldBeTruthy) {
|
||||||
|
const isFoundArgGiven = arguments.length > 2
|
||||||
|
if (isFoundArgGiven) {
|
||||||
|
invariantThrow(message, butFoundInstead)
|
||||||
|
} else {
|
||||||
|
invariantThrow(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an error message with a developer-readable and command line friendly
|
||||||
|
* string of the argument `butFoundInstead`.
|
||||||
|
*
|
||||||
|
* Also see {@link invariant}, which accepts a condition.
|
||||||
|
*/
|
||||||
|
export function invariantThrow(
|
||||||
|
message: (() => AllowedMessageTypes) | AllowedMessageTypes,
|
||||||
|
butFoundInstead?: any,
|
||||||
|
): never {
|
||||||
|
const isFoundArgGiven = arguments.length > 1
|
||||||
|
const prefix = devStringify(
|
||||||
|
typeof message === 'function' ? message() : message,
|
||||||
|
)
|
||||||
|
const suffix = isFoundArgGiven
|
||||||
|
? `\nInstead found: ${devStringify(butFoundInstead)}`
|
||||||
|
: ''
|
||||||
|
throw new InvariantError(`Invariant: ${prefix}${suffix}`, butFoundInstead)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable exhaustive checking
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* function a(x: 'a' | 'b') {
|
||||||
|
* if (x === 'a') {
|
||||||
|
*
|
||||||
|
* } else if (x === 'b') {
|
||||||
|
*
|
||||||
|
* } else {
|
||||||
|
* invariantUnreachable(x)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function invariantUnreachable(x: never): never {
|
||||||
|
invariantThrow(
|
||||||
|
'invariantUnreachable encountered value which was supposed to be never',
|
||||||
|
x,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// regexes to remove lines from thrown error stacktraces
|
||||||
|
const AT_NODE_INTERNAL_RE = /^\s*at.+node:internal.+/gm
|
||||||
|
const AT_INVARIANT_RE = /^\s*(at|[^@]+@) (?:Object\.)?invariant.+/gm
|
||||||
|
const AT_TEST_HELPERS_RE = /^\s*(at|[^@]+@).+test\-helpers.+/gm
|
||||||
|
// const AT_WEB_MODULES = /^\s*(at|[^@]+@).+(web_modules|\-[a-f0-9]{8}\.js).*/gm
|
||||||
|
const AT_ASSORTED_HELPERS_RE =
|
||||||
|
/^\s*(at|[^@]+@).+(debounce|invariant|iif)\.[tj]s.*/gm
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `InvariantError` removes lines from the `Error.stack` stack trace string
|
||||||
|
* which cleans up the stack trace, making it more developer friendly to read.
|
||||||
|
*/
|
||||||
|
class InvariantError extends Error {
|
||||||
|
found: any
|
||||||
|
constructor(message: string, found?: any) {
|
||||||
|
super(message)
|
||||||
|
if (found !== undefined) {
|
||||||
|
this.found = found
|
||||||
|
}
|
||||||
|
// const before = this.stack
|
||||||
|
// prettier-ignore
|
||||||
|
this.stack = this.stack
|
||||||
|
?.replace(AT_INVARIANT_RE, "")
|
||||||
|
.replace(AT_ASSORTED_HELPERS_RE, "")
|
||||||
|
.replace(AT_TEST_HELPERS_RE, "")
|
||||||
|
.replace(AT_NODE_INTERNAL_RE, "")
|
||||||
|
// console.error({ before, after: this.stack })
|
||||||
|
}
|
||||||
|
}
|
27
theatre/studio/src/utils/tightJsonStringify.test.ts
Normal file
27
theatre/studio/src/utils/tightJsonStringify.test.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import {tightJsonStringify} from './tightJsonStringify'
|
||||||
|
describe('tightJsonStringify', () => {
|
||||||
|
it('matches a series of expectations', () => {
|
||||||
|
expect(tightJsonStringify({a: 1, b: 2, c: {y: 4, z: 745}}))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"{ \\"a\\": 1,
|
||||||
|
\\"b\\": 2,
|
||||||
|
\\"c\\": {
|
||||||
|
\\"y\\": 4,
|
||||||
|
\\"z\\": 745 } }"
|
||||||
|
`)
|
||||||
|
expect(tightJsonStringify(true)).toMatchInlineSnapshot(`"true"`)
|
||||||
|
expect(tightJsonStringify('Already a string')).toMatchInlineSnapshot(
|
||||||
|
`"\\"Already a string\\""`,
|
||||||
|
)
|
||||||
|
expect(tightJsonStringify({a: 1, b: {c: [1, 2, {d: 4}], e: 8}}))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"{ \\"a\\": 1,
|
||||||
|
\\"b\\": {
|
||||||
|
\\"c\\": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
{ \\"d\\": 4 } ],
|
||||||
|
\\"e\\": 8 } }"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
30
theatre/studio/src/utils/tightJsonStringify.ts
Normal file
30
theatre/studio/src/utils/tightJsonStringify.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Stringifies an object in a developer-readable, command line friendly way
|
||||||
|
* (not too spaced out, but with enough whitespace to be readable).
|
||||||
|
*
|
||||||
|
* e.g.
|
||||||
|
* ```ts
|
||||||
|
* tightJsonStringify({a:1, b: {c: [1, 2, {d: 4}], e: 8}})
|
||||||
|
* ```
|
||||||
|
* becomes
|
||||||
|
* ```json
|
||||||
|
* { "a": 1,
|
||||||
|
* "b": {
|
||||||
|
* "c": [
|
||||||
|
* 1,
|
||||||
|
* 2,
|
||||||
|
* { "d": 4 } ],
|
||||||
|
* "e": 8 } }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Also, see the examples in [`./tightJsonStringify.test.ts`](./tightJsonStringify.test.ts)
|
||||||
|
*/
|
||||||
|
export function tightJsonStringify(
|
||||||
|
obj: any,
|
||||||
|
replacer?: ((this: any, key: string, value: any) => any) | undefined,
|
||||||
|
) {
|
||||||
|
return JSON.stringify(obj, replacer, 2)
|
||||||
|
.replace(/^([\{\[])\n (\s+)/, '$1$2')
|
||||||
|
.replace(/(\n[ ]+[\{\[])\n\s+/g, '$1 ')
|
||||||
|
.replace(/\n\s*([\]\}])/g, ' $1')
|
||||||
|
}
|
Loading…
Reference in a new issue