(
+ 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)