dev: Add configurable ITheatreLogger

Addresses the lack of options we currently have for surfacing issues in
our application via debugging tools. Prioritizes performance and
usability (visually) over clarity in some places that could have been
object mapped.

A logger with three separate audiences:

 * `internal`: Logs for developers maintaining Theatre.js
 * `dev`: Logs for developers using Theatre.js
 * `public`: Logs for everyone

This logger supports:
 * multiple logging levels (error, warn, debug, trace),
 * multiple audience levels (internal, dev, public),
 * multiple categories (general, todo, troubleshooting)
 * named and keyed loggers (e.g.
   `rootLogger.named("Project", project.id)`)
 * console styling with deterministic coloring
 * console devtool maintains accurate sourcemap link to logging origin
   (e.g. `coreExports.ts:71` as opposed to `logger.ts:45` or whatever)
 * swappable logger
 * customizable filtering
 * Accepts lazy `args`: `args: () => object` via
  `logger.lazy.<level>("message", () => <expensive computation>)` (e.g.
  `logger.lazy.debugDev("Loaded project state", () => ({ save: bigProject.exportToSaveable() }))`)
This commit is contained in:
Cole Lawrence 2022-05-08 21:29:04 -04:00
parent 5d61060828
commit 84daaaf94a
18 changed files with 1712 additions and 89 deletions

View file

@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'
import studio from '@theatre/studio'
import {getProject} from '@theatre/core'
import {Scene} from './Scene'
import {TheatreLoggerLevel} from '@theatre/shared/logger'
/**
* This is a basic example of using Theatre for manipulating the DOM.
*
@ -13,6 +14,16 @@ import {Scene} from './Scene'
studio.initialize()
ReactDOM.render(
<Scene project={getProject('Sample project')} />,
<Scene
project={getProject('Sample project', {
experiments: {
logging: {
internal: true,
dev: true,
min: TheatreLoggerLevel.TRACE,
},
},
})}
/>,
document.getElementById('root'),
)

View file

@ -0,0 +1,43 @@
import type {
ITheatreLoggerConfig,
ITheatreLoggingConfig,
} from '@theatre/shared/logger'
import {TheatreLoggerLevel} from '@theatre/shared/logger'
import {createTheatreInternalLogger} from '@theatre/shared/logger'
export type CoreLoggingConfig = Partial<{
logger: ITheatreLoggerConfig
logging: ITheatreLoggingConfig
}>
function noop() {}
export function _coreLogger(config?: CoreLoggingConfig) {
const internalMin = config?.logging?.internal
? config.logging.min ?? TheatreLoggerLevel.WARN
: Infinity // if not internal, then don't show any logs
const shouldDebugLogger = internalMin <= TheatreLoggerLevel.DEBUG
const shouldShowLoggerErrors = internalMin <= TheatreLoggerLevel.ERROR
const internal = createTheatreInternalLogger(undefined, {
_debug: shouldDebugLogger
? console.debug.bind(console, '_coreLogger(TheatreInternalLogger) debug')
: noop,
_error: shouldShowLoggerErrors
? console.error.bind(console, '_coreLogger(TheatreInternalLogger) error')
: noop,
})
if (config) {
const {logger, logging} = config
if (logger) internal.configureLogger(logger)
if (logging) internal.configureLogging(logging)
else {
// default to showing Theatre.js dev logs in non-production environments
internal.configureLogging({
dev: process.env.NODE_ENV !== 'production',
})
}
}
return internal.getLogger().named('Theatre')
}

View file

@ -17,6 +17,7 @@ import {isDerivation, valueDerivation} from '@theatre/dataverse'
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
import coreTicker from './coreTicker'
import type {ProjectId} from '@theatre/shared/utils/ids'
import {_coreLogger} from './_coreLogger'
export {types}
/**
@ -45,7 +46,6 @@ export {types}
* ```
*/
export function getProject(id: string, config: IProjectConfig = {}): IProject {
const {...restOfConfig} = config
const existingProject = projectsSingleton.get(id as ProjectId)
if (existingProject) {
if (process.env.NODE_ENV !== 'production') {
@ -61,20 +61,28 @@ export function getProject(id: string, config: IProjectConfig = {}): IProject {
return existingProject.publicApi
}
const rootLogger = _coreLogger(config.experiments)
const plogger = rootLogger.named('Project', id)
if (process.env.NODE_ENV !== 'production') {
validateName(id, 'projectName in Theatre.getProject(projectName)', true)
validateProjectIdOrThrow(id)
plogger._debug('validated projectName', {projectName: id})
}
if (config.state) {
if (process.env.NODE_ENV !== 'production') {
shallowValidateOnDiskState(id as ProjectId, config.state)
plogger._debug('shallow validated config.state on disk')
} else {
deepValidateOnDiskState(id as ProjectId, config.state)
plogger._debug('deep validated config.state on disk')
}
} else {
plogger._debug('no config.state')
}
return new TheatreProject(id, restOfConfig)
return new TheatreProject(id, config)
}
/**

View file

@ -18,9 +18,21 @@ import type {
SheetId,
SheetInstanceId,
} from '@theatre/shared/utils/ids'
import type {
ILogger,
ITheatreLoggerConfig,
ITheatreLoggingConfig,
} from '@theatre/shared/logger'
import {_coreLogger} from '@theatre/core/_coreLogger'
export type Conf = Partial<{
state: OnDiskState
experiments: ExperimentsConf
}>
export type ExperimentsConf = Partial<{
logger: ITheatreLoggerConfig
logging: ITheatreLoggingConfig
}>
export default class Project {
@ -47,14 +59,19 @@ export default class Project {
private _studio: Studio | undefined
type: 'Theatre_Project' = 'Theatre_Project'
readonly _logger: ILogger
constructor(
id: ProjectId,
readonly config: Conf = {},
readonly publicApi: TheatreProject,
) {
this._logger = _coreLogger(config.experiments).named('Project', id)
this._logger.traceDev('creating project')
this.address = {projectId: id}
// remove when logger is understood
this._logger._kapow('this is a "kapow"')
const onDiskStateAtom = new Atom<ProjectState>({
ahistoric: {
ahistoricStuff: '',
@ -94,6 +111,7 @@ export default class Project {
// let's give it one tick to attach itself
if (!this._studio) {
this._readyDeferred.resolve(undefined)
this._logger._trace('ready deferred resolved with no state')
}
}, 0)
} else {
@ -118,7 +136,7 @@ export default class Project {
`Project ${this.address.projectId} is already attached to studio ${this._studio.address.studioId}`,
)
} else {
console.warn(
this._logger.warnDev(
`Project ${this.address.projectId} is already attached to studio ${this._studio.address.studioId}`,
)
return

View file

@ -1,6 +1,10 @@
import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs'
import Project from '@theatre/core/projects/Project'
import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
import type {
ITheatreLoggerConfig,
ITheatreLoggingConfig,
} from '@theatre/shared/logger'
import type {ProjectAddress} from '@theatre/shared/utils/addresses'
import type {
ProjectId,
@ -19,6 +23,22 @@ export type IProjectConfig = {
* The state of the project, as [exported](https://docs.theatrejs.com/in-depth/#exporting) by the studio.
*/
state?: $IntentionalAny
experiments?: IProjectConfigExperiments
}
export type IProjectConfigExperiments = {
/**
* Defaults to using global `console` with style args.
*
* (TODO: check for browser environment before using style args)
*/
logger?: ITheatreLoggerConfig
/**
* Defaults:
* * `production` builds: console - error
* * `development` builds: console - error, warning
*/
logging?: ITheatreLoggingConfig
}
/**

View file

@ -14,7 +14,7 @@ import type {
} from './playbackControllers/DefaultPlaybackController'
import DefaultPlaybackController from './playbackControllers/DefaultPlaybackController'
import TheatreSequence from './TheatreSequence'
import logger from '@theatre/shared/logger'
import type {ILogger} from '@theatre/shared/logger'
import type {ISequence} from '..'
export type IPlaybackRange = [from: number, to: number]
@ -44,6 +44,7 @@ export default class Sequence {
readonly pointer: ISequence['pointer'] = pointer({root: this, path: []})
readonly $$isIdentityDerivationProvider = true
readonly _logger: ILogger
constructor(
readonly _project: Project,
@ -52,6 +53,10 @@ export default class Sequence {
readonly _subUnitsPerUnitD: IDerivation<number>,
playbackController?: IPlaybackController,
) {
this._logger = _project._logger
.named('Sheet', _sheet.address.sheetId)
.named('Instance', _sheet.address.sheetInstanceId)
this.address = {...this._sheet.address, sequenceName: 'default'}
this.publicApi = new TheatreSequence(this)
@ -140,13 +145,13 @@ export default class Sequence {
this.pause()
if (process.env.NODE_ENV !== 'production') {
if (typeof position !== 'number') {
logger.error(
this._logger.errorDev(
`value t in sequence.position = t must be a number. ${typeof position} given`,
)
position = 0
}
if (position < 0) {
logger.error(
this._logger.errorDev(
`sequence.position must be a positive number. ${position} given`,
)
position = 0
@ -226,7 +231,7 @@ export default class Sequence {
}
if (range[1] > sequenceDuration) {
logger.warn(
this._logger.warnDev(
`Argument conf.range[1] in sequence.play(conf) cannot be longer than the duration of the sequence, which is ${sequenceDuration}s. ${JSON.stringify(
range[1],
)} given.`,

View file

@ -1,4 +1,3 @@
import logger from '@theatre/shared/logger'
import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs'
import {defer} from '@theatre/shared/utils/defer'
import type Sequence from './Sequence'
@ -235,11 +234,12 @@ export default class TheatreSequence implements ISequence {
direction: IPlaybackDirection
}>,
): Promise<boolean> {
if (privateAPI(this)._project.isReady()) {
return privateAPI(this).play(conf)
const priv = privateAPI(this)
if (priv._project.isReady()) {
return priv.play(conf)
} else {
if (process.env.NODE_ENV !== 'production') {
logger.warn(
priv._logger.warnDev(
`You seem to have called sequence.play() before the project has finished loading.\n` +
`This would **not** a problem in production when using '@theatre/core', since Theatre loads instantly in core mode. ` +
`However, when using '@theatre/studio', it takes a few milliseconds for it to load your project's state, ` +

View file

@ -5,7 +5,7 @@ import type {
} from '@theatre/core/projects/store/types/SheetState_Historic'
import type {IDerivation, Pointer} from '@theatre/dataverse'
import {ConstantDerivation, prism, val} from '@theatre/dataverse'
import logger from '@theatre/shared/logger'
import type {IUtilContext} from '@theatre/shared/logger'
import type {SerializableValue} from '@theatre/shared/utils/types'
import UnitBezier from 'timing-function/lib/UnitBezier'
@ -26,6 +26,7 @@ export type InterpolationTriple = {
// 2. Caching propConfig.deserializeAndSanitize(value)
export default function interpolationTripleAtPosition(
ctx: IUtilContext,
trackP: Pointer<TrackData | undefined>,
timeD: IDerivation<number>,
): IDerivation<InterpolationTriple | undefined> {
@ -37,9 +38,9 @@ export default function interpolationTripleAtPosition(
if (!track) {
return new ConstantDerivation(undefined)
} else if (track.type === 'BasicKeyframedTrack') {
return _forKeyframedTrack(track, timeD)
return _forKeyframedTrack(ctx, track, timeD)
} else {
logger.error(`Track type not yet supported.`)
ctx.logger.error(`Track type not yet supported.`)
return new ConstantDerivation(undefined)
}
},
@ -60,6 +61,7 @@ type IStartedState = {
type IState = {started: false} | IStartedState
function _forKeyframedTrack(
ctx: IUtilContext,
track: BasicKeyframedTrack,
timeD: IDerivation<number>,
): IDerivation<InterpolationTriple | undefined> {
@ -70,7 +72,7 @@ function _forKeyframedTrack(
const time = timeD.getValue()
if (!state.started || time < state.validFrom || state.validTo <= time) {
stateRef.current = state = updateState(timeD, track)
stateRef.current = state = updateState(ctx, timeD, track)
}
return state.der.getValue()
@ -80,6 +82,7 @@ function _forKeyframedTrack(
const undefinedConstD = new ConstantDerivation(undefined)
function updateState(
ctx: IUtilContext,
progressionD: IDerivation<number>,
track: BasicKeyframedTrack,
): IStartedState {
@ -100,7 +103,7 @@ function updateState(
if (!currentKeyframe) {
if (process.env.NODE_ENV !== 'production') {
logger.error(`Bug here`)
ctx.logger.error(`Bug here`)
}
return states.error
}
@ -112,7 +115,7 @@ function updateState(
return states.beforeFirstKeyframe(currentKeyframe)
} else {
if (process.env.NODE_ENV !== 'production') {
logger.error(`Bug here`)
ctx.logger.error(`Bug here`)
}
return states.error
// note: uncomment these if we support starting with currentPointIndex != 0

View file

@ -25,6 +25,7 @@ import type SheetObjectTemplate from './SheetObjectTemplate'
import TheatreSheetObject from './TheatreSheetObject'
import type {Interpolator, PropTypeConfig} from '@theatre/core/propTypes'
import {getPropConfigByPath} from '@theatre/shared/propTypes/utils'
import type {ILogger, IUtilContext} from '@theatre/shared/logger'
/**
* Internally, the sheet's actual configured value is not a specific type, since we
@ -50,12 +51,20 @@ export default class SheetObject implements IdentityDerivationProvider {
readonly publicApi: TheatreSheetObject
private readonly _initialValue = new Atom<SheetObjectPropsValue>({})
private readonly _cache = new SimpleCache()
readonly _logger: ILogger
private readonly _internalUtilCtx: IUtilContext
constructor(
readonly sheet: Sheet,
readonly template: SheetObjectTemplate,
readonly nativeObject: unknown,
) {
this._logger = sheet._logger.named(
'SheetObject',
template.address.objectKey,
)
this._logger._trace('creating object')
this._internalUtilCtx = {logger: this._logger.downgrade.internal()}
this.address = {
...template.address,
sheetInstanceId: sheet.address.sheetInstanceId,
@ -229,7 +238,7 @@ export default class SheetObject implements IdentityDerivationProvider {
const timeD = this.sheet.getSequence().positionDerivation
return interpolationTripleAtPosition(trackP, timeD)
return interpolationTripleAtPosition(this._internalUtilCtx, trackP, timeD)
}
get propsP(): Pointer<SheetObjectPropsValue> {

View file

@ -8,6 +8,7 @@ import {Atom, valueDerivation} from '@theatre/dataverse'
import type SheetTemplate from './SheetTemplate'
import type {ObjectAddressKey, SheetInstanceId} from '@theatre/shared/utils/ids'
import type {StrictRecord} from '@theatre/shared/utils/types'
import type {ILogger} from '@theatre/shared/logger'
type SheetObjectMap = StrictRecord<ObjectAddressKey, SheetObject>
@ -28,11 +29,14 @@ export default class Sheet {
readonly project: Project
readonly objectsP = this._objects.pointer
type: 'Theatre_Sheet' = 'Theatre_Sheet'
readonly _logger: ILogger
constructor(
readonly template: SheetTemplate,
public readonly instanceId: SheetInstanceId,
) {
this._logger = template.project._logger.named('Sheet', instanceId)
this._logger._trace('creating sheet')
this.project = template.project
this.address = {
...template.address,

View file

@ -0,0 +1,174 @@
import {TheatreLoggerLevel, _LoggerLevel} from './logger'
import {_loggerShouldLog} from './logger'
describe('Theatre internal logger: shouldLog', () => {
testIncludes(
'TRACE dev/internal',
{
dev: true,
internal: true,
min: TheatreLoggerLevel.TRACE,
},
{
_ERROR: true,
_HMM: true,
_TODO: true,
ERROR_DEV: true,
ERROR_PUBLIC: true,
_KAPOW: true,
_WARN: true,
WARN_DEV: true,
WARN_PUBLIC: true,
_DEBUG: true,
DEBUG_DEV: true,
_TRACE: true,
TRACE_DEV: true,
},
)
testIncludes(
'DEBUG dev/internal',
{
dev: true,
internal: true,
min: TheatreLoggerLevel.DEBUG,
},
{
_ERROR: true,
_HMM: true,
_TODO: true,
ERROR_DEV: true,
ERROR_PUBLIC: true,
_WARN: true,
_KAPOW: true,
WARN_DEV: true,
WARN_PUBLIC: true,
_DEBUG: true,
DEBUG_DEV: true,
_TRACE: false,
TRACE_DEV: false,
},
)
testIncludes(
'TRACE dev',
{
dev: true,
internal: false,
min: TheatreLoggerLevel.TRACE,
},
{
_ERROR: false,
_HMM: false,
_KAPOW: false,
_TODO: false,
ERROR_DEV: true,
ERROR_PUBLIC: true,
_WARN: false,
WARN_DEV: true,
WARN_PUBLIC: true,
_DEBUG: false,
DEBUG_DEV: true,
_TRACE: false,
TRACE_DEV: true,
},
)
testIncludes(
'TRACE',
{
dev: false,
internal: false,
min: TheatreLoggerLevel.TRACE,
},
{
_ERROR: false,
_HMM: false,
_KAPOW: false,
_TODO: false,
ERROR_DEV: false,
ERROR_PUBLIC: true,
_WARN: false,
WARN_DEV: false,
WARN_PUBLIC: true,
_DEBUG: false,
DEBUG_DEV: false,
_TRACE: false,
TRACE_DEV: false,
},
)
testIncludes(
'WARN dev',
{
dev: true,
internal: false,
min: TheatreLoggerLevel.WARN,
},
{
_ERROR: false,
_HMM: false,
_KAPOW: false,
_TODO: false,
ERROR_DEV: true,
ERROR_PUBLIC: true,
_WARN: false,
WARN_DEV: true,
WARN_PUBLIC: true,
_DEBUG: false,
DEBUG_DEV: false,
_TRACE: false,
TRACE_DEV: false,
},
)
testIncludes(
'TRACE internal',
{
dev: false,
internal: true,
min: TheatreLoggerLevel.TRACE,
},
{
_ERROR: true,
_HMM: true,
_TODO: true,
ERROR_DEV: false,
ERROR_PUBLIC: true,
_KAPOW: true,
_WARN: true,
WARN_DEV: false,
WARN_PUBLIC: true,
_DEBUG: true,
DEBUG_DEV: false,
_TRACE: true,
TRACE_DEV: false,
},
)
})
function testIncludes(
name: string,
config: {
dev: boolean
internal: boolean
min: TheatreLoggerLevel
},
expectations: {[P in keyof typeof _LoggerLevel]: boolean},
) {
test.each(Object.entries(expectations))(
`${name} + %s = %s`,
(level, expectIsIncluded) => {
const actual = _loggerShouldLog(config, _LoggerLevel[level])
if (actual !== expectIsIncluded) {
const stackless = new Error(
`Expected shouldLog({ dev: ${config.dev}, internal: ${
config.internal
}, max: ${TheatreLoggerLevel[config.min]} }, ${level}) = ${actual}`,
)
stackless.stack = undefined // stack is not useful in test
throw stackless
}
},
)
}

View file

@ -0,0 +1,233 @@
import type {
ITheatreConsoleLogger,
_LogFns,
ITheatreInternalLoggerOptions,
IUtilLogger,
ILogger,
} from './logger'
import {createTheatreInternalLogger} from './logger'
const DEBUG_LOGGER = false
function noop() {}
export function describeLogger(
name: string,
body: (setup: () => ReturnType<typeof setupFn>) => void,
) {
describe(name, () => {
body(
setupFn.bind(null, {
_debug: DEBUG_LOGGER ? console.log.bind(console, name) : noop,
_error: console.error.bind(console, name),
}),
)
})
}
function setupFn(options: ITheatreInternalLoggerOptions) {
const con = spyConsole()
const internal = createTheatreInternalLogger(con, options)
function t(logger = internal.getLogger()) {
return {
expectExcluded(kind: keyof _LogFns) {
try {
const message = `${kind} message`
logger[kind](message)
expect(con.debug).not.toBeCalled()
expect(con.info).not.toBeCalled()
expect(con.warn).not.toBeCalled()
expect(con.error).not.toBeCalled()
} catch (err) {
throw new LoggerTestError(
`Expected logger.${kind}(...) excluded from logging\n${(
err as Error
).toString()}`,
)
}
},
expectIncluded(
kind: keyof _LogFns,
expectOutputted: keyof ITheatreConsoleLogger,
includes: TestLoggerIncludes = [],
) {
try {
const message = `${kind} message`
logger[kind](message)
if (expectOutputted !== 'debug') {
expect(con.debug).not.toBeCalled()
}
if (expectOutputted !== 'info') {
expect(con.info).not.toBeCalled()
}
if (expectOutputted !== 'warn') {
expect(con.warn).not.toBeCalled()
}
if (expectOutputted !== 'error') {
expect(con.error).not.toBeCalled()
}
expectLastCalledWith(con[expectOutputted], includes)
} catch (err) {
throw new LoggerTestError(
`Expected logger.${kind}(...) included and outputted via console.${expectOutputted}(...)\n${(
err as Error
).toString()}`,
)
}
con[expectOutputted].mockReset()
},
named(name: string, key?: string | number) {
return t(logger.named(name, key))
},
downgrade: objMap(
logger.downgrade,
([audience, downgradeFn]) =>
() =>
setupUtilLogger(downgradeFn(), audience, con),
),
}
}
return {
internal,
con,
t,
}
}
function expectLastCalledWith(
fn: jest.MockInstance<any, any[]>,
includes: TestLoggerIncludes,
) {
expect(fn).toBeCalled()
if (includes.length > 0) {
const [lastCall] = fn.mock.calls
const concat = lastCall.filter(Boolean).map(String).join(', ')
const errors = includes.flatMap((includeTest) => {
if (typeof includeTest === 'string') {
return concat.includes(includeTest)
? []
: [`didn't include ${JSON.stringify(includeTest)}`]
} else if ('test' in includeTest) {
return includeTest.test(concat)
? []
: [`didn't match ${String(includeTest)}`]
} else if (typeof includeTest.not === 'string') {
return concat.includes(includeTest.not)
? [`wasn't supposed to include ${JSON.stringify(includeTest.not)}`]
: []
} else if ('test' in includeTest.not) {
return includeTest.not.test(concat)
? [`wasn't supposed to match ${String(includeTest.not)}`]
: []
}
})
if (errors.length > 0) {
throw new LoggerTestError(
`Last called with ${JSON.stringify(concat)}, but ${errors.join(', ')}`,
)
}
}
}
function objMap<T, U>(
template: T,
eachEntry: <P extends keyof T>(entry: [name: P, value: T[P]]) => U,
): {[P in keyof T]: U} {
// @ts-ignore
return Object.fromEntries(
Object.entries(template).map((entry) => {
// @ts-ignore
return [entry[0], eachEntry(entry)]
}),
)
}
type TestLoggerIncludes = ((string | RegExp) | {not: string | RegExp})[]
function setupUtilLogger(
logger: IUtilLogger,
audience: keyof ILogger['downgrade'],
con: jest.Mocked<ITheatreConsoleLogger>,
) {
return {
named(name: string, key?: string) {
return setupUtilLogger(logger.named(name, key), audience, con)
},
expectExcluded(kind: keyof IUtilLogger) {
try {
const message = `${audience} ${kind} message`
logger[kind](message)
expect(con.debug).not.toBeCalled()
expect(con.info).not.toBeCalled()
expect(con.warn).not.toBeCalled()
expect(con.error).not.toBeCalled()
} catch (err) {
throw new LoggerTestError(
`Expected "${audience}" logger.${kind}(...) excluded from logging\n${(
err as Error
).toString()}`,
)
}
},
expectIncluded(
kind: keyof IUtilLogger,
expectOutputted: keyof ITheatreConsoleLogger,
includes: TestLoggerIncludes = [],
) {
try {
const message = `${audience} ${kind} message`
logger[kind](message)
if (expectOutputted !== 'debug') {
expect(con.debug).not.toBeCalled()
}
if (expectOutputted !== 'info') {
expect(con.info).not.toBeCalled()
}
if (expectOutputted !== 'warn') {
expect(con.warn).not.toBeCalled()
}
if (expectOutputted !== 'error') {
expect(con.error).not.toBeCalled()
}
expectLastCalledWith(con[expectOutputted], includes)
} catch (err) {
throw new LoggerTestError(
`Expected "${audience}" logger.${kind}(...) included and outputted via console.${expectOutputted}(...)\n${(
err as Error
).toString()}`,
)
}
con[expectOutputted].mockReset()
},
}
}
function spyConsole(): jest.Mocked<ITheatreConsoleLogger> {
return {
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
}
}
// remove these lines from thrown errors
const AT_NODE_INTERNAL_RE = /^\s*at.+node:internal.+/gm
const AT_TEST_HELPERS_RE = /^\s*(at|[^@]+@).+test\-helpers.+/gm
/** `TestError` removes the invariant line & test-helpers from the `Error.stack` */
class LoggerTestError extends Error {
found: any
constructor(message: string, found?: any) {
super(message)
if (found) {
this.found = found
}
// const before = this.stack
// prettier-ignore
this.stack = this.stack
?.replace(AT_TEST_HELPERS_RE, "")
.replace(AT_NODE_INTERNAL_RE, "")
// console.error({ before, after: this.stack })
}
}

View file

@ -0,0 +1,249 @@
import {TheatreLoggerLevel} from './logger'
import {describeLogger} from './logger.test-helpers'
describeLogger('Theatre internal logger', (setup) => {
describe('default logger', () => {
test('it reports public messages', () => {
const t = setup().t()
t.expectIncluded('errorPublic', 'error')
t.expectIncluded('warnPublic', 'warn')
})
test('it does not report dev messages', () => {
const t = setup().t()
t.expectExcluded('errorDev')
t.expectExcluded('warnDev')
t.expectExcluded('debugDev')
t.expectExcluded('traceDev')
})
test('it does not report internal messages', () => {
const t = setup().t()
t.expectExcluded('_hmm')
t.expectExcluded('_kapow')
t.expectExcluded('_debug')
t.expectExcluded('_trace')
})
})
describe('custom logging', () => {
test('it can include all dev and internal logs', () => {
const h = setup()
const initial = h.t()
h.internal.configureLogging({
dev: true,
internal: true,
min: TheatreLoggerLevel.TRACE,
})
const t = h.t()
// initial logger will not have been able to acknowledge
// the logging config update.
initial.expectExcluded('_hmm')
initial.expectExcluded('errorDev')
t.expectIncluded('_hmm', 'error')
t.expectIncluded('_kapow', 'warn')
t.expectIncluded('_debug', 'info')
t.expectIncluded('_trace', 'debug')
t.expectIncluded('errorDev', 'error')
t.expectIncluded('warnDev', 'warn')
t.expectIncluded('debugDev', 'info')
t.expectIncluded('traceDev', 'debug')
})
test('it can include WARN level dev and internal logs', () => {
const h = setup()
h.internal.configureLogging({
dev: true,
internal: true,
min: TheatreLoggerLevel.WARN,
})
const t = h.t()
t.expectIncluded('_hmm', 'error')
t.expectIncluded('_kapow', 'warn')
t.expectIncluded('errorDev', 'error')
t.expectIncluded('warnDev', 'warn')
t.expectExcluded('_debug')
t.expectExcluded('_trace')
t.expectExcluded('debugDev')
t.expectExcluded('traceDev')
})
test('it can include WARN level dev logs', () => {
const h = setup()
h.internal.configureLogging({
dev: true,
min: TheatreLoggerLevel.WARN,
})
const t = h.t()
t.expectIncluded('errorDev', 'error')
t.expectIncluded('warnDev', 'warn')
t.expectExcluded('_hmm')
t.expectExcluded('_kapow')
t.expectExcluded('_debug')
t.expectExcluded('_trace')
t.expectExcluded('debugDev')
t.expectExcluded('traceDev')
})
})
describe('named and keys', () => {
test('default with no name has no colors', () => {
const h = setup()
const app = h.t().named('App')
const appK2 = app.named('K', 1)
app.expectIncluded('errorPublic', 'error', ['App', {not: /K.*#1/}])
app.expectIncluded('warnPublic', 'warn', ['App', {not: /K.*#1/}])
appK2.expectIncluded('errorPublic', 'error', ['App', /App.*K.*#1/])
appK2.expectIncluded('warnPublic', 'warn', ['App', /App.*K.*#1/])
})
})
describe('named gets colors', () => {
test('default with no name has no colors', () => {
const h = setup()
const t = h.t()
t.expectIncluded('errorPublic', 'error', [{not: 'color:'}])
t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}])
})
test('default with name has color', () => {
const h = setup()
const t = h.t().named('Test1')
t.expectIncluded('errorPublic', 'error', ['color:', 'Test1'])
t.expectIncluded('warnPublic', 'warn', ['color:', 'Test1'])
})
test('consoleStyle: false with name does not have color', () => {
const h = setup()
h.internal.configureLogging({
consoleStyle: false,
})
const t = h.t().named('Test1')
t.expectIncluded('errorPublic', 'error', [{not: 'color:'}, 'Test1'])
t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}, 'Test1'])
})
test('console style: false with name does not have color', () => {
const h = setup()
h.internal.configureLogger({
type: 'console',
console: h.con,
style: false,
})
const t = h.t().named('Test1')
t.expectIncluded('errorPublic', 'error', [{not: 'color:'}, 'Test1'])
t.expectIncluded('warnPublic', 'warn', [{not: 'color:'}, 'Test1'])
})
})
describe('downgrade', () => {
test('.downgrade.public() with defaults', () => {
const h = setup()
const publ = h.t().downgrade.public()
publ.expectIncluded('error', 'error')
publ.expectIncluded('warn', 'warn')
publ.expectExcluded('debug')
publ.expectExcluded('trace')
})
test('.downgrade.dev() with defaults', () => {
const h = setup()
const dev = h.t().downgrade.dev()
dev.expectExcluded('error')
dev.expectExcluded('warn')
dev.expectExcluded('debug')
dev.expectExcluded('trace')
})
test('.downgrade.internal() with defaults', () => {
const h = setup()
const internal = h.t().downgrade.internal()
internal.expectExcluded('error')
internal.expectExcluded('warn')
internal.expectExcluded('debug')
internal.expectExcluded('trace')
})
test('.downgrade.internal() can be named', () => {
const h = setup()
h.internal.configureLogging({
internal: true,
min: TheatreLoggerLevel.TRACE,
})
const internal = h.t().downgrade.internal()
const appleInternal = internal.named('Apple')
internal.expectIncluded('error', 'error', [{not: 'Apple'}])
internal.expectIncluded('warn', 'warn', [{not: 'Apple'}])
internal.expectIncluded('debug', 'info', [{not: 'Apple'}])
internal.expectIncluded('trace', 'debug', [{not: 'Apple'}])
appleInternal.expectIncluded('error', 'error', ['Apple'])
appleInternal.expectIncluded('warn', 'warn', ['Apple'])
appleInternal.expectIncluded('debug', 'info', ['Apple'])
appleInternal.expectIncluded('trace', 'debug', ['Apple'])
})
test('.downgrade.public() debug/trace warns internal', () => {
const h = setup()
{
h.internal.configureLogging({
internal: true,
})
const publ = h.t().downgrade.public()
publ.expectIncluded('error', 'error', [{not: 'filtered out'}])
publ.expectIncluded('warn', 'warn', [{not: 'filtered out'}])
// warnings go through internal loggers since public loggers do not have a trace or debug level
publ.expectIncluded('debug', 'warn', ['filtered out'])
publ.expectIncluded('trace', 'warn', ['filtered out'])
}
{
h.internal.configureLogging({
dev: true,
})
const publ = h.t().downgrade.public()
publ.expectIncluded('error', 'error', [{not: /filtered out/}])
publ.expectIncluded('warn', 'warn', [{not: /filtered out/}])
// warnings only go through internal loggers
publ.expectExcluded('debug')
publ.expectExcluded('trace')
}
})
})
})

View file

@ -0,0 +1,796 @@
/** @public configuration type */
export interface ITheatreLogger {
error(level: ITheatreLogMeta, message: string, args?: Loggable): void
warn(level: ITheatreLogMeta, message: string, args?: Loggable): void
debug(level: ITheatreLogMeta, message: string, args?: Loggable): void
trace(level: ITheatreLogMeta, message: string, args?: Loggable): void
}
type ITheatreLogMeta = Readonly<{
audience: 'public' | 'dev' | 'internal'
category: 'general' | 'todo' | 'troubleshooting'
level: TheatreLoggerLevel
}>
/** @public configuration type */
export interface ITheatreConsoleLogger {
/** ERROR level logs */
error(message: string, ...args: any[]): void
/** WARN level logs */
warn(message: string, ...args: any[]): void
/** DEBUG level logs */
info(message: string, ...args: any[]): void
/** TRACE level logs */
debug(message: string, ...args: any[]): void
}
/**
* "Downgraded" {@link ILogger} for passing down to utility functions.
*
* A util logger is usually back by some specific {@link _Audience}.
*/
export interface IUtilLogger {
/** Usually equivalent to `console.error`. */
error(message: string, args?: object): void
/** Usually equivalent to `console.warn`. */
warn(message: string, args?: object): void
/** Usually equivalent to `console.info`. */
debug(message: string, args?: object): void
/** Usually equivalent to `console.debug`. */
trace(message: string, args?: object): void
named(name: string, key?: string): IUtilLogger
}
type Loggable = Record<string, any>
type LogFn = (message: string, args?: Loggable) => void
/**
* Allow for the arguments to only be computed if the level is included.
* If the level is not included, then the fn will still be passed to the filtered
* function.
*/
type LazyLogFn = (message: string, args: () => Loggable) => void
function lazy(f: LogFn): LazyLogFn {
return function lazyLogIncluded(m, lazyArg) {
return f(m, lazyArg())
}
}
export type _LogFns = Readonly<
{
[P in keyof typeof LEVELS]: LogFn
}
>
export type _LazyLogFns = Readonly<
{
[P in keyof typeof LEVELS]: LazyLogFn
}
>
/** Internal library logger */
export interface ILogger extends _LogFns {
named(name: string, key?: string | number): ILogger
lazy: _LazyLogFns
readonly downgrade: {
internal(): IUtilLogger
dev(): IUtilLogger
public(): IUtilLogger
}
}
export type ITheatreLoggerConfig =
| /** default {@link console} */
'console'
| {
type: 'console'
/** default `true` */
style?: boolean
/** default {@link console} */
console?: ITheatreConsoleLogger
}
| {
type: 'named'
named(names: string[]): ITheatreLogger
}
| {
type: 'keyed'
keyed(
nameAndKeys: {
name: string
key?: string | number
}[],
): ITheatreLogger
}
export type ITheatreLogSource = {names: {name: string; key?: number | string}[]}
export type ITheatreLogIncludes = {
/**
* General information max level.
* e.g. `Project imported might be corrupted`
*/
min?: TheatreLoggerLevel
/**
* Include logs meant for developers using Theatre.js
* e.g. `Created new project 'Abc' with options {...}`
*
* defaults to `true` if `internal: true` or defaults to `false`.
*/
dev?: boolean
/**
* Include logs meant for internal development of Theatre.js
* e.g. `Migrated project 'Abc' { duration_ms: 34, from_version: 1, to_version: 3, imported_settings: false }`
*
* defaults to `false`
*/
internal?: boolean
}
export type ITheatreLoggingConfig = ITheatreLogIncludes & {
include?: (source: ITheatreLogSource) => ITheatreLogIncludes
consoleStyle?: boolean
}
/** @internal */
enum _Category {
GENERAL = 1 << 0,
TODO = 1 << 1,
TROUBLESHOOTING = 1 << 2,
}
/** @internal */
enum _Audience {
/** Logs for developers of Theatre.js */
INTERNAL = 1 << 3,
/** Logs for developers using Theatre.js */
DEV = 1 << 4,
/** Logs for users of the app using Theatre.js */
PUBLIC = 1 << 5,
}
export enum TheatreLoggerLevel {
TRACE = 1 << 6,
DEBUG = 1 << 7,
WARN = 1 << 8,
ERROR = 1 << 9,
}
/**
* @internal Theatre internal "dev" levels are odd numbers
*
* You can check if a level is odd quickly by doing `level & 1 === 1`
*/
export enum _LoggerLevel {
/** The highest logging level number. */
ERROR_PUBLIC = TheatreLoggerLevel.ERROR |
_Audience.PUBLIC |
_Category.GENERAL,
ERROR_DEV = TheatreLoggerLevel.ERROR | _Audience.DEV | _Category.GENERAL,
/** @internal this was an unexpected event */
_HMM = TheatreLoggerLevel.ERROR |
_Audience.INTERNAL |
_Category.TROUBLESHOOTING,
_TODO = TheatreLoggerLevel.ERROR | _Audience.INTERNAL | _Category.TODO,
_ERROR = TheatreLoggerLevel.ERROR | _Audience.INTERNAL | _Category.GENERAL,
WARN_PUBLIC = TheatreLoggerLevel.WARN | _Audience.PUBLIC | _Category.GENERAL,
WARN_DEV = TheatreLoggerLevel.WARN | _Audience.DEV | _Category.GENERAL,
/** @internal surface this in this moment, but it probably shouldn't be left in the code after debugging. */
_KAPOW = TheatreLoggerLevel.WARN |
_Audience.INTERNAL |
_Category.TROUBLESHOOTING,
_WARN = TheatreLoggerLevel.WARN | _Audience.INTERNAL | _Category.GENERAL,
DEBUG_DEV = TheatreLoggerLevel.DEBUG | _Audience.DEV | _Category.GENERAL,
/** @internal debug logs for implementation details */
_DEBUG = TheatreLoggerLevel.DEBUG | _Audience.INTERNAL | _Category.GENERAL,
/** trace logs like when the project is saved */
TRACE_DEV = TheatreLoggerLevel.TRACE | _Audience.DEV | _Category.GENERAL,
/**
* The lowest logging level number.
* @internal trace logs for implementation details
*/
_TRACE = TheatreLoggerLevel.TRACE | _Audience.INTERNAL | _Category.GENERAL,
}
const LEVELS = {
_hmm: getLogMeta(_LoggerLevel._HMM),
_todo: getLogMeta(_LoggerLevel._TODO),
_error: getLogMeta(_LoggerLevel._ERROR),
errorDev: getLogMeta(_LoggerLevel.ERROR_DEV),
errorPublic: getLogMeta(_LoggerLevel.ERROR_PUBLIC),
_kapow: getLogMeta(_LoggerLevel._KAPOW),
_warn: getLogMeta(_LoggerLevel._WARN),
warnDev: getLogMeta(_LoggerLevel.WARN_DEV),
warnPublic: getLogMeta(_LoggerLevel.WARN_PUBLIC),
_debug: getLogMeta(_LoggerLevel._DEBUG),
debugDev: getLogMeta(_LoggerLevel.DEBUG_DEV),
_trace: getLogMeta(_LoggerLevel._TRACE),
traceDev: getLogMeta(_LoggerLevel.TRACE_DEV),
}
function getLogMeta(level: _LoggerLevel): ITheatreLogMeta {
return Object.freeze({
audience: hasFlag(level, _Audience.INTERNAL)
? 'internal'
: hasFlag(level, _Audience.DEV)
? 'dev'
: 'public',
category: hasFlag(level, _Category.TROUBLESHOOTING)
? 'troubleshooting'
: hasFlag(level, _Category.TODO)
? 'todo'
: 'general',
level:
// I think this is equivalent... but I'm not using it until we have tests.
// this code won't really impact performance much anyway, since it's just computed once
// up front.
// level &
// (TheatreLoggerLevel.TRACE |
// TheatreLoggerLevel.DEBUG |
// TheatreLoggerLevel.WARN |
// TheatreLoggerLevel.ERROR),
hasFlag(level, TheatreLoggerLevel.ERROR)
? TheatreLoggerLevel.ERROR
: hasFlag(level, TheatreLoggerLevel.WARN)
? TheatreLoggerLevel.WARN
: hasFlag(level, TheatreLoggerLevel.DEBUG)
? TheatreLoggerLevel.DEBUG
: // no other option
TheatreLoggerLevel.TRACE,
})
}
/**
* This is a helper function to determine whether the logger level has a bit flag set.
*
* Flags are interesting, because they give us an opportunity to very easily set up filtering
* based on category and level. This is not available from public api, yet, but it's a good
* start.
*/
function hasFlag(level: _LoggerLevel, flag: number): boolean {
return (level & flag) === flag
}
/**
* @internal
*
* You'd think max, means number "max", but since we use this system of bit flags,
* we actually need to go the other way, with comparisons being math less than.
*
* NOTE: Keep this in the same file as {@link _Audience} to ensure basic compilers
* can inline the enum values.
*/
function shouldLog(
includes: Required<ITheatreLogIncludes>,
level: _LoggerLevel,
) {
return (
((level & _Audience.PUBLIC) === _Audience.PUBLIC
? true
: (level & _Audience.DEV) === _Audience.DEV
? includes.dev
: (level & _Audience.INTERNAL) === _Audience.INTERNAL
? includes.internal
: false) && includes.min <= level
)
}
export {shouldLog as _loggerShouldLog}
type InternalLoggerStyleRef = {
italic?: RegExp
bold?: RegExp
color?: (name: string) => string
collapseOnRE: RegExp
cssMemo: Map<string, string>
css(this: InternalLoggerStyleRef, name: string): string
collapsed(this: InternalLoggerStyleRef, name: string): string
}
type InternalLoggerRef = {
loggingConsoleStyle: boolean
loggerConsoleStyle: boolean
includes: Required<ITheatreLogIncludes>
filtered: (
this: ITheatreLogSource,
level: _LoggerLevel,
message: string,
args?: Loggable | (() => Loggable),
) => void
include: (obj: ITheatreLogSource) => ITheatreLogIncludes
create: (obj: ITheatreLogSource) => ILogger
creatExt: (obj: ITheatreLogSource) => ITheatreLogger
style: InternalLoggerStyleRef
named(
this: InternalLoggerRef,
parent: ITheatreLogSource,
name: string,
key?: number | string,
): ILogger
}
const DEFAULTS: InternalLoggerRef = {
loggingConsoleStyle: true,
loggerConsoleStyle: true,
includes: Object.freeze({
internal: false,
dev: false,
min: TheatreLoggerLevel.WARN,
}),
filtered: function defaultFiltered() {},
include: function defaultInclude() {
return {}
},
create: null!,
creatExt: null!,
named(this: InternalLoggerRef, parent, name, key) {
return this.create({
names: [...parent.names, {name, key}],
})
},
style: {
bold: undefined, // /Service$/
italic: undefined, // /Model$/
cssMemo: new Map<string, string>([
// handle empty names so we don't have to check for
// name.length > 0 during this.css('')
['', ''],
// bring a specific override
// ["Marker", "color:#aea9ff;font-size:0.75em;text-transform:uppercase"]
]),
collapseOnRE: /[a-z- ]+/g,
color: undefined,
// create collapsed name
// insert collapsed name into cssMemo with original's style
collapsed(this, name) {
if (name.length < 5) return name
const collapsed = name.replace(this.collapseOnRE, '')
if (!this.cssMemo.has(collapsed)) {
this.cssMemo.set(collapsed, this.css(name))
}
return collapsed
},
css(this, name): string {
const found = this.cssMemo.get(name)
if (found) return found
let css = `color:${
this.color?.(name) ??
`hsl(${
(name.charCodeAt(0) + name.charCodeAt(name.length - 1)) % 360
}, 100%, 60%)`
}`
if (this.bold?.test(name)) {
css += ';font-weight:600'
}
if (this.italic?.test(name)) {
css += ';font-style:italic'
}
this.cssMemo.set(name, css)
return css
},
},
}
/** @internal */
export type ITheatreInternalLogger = {
configureLogger(config: ITheatreLoggerConfig): void
configureLogging(config: ITheatreLoggingConfig): void
getLogger(): ILogger
}
export type ITheatreInternalLoggerOptions = {
_error?: (message: string, args?: object) => void
_debug?: (message: string, args?: object) => void
}
export function createTheatreInternalLogger(
useConsole: ITheatreConsoleLogger = console,
// Not yet, used, but good pattern to have in case we want to log something
// or report something interesting.
_options: ITheatreInternalLoggerOptions = {},
): ITheatreInternalLogger {
const ref: InternalLoggerRef = {...DEFAULTS, includes: {...DEFAULTS.includes}}
const createConsole = {
styled: createConsoleLoggerStyled.bind(ref, useConsole),
noStyle: createConsoleLoggerNoStyle.bind(ref, useConsole),
}
const createExtBound = createExtLogger.bind(ref)
function getConCreate() {
return ref.loggingConsoleStyle && ref.loggerConsoleStyle
? createConsole.styled
: createConsole.noStyle
}
ref.create = getConCreate()
return {
configureLogger(config) {
if (config === 'console') {
ref.loggerConsoleStyle = DEFAULTS.loggerConsoleStyle
ref.create = getConCreate()
} else if (config.type === 'console') {
ref.loggerConsoleStyle = config.style ?? DEFAULTS.loggerConsoleStyle
ref.create = getConCreate()
} else if (config.type === 'keyed') {
ref.creatExt = (source) => config.keyed(source.names)
ref.create = createExtBound
} else if (config.type === 'named') {
ref.creatExt = configNamedToKeyed.bind(null, config.named)
ref.create = createExtBound
}
},
configureLogging(config) {
ref.includes.dev = config.dev ?? DEFAULTS.includes.dev
ref.includes.internal = config.internal ?? DEFAULTS.includes.internal
ref.includes.min = config.min ?? DEFAULTS.includes.min
ref.include = config.include ?? DEFAULTS.include
ref.loggingConsoleStyle =
config.consoleStyle ?? DEFAULTS.loggingConsoleStyle
ref.create = getConCreate()
},
getLogger() {
return ref.create({names: []})
},
}
}
/** used by `configureLogger` for `'named'` */
function configNamedToKeyed(
namedFn: (names: string[]) => ITheatreLogger,
source: ITheatreLogSource,
): ITheatreLogger {
const names: string[] = []
for (let {name, key} of source.names) {
names.push(key == null ? name : `${name} (${key})`)
}
return namedFn(names)
}
function createExtLogger(
this: InternalLoggerRef,
source: ITheatreLogSource,
): ILogger {
const includes = {...this.includes, ...this.include(source)}
const f = this.filtered
const named = this.named.bind(this, source)
const ext = this.creatExt(source)
const _HMM = shouldLog(includes, _LoggerLevel._HMM)
const _TODO = shouldLog(includes, _LoggerLevel._TODO)
const _ERROR = shouldLog(includes, _LoggerLevel._ERROR)
const ERROR_DEV = shouldLog(includes, _LoggerLevel.ERROR_DEV)
const ERROR_PUBLIC = shouldLog(includes, _LoggerLevel.ERROR_PUBLIC)
const _WARN = shouldLog(includes, _LoggerLevel._WARN)
const _KAPOW = shouldLog(includes, _LoggerLevel._KAPOW)
const WARN_DEV = shouldLog(includes, _LoggerLevel.WARN_DEV)
const WARN_PUBLIC = shouldLog(includes, _LoggerLevel.WARN_PUBLIC)
const _DEBUG = shouldLog(includes, _LoggerLevel._DEBUG)
const DEBUG_DEV = shouldLog(includes, _LoggerLevel.DEBUG_DEV)
const _TRACE = shouldLog(includes, _LoggerLevel._TRACE)
const TRACE_DEV = shouldLog(includes, _LoggerLevel.TRACE_DEV)
const _hmm = _HMM
? ext.error.bind(ext, LEVELS._hmm)
: f.bind(source, _LoggerLevel._HMM)
const _todo = _TODO
? ext.error.bind(ext, LEVELS._todo)
: f.bind(source, _LoggerLevel._TODO)
const _error = _ERROR
? ext.error.bind(ext, LEVELS._error)
: f.bind(source, _LoggerLevel._ERROR)
const errorDev = ERROR_DEV
? ext.error.bind(ext, LEVELS.errorDev)
: f.bind(source, _LoggerLevel.ERROR_DEV)
const errorPublic = ERROR_PUBLIC
? ext.error.bind(ext, LEVELS.errorPublic)
: f.bind(source, _LoggerLevel.ERROR_PUBLIC)
const _kapow = _KAPOW
? ext.warn.bind(ext, LEVELS._kapow)
: f.bind(source, _LoggerLevel._KAPOW)
const _warn = _WARN
? ext.warn.bind(ext, LEVELS._warn)
: f.bind(source, _LoggerLevel._WARN)
const warnDev = WARN_DEV
? ext.warn.bind(ext, LEVELS.warnDev)
: f.bind(source, _LoggerLevel.WARN_DEV)
const warnPublic = WARN_PUBLIC
? ext.warn.bind(ext, LEVELS.warnPublic)
: f.bind(source, _LoggerLevel.WARN_DEV)
const _debug = _DEBUG
? ext.debug.bind(ext, LEVELS._debug)
: f.bind(source, _LoggerLevel._DEBUG)
const debugDev = DEBUG_DEV
? ext.debug.bind(ext, LEVELS.debugDev)
: f.bind(source, _LoggerLevel.DEBUG_DEV)
const _trace = _TRACE
? ext.trace.bind(ext, LEVELS._trace)
: f.bind(source, _LoggerLevel._TRACE)
const traceDev = TRACE_DEV
? ext.trace.bind(ext, LEVELS.traceDev)
: f.bind(source, _LoggerLevel.TRACE_DEV)
const logger: ILogger = {
_hmm,
_todo,
_error,
errorDev,
errorPublic,
_kapow,
_warn,
warnDev,
warnPublic,
_debug,
debugDev,
_trace,
traceDev,
lazy: {
_hmm: _HMM ? lazy(_hmm) : _hmm,
_todo: _TODO ? lazy(_todo) : _todo,
_error: _ERROR ? lazy(_error) : _error,
errorDev: ERROR_DEV ? lazy(errorDev) : errorDev,
errorPublic: ERROR_PUBLIC ? lazy(errorPublic) : errorPublic,
_kapow: _KAPOW ? lazy(_kapow) : _kapow,
_warn: _WARN ? lazy(_warn) : _warn,
warnDev: WARN_DEV ? lazy(warnDev) : warnDev,
warnPublic: WARN_PUBLIC ? lazy(warnPublic) : warnPublic,
_debug: _DEBUG ? lazy(_debug) : _debug,
debugDev: DEBUG_DEV ? lazy(debugDev) : debugDev,
_trace: _TRACE ? lazy(_trace) : _trace,
traceDev: TRACE_DEV ? lazy(traceDev) : traceDev,
},
//
named,
downgrade: {
internal() {
return {
debug: logger._debug,
error: logger._error,
warn: logger._warn,
trace: logger._trace,
named(name, key) {
return logger.named(name, key).downgrade.internal()
},
}
},
dev() {
return {
debug: logger.debugDev,
error: logger.errorDev,
warn: logger.warnDev,
trace: logger.traceDev,
named(name, key) {
return logger.named(name, key).downgrade.dev()
},
}
},
public() {
return {
error: logger.errorPublic,
warn: logger.warnPublic,
debug(message, obj) {
logger._warn(`(public "debug" filtered out) ${message}`, obj)
},
trace(message, obj) {
logger._warn(`(public "trace" filtered out) ${message}`, obj)
},
named(name, key) {
return logger.named(name, key).downgrade.public()
},
}
},
},
}
return logger
}
function createConsoleLoggerStyled(
this: InternalLoggerRef,
con: ITheatreConsoleLogger,
source: ITheatreLogSource,
): ILogger {
const includes = {...this.includes, ...this.include(source)}
const styleArgs: any[] = []
let prefix = ''
for (let i = 0; i < source.names.length; i++) {
const {name, key} = source.names[i]
prefix += ` %c${name}`
styleArgs.push(this.style.css(name))
if (key != null) {
const keyStr = `%c#${key}`
prefix += keyStr
styleArgs.push(this.style.css(keyStr))
}
}
const f = this.filtered
const named = this.named.bind(this, source)
const prefixArr = [prefix, ...styleArgs]
return _createConsoleLogger(
f,
source,
includes,
con,
prefixArr,
styledKapowPrefix(prefixArr),
named,
)
}
function styledKapowPrefix(args: ReadonlyArray<string>): ReadonlyArray<string> {
const start = args.slice(0)
for (let i = 1; i < start.length; i++)
// add big font to all part styles
start[i] += ';background-color:#e0005a;padding:2px;color:white'
return start
}
function createConsoleLoggerNoStyle(
this: InternalLoggerRef,
con: ITheatreConsoleLogger,
source: ITheatreLogSource,
): ILogger {
const includes = {...this.includes, ...this.include(source)}
let prefix = ''
for (let i = 0; i < source.names.length; i++) {
const {name, key} = source.names[i]
prefix += ` ${name}`
if (key != null) {
prefix += `#${key}`
}
}
const f = this.filtered
const named = this.named.bind(this, source)
const prefixArr = [prefix]
return _createConsoleLogger(
f,
source,
includes,
con,
prefixArr,
prefixArr,
named,
)
}
/** Used by {@link createConsoleLoggerNoStyle} and {@link createConsoleLoggerStyled} */
function _createConsoleLogger(
f: (
this: ITheatreLogSource,
level: _LoggerLevel,
message: string,
args?: object | undefined,
) => void,
source: ITheatreLogSource,
includes: {min: TheatreLoggerLevel; dev: boolean; internal: boolean},
con: ITheatreConsoleLogger,
prefix: ReadonlyArray<any>,
kapowPrefix: ReadonlyArray<any>,
named: (name: string, key?: string | number | undefined) => ILogger,
) {
const _HMM = shouldLog(includes, _LoggerLevel._HMM)
const _TODO = shouldLog(includes, _LoggerLevel._TODO)
const _ERROR = shouldLog(includes, _LoggerLevel._ERROR)
const ERROR_DEV = shouldLog(includes, _LoggerLevel.ERROR_DEV)
const ERROR_PUBLIC = shouldLog(includes, _LoggerLevel.ERROR_PUBLIC)
const _WARN = shouldLog(includes, _LoggerLevel._WARN)
const _KAPOW = shouldLog(includes, _LoggerLevel._KAPOW)
const WARN_DEV = shouldLog(includes, _LoggerLevel.WARN_DEV)
const WARN_PUBLIC = shouldLog(includes, _LoggerLevel.WARN_PUBLIC)
const _DEBUG = shouldLog(includes, _LoggerLevel._DEBUG)
const DEBUG_DEV = shouldLog(includes, _LoggerLevel.DEBUG_DEV)
const _TRACE = shouldLog(includes, _LoggerLevel._TRACE)
const TRACE_DEV = shouldLog(includes, _LoggerLevel.TRACE_DEV)
const _hmm = _HMM
? con.error.bind(con, ...prefix)
: f.bind(source, _LoggerLevel._HMM)
const _todo = _TODO
? con.error.bind(con, ...prefix)
: f.bind(source, _LoggerLevel._TODO)
const _error = _ERROR
? con.error.bind(con, ...prefix)
: f.bind(source, _LoggerLevel._ERROR)
const errorDev = ERROR_DEV
? con.error.bind(con, ...prefix)
: f.bind(source, _LoggerLevel.ERROR_DEV)
const errorPublic = ERROR_PUBLIC
? con.error.bind(con, ...prefix)
: f.bind(source, _LoggerLevel.ERROR_PUBLIC)
const _kapow = _KAPOW
? con.warn.bind(con, ...kapowPrefix)
: f.bind(source, _LoggerLevel._KAPOW)
const _warn = _WARN
? con.warn.bind(con, ...prefix)
: f.bind(source, _LoggerLevel._WARN)
const warnDev = WARN_DEV
? con.warn.bind(con, ...prefix)
: f.bind(source, _LoggerLevel.WARN_DEV)
const warnPublic = WARN_PUBLIC
? con.warn.bind(con, ...prefix)
: f.bind(source, _LoggerLevel.WARN_DEV)
const _debug = _DEBUG
? con.info.bind(con, ...prefix)
: f.bind(source, _LoggerLevel._DEBUG)
const debugDev = DEBUG_DEV
? con.info.bind(con, ...prefix)
: f.bind(source, _LoggerLevel.DEBUG_DEV)
const _trace = _TRACE
? con.debug.bind(con, ...prefix)
: f.bind(source, _LoggerLevel._TRACE)
const traceDev = TRACE_DEV
? con.debug.bind(con, ...prefix)
: f.bind(source, _LoggerLevel.TRACE_DEV)
const logger: ILogger = {
_hmm,
_todo,
_error,
errorDev,
errorPublic,
_kapow,
_warn,
warnDev,
warnPublic,
_debug,
debugDev,
_trace,
traceDev,
lazy: {
_hmm: _HMM ? lazy(_hmm) : _hmm,
_todo: _TODO ? lazy(_todo) : _todo,
_error: _ERROR ? lazy(_error) : _error,
errorDev: ERROR_DEV ? lazy(errorDev) : errorDev,
errorPublic: ERROR_PUBLIC ? lazy(errorPublic) : errorPublic,
_kapow: _KAPOW ? lazy(_kapow) : _kapow,
_warn: _WARN ? lazy(_warn) : _warn,
warnDev: WARN_DEV ? lazy(warnDev) : warnDev,
warnPublic: WARN_PUBLIC ? lazy(warnPublic) : warnPublic,
_debug: _DEBUG ? lazy(_debug) : _debug,
debugDev: DEBUG_DEV ? lazy(debugDev) : debugDev,
_trace: _TRACE ? lazy(_trace) : _trace,
traceDev: TRACE_DEV ? lazy(traceDev) : traceDev,
},
//
named,
downgrade: {
internal() {
return {
debug: logger._debug,
error: logger._error,
warn: logger._warn,
trace: logger._trace,
named(name, key) {
return logger.named(name, key).downgrade.internal()
},
}
},
dev() {
return {
debug: logger.debugDev,
error: logger.errorDev,
warn: logger.warnDev,
trace: logger.traceDev,
named(name, key) {
return logger.named(name, key).downgrade.dev()
},
}
},
public() {
return {
error: logger.errorPublic,
warn: logger.warnPublic,
debug(message, obj) {
logger._warn(`(public "debug" filtered out) ${message}`, obj)
},
trace(message, obj) {
logger._warn(`(public "trace" filtered out) ${message}`, obj)
},
named(name, key) {
return logger.named(name, key).downgrade.public()
},
}
},
},
}
return logger
}

View file

@ -1,8 +1,37 @@
const logger = {
log: console.log,
warn: console.warn,
error: console.error,
trace: console.trace,
export type {
ILogger,
IUtilLogger,
ITheatreConsoleLogger,
ITheatreLogIncludes,
ITheatreLogSource,
ITheatreLoggerConfig,
ITheatreLoggingConfig,
ITheatreInternalLogger,
} from './_logger/logger'
import {createTheatreInternalLogger, TheatreLoggerLevel} from './_logger/logger'
import type {IUtilLogger} from './_logger/logger'
export {TheatreLoggerLevel, createTheatreInternalLogger} from './_logger/logger'
/**
* Common object interface for the context to pass in to utility functions.
*
* Prefer to pass this into utility function rather than an {@link IUtilLogger}.
*/
export interface IUtilContext {
readonly logger: IUtilLogger
}
export default logger
const internal = createTheatreInternalLogger(console, {
_debug: function () {},
_error: function () {},
})
internal.configureLogging({
dev: true,
min: TheatreLoggerLevel.TRACE,
})
export default internal
.getLogger()
.named('Theatre.js (default logger)')
.downgrade.dev()

View file

@ -1,3 +1,4 @@
import type {IUtilContext} from '@theatre/shared/logger'
import {
getLastMultipleOf,
numberOfDecimals,
@ -27,36 +28,44 @@ const example = <Args extends $IntentionalAny[], Return>(
)
}
const CTX: IUtilContext = {
get logger(): never {
throw new Error('unexpected logger access in test example')
},
}
describe(`numberRoundingUtils()`, () => {
describe(`roundestNumberBetween()`, () => {
example(roundestNumberBetween, [0.1, 1.1], 1)
example(roundestNumberBetween, [0.1111111123, 0.2943439448], 0.25)
example(roundestNumberBetween, [0.19, 0.23], 0.2)
example(roundestNumberBetween, [-0.19, 0.23], 0)
example(roundestNumberBetween, [-0.19, -0.02], -0.1, {debug: false})
example(roundestNumberBetween, [-0.19, -0.022], -0.1, {debug: false})
example(roundestNumberBetween, [-0.19, -0.022234324], -0.1, {debug: false})
example(roundestNumberBetween, [-0.19, 0.0222222], 0)
example(roundestNumberBetween, [-0.19, 0.02], 0)
example(roundestNumberBetween, [CTX, 0.1, 1.1], 1)
example(roundestNumberBetween, [CTX, 0.1111111123, 0.2943439448], 0.25)
example(roundestNumberBetween, [CTX, 0.19, 0.23], 0.2)
example(roundestNumberBetween, [CTX, -0.19, 0.23], 0)
example(roundestNumberBetween, [CTX, -0.19, -0.02], -0.1, {debug: false})
example(roundestNumberBetween, [CTX, -0.19, -0.022], -0.1, {debug: false})
example(roundestNumberBetween, [CTX, -0.19, -0.022234324], -0.1, {
debug: false,
})
example(roundestNumberBetween, [CTX, -0.19, 0.0222222], 0)
example(roundestNumberBetween, [CTX, -0.19, 0.02], 0)
example(
roundestNumberBetween,
[22304.2398427391, 22304.2398427393],
[CTX, 22304.2398427391, 22304.2398427393],
22304.2398427392,
)
example(roundestNumberBetween, [22304.2398427391, 22304.4], 22304.25)
example(roundestNumberBetween, [902, 901], 902)
example(roundestNumberBetween, [-10, -5], -10)
example(roundestNumberBetween, [-5, -10], -10)
example(roundestNumberBetween, [-10, -5], -10)
example(roundestNumberBetween, [CTX, 22304.2398427391, 22304.4], 22304.25)
example(roundestNumberBetween, [CTX, 902, 901], 902)
example(roundestNumberBetween, [CTX, -10, -5], -10)
example(roundestNumberBetween, [CTX, -5, -10], -10)
example(roundestNumberBetween, [CTX, -10, -5], -10)
example(
roundestNumberBetween,
[-0.00876370109231405, -2.909374013346118e-50],
[CTX, -0.00876370109231405, -2.909374013346118e-50],
0,
{debug: false},
)
example(
roundestNumberBetween,
[0.059449443526800295, 0.06682093143783596],
[CTX, 0.059449443526800295, 0.06682093143783596],
0.06,
{debug: false},
)
@ -73,7 +82,7 @@ describe(`numberRoundingUtils()`, () => {
const from = toPrecision(getRandomNumber())
const to = toPrecision(getRandomNumber())
const result = roundestNumberBetween(from, to)
const result = roundestNumberBetween(CTX, from, to)
if (from < to) {
if (result < from || result > to) {
throw new Error(`Invalid: ${from} ${to} ${result}`)
@ -87,43 +96,43 @@ describe(`numberRoundingUtils()`, () => {
})
})
describe(`roundestIntegerBetween`, () => {
example(roundestIntegerBetween, [-1, 6], 0, {})
example(roundestIntegerBetween, [0, 6], 0, {})
example(roundestIntegerBetween, [-1, 0], 0, {})
example(roundestIntegerBetween, [-1850, -1740], -1750, {})
example(roundestIntegerBetween, [1, 6], 5, {})
example(roundestIntegerBetween, [1, 5], 5)
example(roundestIntegerBetween, [1, 2], 2)
example(roundestIntegerBetween, [1, 10], 10)
example(roundestIntegerBetween, [1, 12], 10)
example(roundestIntegerBetween, [11, 15], 15)
example(roundestIntegerBetween, [101, 102], 102, {debug: true})
example(roundestIntegerBetween, [11, 14, false], 12)
example(roundestIntegerBetween, [11, 14, true], 12.5)
example(roundestIntegerBetween, [11, 12], 12)
example(roundestIntegerBetween, [11, 12], 12, {})
example(roundestIntegerBetween, [10, 90], 50)
example(roundestIntegerBetween, [10, 100], 100)
example(roundestIntegerBetween, [10, 110], 100)
example(roundestIntegerBetween, [9, 100], 10)
example(roundestIntegerBetween, [9, 1100], 10)
example(roundestIntegerBetween, [9, 699], 10)
example(roundestIntegerBetween, [9, 400], 10)
example(roundestIntegerBetween, [9, 199], 10)
example(roundestIntegerBetween, [9, 1199], 10)
example(roundestIntegerBetween, [1921, 1998], 1950)
example(roundestIntegerBetween, [1921, 2020], 2000)
example(roundestIntegerBetween, [1601, 1998], 1750)
example(roundestIntegerBetween, [1919, 1921], 1920)
example(roundestIntegerBetween, [1919, 1919], 1919)
example(roundestIntegerBetween, [3901, 3902], 3902)
example(roundestIntegerBetween, [901, 902], 902)
example(roundestIntegerBetween, [CTX, -1, 6], 0, {})
example(roundestIntegerBetween, [CTX, 0, 6], 0, {})
example(roundestIntegerBetween, [CTX, -1, 0], 0, {})
example(roundestIntegerBetween, [CTX, -1850, -1740], -1750, {})
example(roundestIntegerBetween, [CTX, 1, 6], 5, {})
example(roundestIntegerBetween, [CTX, 1, 5], 5)
example(roundestIntegerBetween, [CTX, 1, 2], 2)
example(roundestIntegerBetween, [CTX, 1, 10], 10)
example(roundestIntegerBetween, [CTX, 1, 12], 10)
example(roundestIntegerBetween, [CTX, 11, 15], 15)
example(roundestIntegerBetween, [CTX, 101, 102], 102, {debug: true})
example(roundestIntegerBetween, [CTX, 11, 14, false], 12)
example(roundestIntegerBetween, [CTX, 11, 14, true], 12.5)
example(roundestIntegerBetween, [CTX, 11, 12], 12)
example(roundestIntegerBetween, [CTX, 11, 12], 12, {})
example(roundestIntegerBetween, [CTX, 10, 90], 50)
example(roundestIntegerBetween, [CTX, 10, 100], 100)
example(roundestIntegerBetween, [CTX, 10, 110], 100)
example(roundestIntegerBetween, [CTX, 9, 100], 10)
example(roundestIntegerBetween, [CTX, 9, 1100], 10)
example(roundestIntegerBetween, [CTX, 9, 699], 10)
example(roundestIntegerBetween, [CTX, 9, 400], 10)
example(roundestIntegerBetween, [CTX, 9, 199], 10)
example(roundestIntegerBetween, [CTX, 9, 1199], 10)
example(roundestIntegerBetween, [CTX, 1921, 1998], 1950)
example(roundestIntegerBetween, [CTX, 1921, 2020], 2000)
example(roundestIntegerBetween, [CTX, 1601, 1998], 1750)
example(roundestIntegerBetween, [CTX, 1919, 1921], 1920)
example(roundestIntegerBetween, [CTX, 1919, 1919], 1919)
example(roundestIntegerBetween, [CTX, 3901, 3902], 3902)
example(roundestIntegerBetween, [CTX, 901, 902], 902)
})
describe(`roundestFloat()`, () => {
example(roundestFloat, [0.19, 0.2122], 0.2)
example(roundestFloat, [0.19, 0.31], 0.25)
example(roundestFloat, [0.19, 0.41], 0.25)
example(roundestFloat, [0.19, 1.9], 0.5)
example(roundestFloat, [CTX, 0.19, 0.2122], 0.2)
example(roundestFloat, [CTX, 0.19, 0.31], 0.25)
example(roundestFloat, [CTX, 0.19, 0.41], 0.25)
example(roundestFloat, [CTX, 0.19, 1.9], 0.5)
})
describe(`numberOfDecimals()`, () => {
example(numberOfDecimals, [1.1], 1)

View file

@ -1,25 +1,29 @@
import padEnd from 'lodash-es/padEnd'
import logger from '@theatre/shared/logger'
import type {IUtilContext} from '@theatre/shared/logger'
export function roundestNumberBetween(_a: number, _b: number): number {
export function roundestNumberBetween(
ctx: IUtilContext,
_a: number,
_b: number,
): number {
if (_b < _a) {
return roundestNumberBetween(_b, _a)
return roundestNumberBetween(ctx, _b, _a)
}
if (_a < 0 && _b < 0) {
return noMinusZero(roundestNumberBetween(-_b, -_a) * -1)
return noMinusZero(roundestNumberBetween(ctx, -_b, -_a) * -1)
}
if (_a <= 0 && _b >= 0) return 0
const aCeiling = Math.ceil(_a)
if (aCeiling <= _b) {
return roundestIntegerBetween(aCeiling, Math.floor(_b))
return roundestIntegerBetween(ctx, aCeiling, Math.floor(_b))
} else {
const [a, b] = [_a, _b]
const integer = Math.floor(a)
return integer + roundestFloat(a - integer, b - integer)
return integer + roundestFloat(ctx, a - integer, b - integer)
}
}
@ -28,6 +32,7 @@ const halvesAndQuartiles = [5, 2.5, 7.5]
const multipliersWithoutQuartiles = [5, 2, 4, 6, 8, 1, 3, 7, 9]
export function roundestIntegerBetween(
ctx: IUtilContext,
_a: number,
_b: number,
decimalsAllowed: boolean = true,
@ -77,7 +82,9 @@ export function roundestIntegerBetween(
base = highestTotalFound
if (currentExponentiationOfTen === 1) {
logger.error(`Coudn't find a human-readable number between ${a} and ${b}`)
ctx.logger.error(
`Coudn't find a human-readable number between ${a} and ${b}`,
)
return _a
} else {
currentExponentiationOfTen /= 10
@ -126,7 +133,11 @@ export const stringifyNumber = (n: number): string => {
/**
* it is expected that both args are 0 \< arg \< 1
*/
export const roundestFloat = (a: number, b: number): number => {
export const roundestFloat = (
ctx: IUtilContext,
a: number,
b: number,
): number => {
const inString = {
a: stringifyNumber(a),
b: stringifyNumber(b),
@ -160,6 +171,7 @@ export const roundestFloat = (a: number, b: number): number => {
}
const roundestInt = roundestIntegerBetween(
ctx,
parseInt(withPaddedDecimals.a, 10) * Math.pow(10, maxNumberOfLeadingZeros),
parseInt(withPaddedDecimals.b, 10) * Math.pow(10, maxNumberOfLeadingZeros),
true,

View file

@ -1,4 +1,4 @@
import logger from '@theatre/shared/logger'
import type {IUtilContext} from '@theatre/shared/logger'
import type {$IntentionalAny, GenericAction} from '@theatre/shared/utils/types'
import mapValues from 'lodash-es/mapValues'
@ -13,7 +13,7 @@ export type PayloadTypeOfReducer<
> = Parameters<Fn>[1]['payload']
const actionReducersBundle =
<State>() =>
<State>(ctx: IUtilContext) =>
<
Reducers extends Record<
string,
@ -37,7 +37,7 @@ const actionReducersBundle =
const {type} = action
const innerReducer = (reducers as $IntentionalAny)[type]
if (!innerReducer) {
logger.error(`Unkown action type '${type}'`)
ctx.logger.error(`Unkown action type '${type}'`)
return prevState
}
const newState: State = innerReducer(prevState, action)