diff --git a/packages/playground/src/shared/dom/index.tsx b/packages/playground/src/shared/dom/index.tsx index e461419..d1aa76f 100644 --- a/packages/playground/src/shared/dom/index.tsx +++ b/packages/playground/src/shared/dom/index.tsx @@ -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( - , + , document.getElementById('root'), ) diff --git a/theatre/core/src/_coreLogger.ts b/theatre/core/src/_coreLogger.ts new file mode 100644 index 0000000..4f32bd5 --- /dev/null +++ b/theatre/core/src/_coreLogger.ts @@ -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') +} diff --git a/theatre/core/src/coreExports.ts b/theatre/core/src/coreExports.ts index 8e2489b..c3bce09 100644 --- a/theatre/core/src/coreExports.ts +++ b/theatre/core/src/coreExports.ts @@ -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) } /** diff --git a/theatre/core/src/projects/Project.ts b/theatre/core/src/projects/Project.ts index 86d094d..9b8c975 100644 --- a/theatre/core/src/projects/Project.ts +++ b/theatre/core/src/projects/Project.ts @@ -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({ 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 diff --git a/theatre/core/src/projects/TheatreProject.ts b/theatre/core/src/projects/TheatreProject.ts index 5809953..7b8ae4b 100644 --- a/theatre/core/src/projects/TheatreProject.ts +++ b/theatre/core/src/projects/TheatreProject.ts @@ -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 } /** diff --git a/theatre/core/src/sequences/Sequence.ts b/theatre/core/src/sequences/Sequence.ts index 1725fb9..3dcc08d 100644 --- a/theatre/core/src/sequences/Sequence.ts +++ b/theatre/core/src/sequences/Sequence.ts @@ -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, 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.`, diff --git a/theatre/core/src/sequences/TheatreSequence.ts b/theatre/core/src/sequences/TheatreSequence.ts index f0e052c..d94b4ea 100644 --- a/theatre/core/src/sequences/TheatreSequence.ts +++ b/theatre/core/src/sequences/TheatreSequence.ts @@ -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 { - 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, ` + diff --git a/theatre/core/src/sequences/interpolationTripleAtPosition.ts b/theatre/core/src/sequences/interpolationTripleAtPosition.ts index 55f9627..ad3fd89 100644 --- a/theatre/core/src/sequences/interpolationTripleAtPosition.ts +++ b/theatre/core/src/sequences/interpolationTripleAtPosition.ts @@ -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, timeD: IDerivation, ): IDerivation { @@ -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, ): IDerivation { @@ -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, 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 diff --git a/theatre/core/src/sheetObjects/SheetObject.ts b/theatre/core/src/sheetObjects/SheetObject.ts index 319de7a..8ab798a 100644 --- a/theatre/core/src/sheetObjects/SheetObject.ts +++ b/theatre/core/src/sheetObjects/SheetObject.ts @@ -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({}) 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 { diff --git a/theatre/core/src/sheets/Sheet.ts b/theatre/core/src/sheets/Sheet.ts index 54158d5..dc06b39 100644 --- a/theatre/core/src/sheets/Sheet.ts +++ b/theatre/core/src/sheets/Sheet.ts @@ -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 @@ -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, diff --git a/theatre/shared/src/_logger/logger.shouldLog.test.ts b/theatre/shared/src/_logger/logger.shouldLog.test.ts new file mode 100644 index 0000000..8073843 --- /dev/null +++ b/theatre/shared/src/_logger/logger.shouldLog.test.ts @@ -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 + } + }, + ) +} diff --git a/theatre/shared/src/_logger/logger.test-helpers.ts b/theatre/shared/src/_logger/logger.test-helpers.ts new file mode 100644 index 0000000..b14e8cd --- /dev/null +++ b/theatre/shared/src/_logger/logger.test-helpers.ts @@ -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) => 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, + 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( + template: T, + eachEntry:

(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, +) { + 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 { + 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 }) + } +} diff --git a/theatre/shared/src/_logger/logger.test.ts b/theatre/shared/src/_logger/logger.test.ts new file mode 100644 index 0000000..ef98d86 --- /dev/null +++ b/theatre/shared/src/_logger/logger.test.ts @@ -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') + } + }) + }) +}) diff --git a/theatre/shared/src/_logger/logger.ts b/theatre/shared/src/_logger/logger.ts new file mode 100644 index 0000000..ac70b19 --- /dev/null +++ b/theatre/shared/src/_logger/logger.ts @@ -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 +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, + 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 + css(this: InternalLoggerStyleRef, name: string): string + collapsed(this: InternalLoggerStyleRef, name: string): string +} + +type InternalLoggerRef = { + loggingConsoleStyle: boolean + loggerConsoleStyle: boolean + includes: Required + 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([ + // 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): ReadonlyArray { + 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, + kapowPrefix: ReadonlyArray, + 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 +} diff --git a/theatre/shared/src/logger.ts b/theatre/shared/src/logger.ts index 651a833..74eb7c2 100644 --- a/theatre/shared/src/logger.ts +++ b/theatre/shared/src/logger.ts @@ -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() diff --git a/theatre/shared/src/utils/numberRoundingUtils.test.ts b/theatre/shared/src/utils/numberRoundingUtils.test.ts index 316dac5..2ca41a2 100644 --- a/theatre/shared/src/utils/numberRoundingUtils.test.ts +++ b/theatre/shared/src/utils/numberRoundingUtils.test.ts @@ -1,3 +1,4 @@ +import type {IUtilContext} from '@theatre/shared/logger' import { getLastMultipleOf, numberOfDecimals, @@ -27,36 +28,44 @@ const example = ( ) } +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) diff --git a/theatre/shared/src/utils/numberRoundingUtils.ts b/theatre/shared/src/utils/numberRoundingUtils.ts index b72b2ca..2a0784d 100644 --- a/theatre/shared/src/utils/numberRoundingUtils.ts +++ b/theatre/shared/src/utils/numberRoundingUtils.ts @@ -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, diff --git a/theatre/studio/src/utils/redux/actionReducersBundle.ts b/theatre/studio/src/utils/redux/actionReducersBundle.ts index bae0e2c..585206d 100644 --- a/theatre/studio/src/utils/redux/actionReducersBundle.ts +++ b/theatre/studio/src/utils/redux/actionReducersBundle.ts @@ -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[1]['payload'] const actionReducersBundle = - () => + (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)