diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 05ad90e89b..6ee6a9c8f4 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -65,6 +65,7 @@ "@emotion/styled": "^11.10.6", "@fontsource/inter": "^4.5.15", "@reduxjs/toolkit": "^1.9.5", + "@roarr/browser-log-writer": "^1.1.5", "chakra-ui-contextmenu": "^1.0.5", "dateformat": "^5.0.3", "formik": "^2.2.9", @@ -93,17 +94,19 @@ "redux-deep-persist": "^1.0.7", "redux-dynamic-middlewares": "^2.2.0", "redux-persist": "^6.0.0", + "roarr": "^7.15.0", "socket.io-client": "^4.6.0", "use-image": "^1.1.0", "uuid": "^9.0.0" }, "peerDependencies": { + "@chakra-ui/cli": "^2.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "ts-toolbelt": "^9.6.0", - "@chakra-ui/cli": "^2.4.0" + "ts-toolbelt": "^9.6.0" }, "devDependencies": { + "@chakra-ui/cli": "^2.4.0", "@types/dateformat": "^5.0.0", "@types/lodash-es": "^4.14.194", "@types/node": "^18.16.2", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index e5245c7ad5..d3ccbcb395 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -530,7 +530,11 @@ "resetWebUI": "Reset Web UI", "resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.", "resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.", - "resetComplete": "Web UI has been reset. Refresh the page to reload." + "resetComplete": "Web UI has been reset. Refresh the page to reload.", + "consoleLogLevel": "Log Level", + "shouldLogToConsole": "Console Logging", + "developer": "Developer", + "general": "General" }, "toast": { "serverError": "Server Error", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index b33a7cac3f..37d0c7ba72 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -1,5 +1,4 @@ import ImageUploader from 'common/components/ImageUploader'; -import Console from 'features/system/components/Console'; import ProgressBar from 'features/system/components/ProgressBar'; import SiteHeader from 'features/system/components/SiteHeader'; import InvokeTabs from 'features/ui/components/InvokeTabs'; @@ -27,6 +26,7 @@ import { PartialAppConfig } from 'app/types/invokeai'; import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; import { configChanged } from 'features/system/store/configSlice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { useLogger } from 'app/logging/useLogger'; const DEFAULT_CONFIG = {}; @@ -37,6 +37,7 @@ interface Props extends PropsWithChildren { const App = ({ config = DEFAULT_CONFIG, children }: Props) => { useToastWatcher(); useGlobalHotkeys(); + const log = useLogger(); const currentTheme = useAppSelector((state) => state.ui.currentTheme); @@ -50,9 +51,9 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => { const dispatch = useAppDispatch(); useEffect(() => { - console.log('Received config: ', config); + log.info({ namespace: 'App', data: config }, 'Received config'); dispatch(configChanged(config)); - }, [dispatch, config]); + }, [dispatch, config, log]); useEffect(() => { setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark'); @@ -119,9 +120,6 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => { <Portal> <FloatingGalleryButton /> </Portal> - <Portal> - <Console /> - </Portal> </Grid> ); }; diff --git a/invokeai/frontend/web/src/app/logging/useLogger.ts b/invokeai/frontend/web/src/app/logging/useLogger.ts new file mode 100644 index 0000000000..c69b13dc72 --- /dev/null +++ b/invokeai/frontend/web/src/app/logging/useLogger.ts @@ -0,0 +1,94 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import { isEqual } from 'lodash-es'; +import { useEffect } from 'react'; +import { LogLevelName, ROARR, Roarr } from 'roarr'; +import { createLogWriter } from '@roarr/browser-log-writer'; + +// Base logging context includes only the package name +const baseContext = { package: '@invoke-ai/invoke-ai-ui' }; + +// Create browser log writer +ROARR.write = createLogWriter(); + +// Module-scoped logger - can be imported and used anywhere +export let log = Roarr.child(baseContext); + +// Translate human-readable log levels to numbers, used for log filtering +export const LOG_LEVEL_MAP: Record<LogLevelName, number> = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, +}; + +export const VALID_LOG_LEVELS = [ + 'trace', + 'debug', + 'info', + 'warn', + 'error', + 'fatal', +] as const; + +export type InvokeLogLevel = (typeof VALID_LOG_LEVELS)[number]; + +const selector = createSelector( + systemSelector, + (system) => { + const { app_version, consoleLogLevel, shouldLogToConsole } = system; + + return { + version: app_version, + consoleLogLevel, + shouldLogToConsole, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +export const useLogger = () => { + const { version, consoleLogLevel, shouldLogToConsole } = + useAppSelector(selector); + + // The provided Roarr browser log writer uses localStorage to config logging to console + useEffect(() => { + if (shouldLogToConsole) { + // Enable console log output + localStorage.setItem('ROARR_LOG', 'true'); + + // Use a filter to show only logs of the given level + localStorage.setItem( + 'ROARR_FILTER', + `context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}` + ); + } else { + // Disable console log output + localStorage.setItem('ROARR_LOG', 'false'); + } + ROARR.write = createLogWriter(); + }, [consoleLogLevel, shouldLogToConsole]); + + // Update the module-scoped logger context as needed + useEffect(() => { + const newContext: Record<string, any> = { + ...baseContext, + }; + + if (version) { + newContext.version = version; + } + + log = Roarr.child(newContext); + }, [version]); + + // Use the logger within components - no different than just importing it directly + return log; +}; diff --git a/invokeai/frontend/web/src/app/socketio/emitters.ts b/invokeai/frontend/web/src/app/socketio/emitters.ts index 5c9057cac3..610f05b826 100644 --- a/invokeai/frontend/web/src/app/socketio/emitters.ts +++ b/invokeai/frontend/web/src/app/socketio/emitters.ts @@ -12,7 +12,6 @@ import { removeImage, } from 'features/gallery/store/gallerySlice'; import { - addLogEntry, generationRequested, modelChangeRequested, modelConvertRequested, diff --git a/invokeai/frontend/web/src/app/socketio/listeners.ts b/invokeai/frontend/web/src/app/socketio/listeners.ts index 39d4b4ec23..de2f86fd4c 100644 --- a/invokeai/frontend/web/src/app/socketio/listeners.ts +++ b/invokeai/frontend/web/src/app/socketio/listeners.ts @@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid'; import * as InvokeAI from 'app/types/invokeai'; import { - addLogEntry, addToast, errorOccurred, processingCanceled, diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 8a805d6f16..627c4f0063 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -28,7 +28,7 @@ import { lightboxDenylist } from 'features/lightbox/store/lightboxPersistDenylis import { modelsDenylist } from 'features/system/store/modelsPersistDenylist'; import { nodesDenylist } from 'features/nodes/store/nodesPersistDenylist'; import { postprocessingDenylist } from 'features/parameters/store/postprocessingPersistDenylist'; -import { systemDenylist } from 'features/system/store/systemPersistsDenylist'; +import { systemDenylist } from 'features/system/store/systemPersistDenylist'; import { uiDenylist } from 'features/ui/store/uiPersistDenylist'; /** @@ -82,7 +82,6 @@ const rootPersistConfig = getPersistConfig({ 'hotkeys', 'config', ], - debounce: 300, }); const persistedReducer = persistReducer(rootPersistConfig, rootReducer); diff --git a/invokeai/frontend/web/src/common/components/IAISelect.tsx b/invokeai/frontend/web/src/common/components/IAISelect.tsx index f0998b8937..7bc8952d94 100644 --- a/invokeai/frontend/web/src/common/components/IAISelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAISelect.tsx @@ -16,13 +16,23 @@ type IAISelectProps = SelectProps & { validValues: | Array<number | string> | Array<{ key: string; value: string | number }>; + horizontal?: boolean; + spaceEvenly?: boolean; }; /** * Customized Chakra FormControl + Select multi-part component. */ const IAISelect = (props: IAISelectProps) => { - const { label, isDisabled, validValues, tooltip, tooltipProps, ...rest } = - props; + const { + label, + isDisabled, + validValues, + tooltip, + tooltipProps, + horizontal, + spaceEvenly, + ...rest + } = props; return ( <FormControl isDisabled={isDisabled} @@ -32,10 +42,28 @@ const IAISelect = (props: IAISelectProps) => { e.nativeEvent.stopPropagation(); e.nativeEvent.cancelBubble = true; }} + sx={ + horizontal + ? { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 4, + } + : {} + } > - {label && <FormLabel>{label}</FormLabel>} + {label && ( + <FormLabel sx={spaceEvenly ? { flexBasis: 0, flexGrow: 1 } : {}}> + {label} + </FormLabel> + )} <Tooltip label={tooltip} {...tooltipProps}> - <Select {...rest}> + <Select + {...rest} + rootProps={{ sx: spaceEvenly ? { flexBasis: 0, flexGrow: 1 } : {} }} + > {validValues.map((opt) => { return typeof opt === 'string' || typeof opt === 'number' ? ( <IAIOption key={opt} value={opt}> diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 960625135a..d0202a5932 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -16,6 +16,8 @@ import { receivedOpenAPISchema } from 'services/thunks/schema'; import { isFulfilledAnyGraphBuilt } from 'services/thunks/session'; import { InvocationTemplate, InvocationValue } from '../types/types'; import { parseSchema } from '../util/parseSchema'; +import { log } from 'app/logging/useLogger'; +import { size } from 'lodash-es'; export type NodesState = { nodes: Node<InvocationValue>[]; @@ -85,7 +87,12 @@ const nodesSlice = createSlice({ parsedOpenAPISchema: (state, action: PayloadAction<OpenAPIV3.Document>) => { try { const parsedSchema = parseSchema(action.payload); - console.debug('Parsed schema: ', parsedSchema); + + // TODO: Achtung! Side effect in a reducer! + log.info( + { namespace: 'schema', nodes: parsedSchema }, + `Parsed ${size(parsedSchema)} nodes` + ); state.invocationTemplates = parsedSchema; } catch (err) { console.error(err); diff --git a/invokeai/frontend/web/src/features/system/components/Console.tsx b/invokeai/frontend/web/src/features/system/components/Console.tsx deleted file mode 100644 index 4f7946ee78..0000000000 --- a/invokeai/frontend/web/src/features/system/components/Console.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { Flex, Text, Tooltip } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIIconButton from 'common/components/IAIIconButton'; -import { - errorSeen, - setShouldShowLogViewer, - SystemState, -} from 'features/system/store/systemSlice'; -import { isEqual } from 'lodash-es'; -import { Resizable } from 're-resizable'; -import { memo, useLayoutEffect, useRef, useState } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa'; -import { systemSelector } from '../store/systemSelectors'; - -const logSelector = createSelector( - systemSelector, - (system: SystemState) => system.log, - { - memoizeOptions: { - // We don't need a deep equality check for this selector. - resultEqualityCheck: (a, b) => a.length === b.length, - }, - } -); - -const consoleSelector = createSelector( - systemSelector, - (system: SystemState) => { - return { - shouldShowLogViewer: system.shouldShowLogViewer, - hasError: system.hasError, - wasErrorSeen: system.wasErrorSeen, - }; - }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } -); - -/** - * Basic log viewer, floats on bottom of page. - */ -const Console = () => { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const log = useAppSelector(logSelector); - const { shouldShowLogViewer, hasError, wasErrorSeen } = - useAppSelector(consoleSelector); - - // Rudimentary autoscroll - const [shouldAutoscroll, setShouldAutoscroll] = useState<boolean>(true); - const viewerRef = useRef<HTMLDivElement>(null); - - /** - * If autoscroll is on, scroll to the bottom when: - * - log updates - * - viewer is toggled - * - * Also scroll to the bottom whenever autoscroll is turned on. - */ - useLayoutEffect(() => { - if (viewerRef.current !== null && shouldAutoscroll) { - viewerRef.current.scrollTop = viewerRef.current.scrollHeight; - } - }, [shouldAutoscroll, log, shouldShowLogViewer]); - - const handleClickLogViewerToggle = () => { - dispatch(errorSeen()); - dispatch(setShouldShowLogViewer(!shouldShowLogViewer)); - }; - - useHotkeys( - '`', - () => { - dispatch(setShouldShowLogViewer(!shouldShowLogViewer)); - }, - [shouldShowLogViewer] - ); - - useHotkeys('esc', () => { - dispatch(setShouldShowLogViewer(false)); - }); - - const handleOnScroll = () => { - if (!viewerRef.current) return; - if ( - shouldAutoscroll && - viewerRef.current.scrollTop < - viewerRef.current.scrollHeight - viewerRef.current.clientHeight - ) { - setShouldAutoscroll(false); - } - }; - - return ( - <> - {shouldShowLogViewer && ( - <Resizable - defaultSize={{ - width: '100%', - height: 200, - }} - style={{ - display: 'flex', - position: 'fixed', - insetInlineStart: 0, - bottom: 0, - zIndex: 1, - }} - maxHeight="90vh" - > - <Flex - sx={{ - flexDirection: 'column', - width: '100vw', - overflow: 'auto', - direction: 'column', - fontFamily: 'monospace', - pt: 0, - pr: 4, - pb: 4, - pl: 12, - borderTopWidth: 5, - bg: 'base.850', - borderColor: 'base.700', - zIndex: 2, - }} - ref={viewerRef} - onScroll={handleOnScroll} - > - {log.map((entry, i) => { - const { timestamp, message, level } = entry; - const colorScheme = level === 'info' ? 'base' : level; - return ( - <Flex - key={i} - sx={{ - gap: 2, - color: `${colorScheme}.300`, - }} - > - <Text fontWeight="600">{timestamp}:</Text> - <Text wordBreak="break-all">{message}</Text> - </Flex> - ); - })} - </Flex> - </Resizable> - )} - {shouldShowLogViewer && ( - <Tooltip - hasArrow - label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'} - > - <IAIIconButton - size="sm" - aria-label={t('accessibility.toggleAutoscroll')} - icon={<FaAngleDoubleDown />} - onClick={() => setShouldAutoscroll(!shouldAutoscroll)} - isChecked={shouldAutoscroll} - sx={{ - position: 'fixed', - insetInlineStart: 2, - bottom: 12, - zIndex: 1, - }} - /> - </Tooltip> - )} - <Tooltip - hasArrow - label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'} - > - <IAIIconButton - size="sm" - aria-label={t('accessibility.toggleLogViewer')} - icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />} - onClick={handleClickLogViewerToggle} - sx={{ - position: 'fixed', - insetInlineStart: 2, - bottom: 2, - zIndex: 1, - }} - colorScheme={hasError || !wasErrorSeen ? 'error' : 'base'} - /> - </Tooltip> - </> - ); -}; - -export default memo(Console); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 3edd6229a4..a806ef262b 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -23,12 +23,14 @@ import IAISelect from 'common/components/IAISelect'; import IAISwitch from 'common/components/IAISwitch'; import { systemSelector } from 'features/system/store/systemSelectors'; import { + consoleLogLevelChanged, InProgressImageType, setEnableImageDebugging, setSaveIntermediatesInterval, setShouldConfirmOnDelete, setShouldDisplayGuides, setShouldDisplayInProgressType, + shouldLogToConsoleChanged, SystemState, } from 'features/system/store/systemSlice'; import { uiSelector } from 'features/ui/store/uiSelectors'; @@ -39,8 +41,11 @@ import { import { UIState } from 'features/ui/store/uiTypes'; import { isEqual, map } from 'lodash-es'; import { persistor } from 'app/store/persistor'; -import { ChangeEvent, cloneElement, ReactElement } from 'react'; +import { ChangeEvent, cloneElement, ReactElement, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { InvokeLogLevel, VALID_LOG_LEVELS } from 'app/logging/useLogger'; +import { LogLevelName } from 'roarr'; +import { F } from 'ts-toolbelt'; const selector = createSelector( [systemSelector, uiSelector], @@ -52,6 +57,8 @@ const selector = createSelector( model_list, saveIntermediatesInterval, enableImageDebugging, + consoleLogLevel, + shouldLogToConsole, } = system; const { shouldUseCanvasBetaLayout, shouldUseSliders } = ui; @@ -65,6 +72,8 @@ const selector = createSelector( enableImageDebugging, shouldUseCanvasBetaLayout, shouldUseSliders, + consoleLogLevel, + shouldLogToConsole, }; }, { @@ -77,6 +86,7 @@ const modalSectionStyles: ChakraProps['sx'] = { gap: 2, p: 4, bg: 'base.900', + borderRadius: 'base', }; type SettingsModalProps = { @@ -116,6 +126,8 @@ const SettingsModal = ({ children }: SettingsModalProps) => { enableImageDebugging, shouldUseCanvasBetaLayout, shouldUseSliders, + consoleLogLevel, + shouldLogToConsole, } = useAppSelector(selector); /** @@ -135,6 +147,20 @@ const SettingsModal = ({ children }: SettingsModalProps) => { dispatch(setSaveIntermediatesInterval(value)); }; + const handleLogLevelChanged = useCallback( + (e: ChangeEvent<HTMLSelectElement>) => { + dispatch(consoleLogLevelChanged(e.target.value as LogLevelName)); + }, + [dispatch] + ); + + const handleLogToConsoleChanged = useCallback( + (e: ChangeEvent<HTMLInputElement>) => { + dispatch(shouldLogToConsoleChanged(e.target.checked)); + }, + [dispatch] + ); + return ( <> {cloneElement(children, { @@ -145,15 +171,20 @@ const SettingsModal = ({ children }: SettingsModalProps) => { isOpen={isSettingsModalOpen} onClose={onSettingsModalClose} size="xl" + isCentered > <ModalOverlay /> <ModalContent paddingInlineEnd={4}> <ModalHeader>{t('common.settingsLabel')}</ModalHeader> <ModalCloseButton /> <ModalBody> - <Grid gap={4}> + <Flex sx={{ gap: 4, flexDirection: 'column' }}> <Flex sx={modalSectionStyles}> + <Heading size="sm">{t('settings.general')}</Heading> + <IAISelect + horizontal + spaceEvenly label={t('settings.displayInProgress')} validValues={IN_PROGRESS_IMAGE_TYPES} value={shouldDisplayInProgressType} @@ -208,9 +239,21 @@ const SettingsModal = ({ children }: SettingsModalProps) => { </Flex> <Flex sx={modalSectionStyles}> - <Heading size="sm" style={{ fontWeight: 'bold' }}> - Developer - </Heading> + <Heading size="sm">{t('settings.developer')}</Heading> + <IAISwitch + label={t('settings.shouldLogToConsole')} + isChecked={shouldLogToConsole} + onChange={handleLogToConsoleChanged} + /> + <IAISelect + horizontal + spaceEvenly + isDisabled={!shouldLogToConsole} + label={t('settings.consoleLogLevel')} + onChange={handleLogLevelChanged} + value={consoleLogLevel} + validValues={VALID_LOG_LEVELS.concat()} + /> <IAISwitch label={t('settings.enableImageDebugging')} isChecked={enableImageDebugging} @@ -228,7 +271,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => { <Text>{t('settings.resetWebUIDesc1')}</Text> <Text>{t('settings.resetWebUIDesc2')}</Text> </Flex> - </Grid> + </Flex> </ModalBody> <ModalFooter> diff --git a/invokeai/frontend/web/src/features/system/store/systemPersistsDenylist.ts b/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts similarity index 96% rename from invokeai/frontend/web/src/features/system/store/systemPersistsDenylist.ts rename to invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts index bac2a652b5..8a4d381775 100644 --- a/invokeai/frontend/web/src/features/system/store/systemPersistsDenylist.ts +++ b/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts @@ -17,7 +17,6 @@ const itemsToDenylist: (keyof SystemState)[] = [ 'totalSteps', 'openModel', 'isCancelScheduled', - // 'sessionId', 'progressImage', 'wereModelsReceived', 'wasSchemaParsed', diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 3f572a253d..84176bd096 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -14,13 +14,14 @@ import { } from 'services/events/actions'; import i18n from 'i18n'; -import { isImageOutput } from 'services/types/guards'; import { ProgressImage } from 'services/events/types'; import { initialImageSelected } from 'features/parameters/store/generationSlice'; import { makeToast } from '../hooks/useToastWatcher'; import { sessionCanceled, sessionInvoked } from 'services/thunks/session'; import { receivedModels } from 'services/thunks/model'; import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice'; +import { LogLevelName } from 'roarr'; +import { InvokeLogLevel } from 'app/logging/useLogger'; export type LogLevel = 'info' | 'warning' | 'error'; @@ -42,7 +43,6 @@ export interface SystemState extends InvokeAI.SystemStatus, InvokeAI.SystemConfig { shouldDisplayInProgressType: InProgressImageType; - log: Array<LogEntry>; shouldShowLogViewer: boolean; isGFPGANAvailable: boolean; isESRGANAvailable: boolean; @@ -97,12 +97,16 @@ export interface SystemState * Whether or not the OpenAPI schema was received and parsed */ wasSchemaParsed: boolean; + /** + * The console output logging level + */ + consoleLogLevel: InvokeLogLevel; + shouldLogToConsole: boolean; } const initialSystemState: SystemState = { isConnected: false, isProcessing: false, - log: [], shouldShowLogViewer: false, shouldDisplayInProgressType: 'latents', shouldDisplayGuides: true, @@ -144,11 +148,10 @@ const initialSystemState: SystemState = { cancelType: 'immediate', isCancelScheduled: false, subscribedNodeIds: [], - // shouldTransformUrls: false, - // disabledTabs: [], - // disabledFeatures: [], wereModelsReceived: false, wasSchemaParsed: false, + consoleLogLevel: 'error', + shouldLogToConsole: true, }; export const systemSlice = createSlice({ @@ -189,25 +192,6 @@ export const systemSlice = createSlice({ ? i18n.t('common.statusConnected') : i18n.t('common.statusDisconnected'); }, - addLogEntry: ( - state, - action: PayloadAction<{ - timestamp: string; - message: string; - level?: LogLevel; - }> - ) => { - const { timestamp, message, level } = action.payload; - const logLevel = level || 'info'; - - const entry: LogEntry = { - timestamp, - message, - level: logLevel, - }; - - state.log.push(entry); - }, setShouldShowLogViewer: (state, action: PayloadAction<boolean>) => { state.shouldShowLogViewer = action.payload; }, @@ -346,6 +330,12 @@ export const systemSlice = createSlice({ subscribedNodeIdsSet: (state, action: PayloadAction<string[]>) => { state.subscribedNodeIds = action.payload; }, + consoleLogLevelChanged: (state, action: PayloadAction<LogLevelName>) => { + state.consoleLogLevel = action.payload; + }, + shouldLogToConsoleChanged: (state, action: PayloadAction<boolean>) => { + state.shouldLogToConsole = action.payload; + }, }, extraReducers(builder) { /** @@ -353,7 +343,6 @@ export const systemSlice = createSlice({ */ builder.addCase(socketSubscribed, (state, action) => { state.sessionId = action.payload.sessionId; - console.log(`Subscribed to session ${action.payload.sessionId}`); }); /** @@ -371,11 +360,6 @@ export const systemSlice = createSlice({ state.isConnected = true; state.currentStatus = i18n.t('common.statusConnected'); - state.log.push({ - timestamp, - message: `Connected to server`, - level: 'info', - }); }); /** @@ -386,11 +370,6 @@ export const systemSlice = createSlice({ state.isConnected = false; state.currentStatus = i18n.t('common.statusDisconnected'); - state.log.push({ - timestamp, - message: `Disconnected from server`, - level: 'error', - }); }); /** @@ -433,15 +412,6 @@ export const systemSlice = createSlice({ state.totalSteps = 0; state.progressImage = null; state.currentStatus = i18n.t('common.statusProcessingComplete'); - - // TODO: handle logging for other invocation types - if (isImageOutput(data.result)) { - state.log.push({ - timestamp, - message: `Generated: ${data.result.image.image_name}`, - level: 'info', - }); - } }); /** @@ -450,12 +420,6 @@ export const systemSlice = createSlice({ builder.addCase(invocationError, (state, action) => { const { data, timestamp } = action.payload; - state.log.push({ - timestamp, - message: `Server error: ${data.error}`, - level: 'error', - }); - state.wasErrorSeen = true; state.progressImage = null; state.isProcessing = false; @@ -463,12 +427,6 @@ export const systemSlice = createSlice({ state.toastQueue.push( makeToast({ title: i18n.t('toast.serverError'), status: 'error' }) ); - - state.log.push({ - timestamp, - message: `Server error: ${data.error}`, - level: 'error', - }); }); /** @@ -495,12 +453,6 @@ export const systemSlice = createSlice({ state.toastQueue.push( makeToast({ title: i18n.t('toast.canceled'), status: 'warning' }) ); - - state.log.push({ - timestamp, - message: `Processing canceled`, - level: 'warning', - }); }); /** @@ -529,7 +481,6 @@ export const systemSlice = createSlice({ export const { setShouldDisplayInProgressType, setIsProcessing, - addLogEntry, setShouldShowLogViewer, setIsConnected, setSocketId, @@ -562,6 +513,8 @@ export const { scheduledCancelAborted, cancelTypeChanged, subscribedNodeIdsSet, + consoleLogLevelChanged, + shouldLogToConsoleChanged, } = systemSlice.actions; export default systemSlice.reducer; diff --git a/invokeai/frontend/web/src/services/events/actions.ts b/invokeai/frontend/web/src/services/events/actions.ts index 5ea0a1e9e1..84268773a9 100644 --- a/invokeai/frontend/web/src/services/events/actions.ts +++ b/invokeai/frontend/web/src/services/events/actions.ts @@ -1,6 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; import { GeneratorProgressEvent, + GraphExecutionStateCompleteEvent, InvocationCompleteEvent, InvocationErrorEvent, InvocationStartedEvent, @@ -45,6 +46,10 @@ export const invocationError = createAction< BaseSocketPayload & { data: InvocationErrorEvent } >('socket/invocationError'); +export const graphExecutionStateComplete = createAction< + BaseSocketPayload & { data: GraphExecutionStateCompleteEvent } +>('socket/graphExecutionStateComplete'); + export const generatorProgress = createAction< BaseSocketPayload & { data: GeneratorProgressEvent } >('socket/generatorProgress'); diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts index 824e48648c..e57851e19c 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -6,13 +6,9 @@ import { ServerToClientEvents, } from 'services/events/types'; import { - generatorProgress, invocationComplete, - invocationError, - invocationStarted, socketConnected, socketDisconnected, - socketReset, socketSubscribed, socketUnsubscribed, } from './actions'; @@ -25,7 +21,6 @@ import { getTimestamp } from 'common/util/getTimestamp'; import { sessionInvoked, isFulfilledSessionCreatedAction, - sessionCanceled, } from 'services/thunks/session'; import { OpenAPI } from 'services/api'; import { receivedModels } from 'services/thunks/model'; @@ -33,6 +28,9 @@ import { receivedOpenAPISchema } from 'services/thunks/schema'; import { isImageOutput } from 'services/types/guards'; import { imageReceived, thumbnailReceived } from 'services/thunks/image'; import { setEventListeners } from 'services/events/util/setEventListeners'; +import { log } from 'app/logging/useLogger'; + +const moduleLog = log.child({ namespace: 'socketio' }); export const socketMiddleware = () => { let areListenersSet = false; @@ -91,6 +89,8 @@ export const socketMiddleware = () => { // Must happen in middleware to get access to `dispatch` if (!areListenersSet) { socket.on('connect', () => { + moduleLog.debug('Connected'); + dispatch(socketConnected({ timestamp: getTimestamp() })); const { results, uploads, models, nodes, config, system } = @@ -116,7 +116,12 @@ export const socketMiddleware = () => { } if (system.sessionId) { - console.log(`Re-subscribing to session ${system.sessionId}`); + const sessionLog = moduleLog.child({ sessionId: system.sessionId }); + + sessionLog.debug( + `Subscribed to existing session (${system.sessionId})` + ); + socket.emit('subscribe', { session: system.sessionId }); dispatch( socketSubscribed({ @@ -124,11 +129,12 @@ export const socketMiddleware = () => { timestamp: getTimestamp(), }) ); - setEventListeners({ socket, store }); + setEventListeners({ socket, store, sessionLog }); } }); socket.on('disconnect', () => { + moduleLog.debug('Disconnected'); dispatch(socketDisconnected({ timestamp: getTimestamp() })); }); @@ -140,6 +146,8 @@ export const socketMiddleware = () => { // Everything else only happens once we have created a session if (isFulfilledSessionCreatedAction(action)) { + const sessionId = action.payload.id; + const sessionLog = moduleLog.child({ sessionId }); const oldSessionId = getState().system.sessionId; // const subscribedNodeIds = getState().system.subscribedNodeIds; @@ -152,6 +160,10 @@ export const socketMiddleware = () => { // }; if (oldSessionId) { + sessionLog.debug( + { oldSessionId }, + `Unsubscribed from old session (${oldSessionId})` + ); // Unsubscribe when invocations complete socket.emit('unsubscribe', { session: oldSessionId, @@ -176,8 +188,7 @@ export const socketMiddleware = () => { }); } - const sessionId = action.payload.id; - + sessionLog.debug(`Subscribe to new session (${sessionId})`); socket.emit('subscribe', { session: sessionId }); dispatch( socketSubscribed({ @@ -185,7 +196,7 @@ export const socketMiddleware = () => { timestamp: getTimestamp(), }) ); - setEventListeners({ socket, store }); + setEventListeners({ socket, store, sessionLog }); // Finally we actually invoke the session, starting processing dispatch(sessionInvoked({ sessionId })); diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index 950cfb4083..90a285d238 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -5,34 +5,42 @@ import { sessionCanceled } from 'services/thunks/session'; import { Socket } from 'socket.io-client'; import { generatorProgress, + graphExecutionStateComplete, invocationComplete, invocationError, invocationStarted, } from '../actions'; import { ClientToServerEvents, ServerToClientEvents } from '../types'; +import { Logger } from 'roarr'; +import { JsonObject } from 'roarr/dist/types'; type SetEventListenersArg = { socket: Socket<ServerToClientEvents, ClientToServerEvents>; store: MiddlewareAPI<AppDispatch, RootState>; + sessionLog: Logger<JsonObject>; }; export const setEventListeners = (arg: SetEventListenersArg) => { - const { socket, store } = arg; + const { socket, store, sessionLog } = arg; const { dispatch, getState } = store; // Set up listeners for the present subscription socket.on('invocation_started', (data) => { + sessionLog.child({ data }).info(`Invocation started (${data.node.type})`); dispatch(invocationStarted({ data, timestamp: getTimestamp() })); }); socket.on('generator_progress', (data) => { + sessionLog.child({ data }).trace(`Generator progress (${data.node.type})`); dispatch(generatorProgress({ data, timestamp: getTimestamp() })); }); socket.on('invocation_error', (data) => { + sessionLog.child({ data }).error(`Invocation error (${data.node.type})`); dispatch(invocationError({ data, timestamp: getTimestamp() })); }); socket.on('invocation_complete', (data) => { + sessionLog.child({ data }).info(`Invocation complete (${data.node.type})`); const sessionId = data.graph_execution_state_id; const { cancelType, isCancelScheduled } = getState().system; @@ -51,4 +59,13 @@ export const setEventListeners = (arg: SetEventListenersArg) => { }) ); }); + + socket.on('graph_execution_state_complete', (data) => { + sessionLog + .child({ data }) + .info( + `Graph execution state complete (${data.graph_execution_state_id})` + ); + dispatch(graphExecutionStateComplete({ data, timestamp: getTimestamp() })); + }); }; diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts index 4361ce1499..f908cbddcb 100644 --- a/invokeai/frontend/web/src/services/thunks/gallery.ts +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -1,8 +1,11 @@ +import { log } from 'app/logging/useLogger'; import { createAppAsyncThunk } from 'app/store/storeUtils'; import { ImagesService } from 'services/api'; export const IMAGES_PER_PAGE = 20; +const galleryLog = log.child({ namespace: 'gallery' }); + export const receivedResultImagesPage = createAppAsyncThunk( 'results/receivedResultImagesPage', async (_arg, { getState }) => { @@ -12,6 +15,8 @@ export const receivedResultImagesPage = createAppAsyncThunk( perPage: IMAGES_PER_PAGE, }); + galleryLog.info({ response }, `Received ${response.items.length} results`); + return response; } ); @@ -25,6 +30,8 @@ export const receivedUploadImagesPage = createAppAsyncThunk( perPage: IMAGES_PER_PAGE, }); + galleryLog.info({ response }, `Received ${response.items.length} uploads`); + return response; } ); diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index d9fdd1c589..c4da9d9f16 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -1,10 +1,13 @@ import { isFulfilled, isRejected } from '@reduxjs/toolkit'; +import { log } from 'app/logging/useLogger'; import { createAppAsyncThunk } from 'app/store/storeUtils'; import { imageSelected } from 'features/gallery/store/gallerySlice'; import { clamp } from 'lodash-es'; import { ImagesService } from 'services/api'; import { getHeaders } from 'services/util/getHeaders'; +const imagesLog = log.child({ namespace: 'image' }); + type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0]; /** @@ -14,6 +17,9 @@ export const imageReceived = createAppAsyncThunk( 'api/imageReceived', async (arg: ImageReceivedArg, _thunkApi) => { const response = await ImagesService.getImage(arg); + + imagesLog.info({ arg, response }, 'Received image'); + return response; } ); @@ -29,6 +35,9 @@ export const thumbnailReceived = createAppAsyncThunk( 'api/thumbnailReceived', async (arg: ThumbnailReceivedArg, _thunkApi) => { const response = await ImagesService.getThumbnail(arg); + + imagesLog.info({ arg, response }, 'Received thumbnail'); + return response; } ); @@ -43,6 +52,12 @@ export const imageUploaded = createAppAsyncThunk( async (arg: ImageUploadedArg, _thunkApi) => { const response = await ImagesService.uploadImage(arg); const { location } = getHeaders(response); + + imagesLog.info( + { arg: '<Blob>', response, location }, + `Image uploaded (${response.image_name})` + ); + return { response, location }; } ); @@ -96,6 +111,11 @@ export const imageDeleted = createAppAsyncThunk( const response = await ImagesService.deleteImage(arg); + imagesLog.info( + { arg, response }, + `Image deleted (${arg.imageType} - ${arg.imageName})` + ); + return response; } ); diff --git a/invokeai/frontend/web/src/services/thunks/model.ts b/invokeai/frontend/web/src/services/thunks/model.ts index a4aac7563d..84f7a24e81 100644 --- a/invokeai/frontend/web/src/services/thunks/model.ts +++ b/invokeai/frontend/web/src/services/thunks/model.ts @@ -1,14 +1,18 @@ +import { log } from 'app/logging/useLogger'; import { createAppAsyncThunk } from 'app/store/storeUtils'; import { Model } from 'features/system/store/modelSlice'; -import { reduce } from 'lodash-es'; +import { reduce, size } from 'lodash-es'; import { ModelsService } from 'services/api'; +const models = log.child({ namespace: 'model' }); + export const IMAGES_PER_PAGE = 20; export const receivedModels = createAppAsyncThunk( 'models/receivedModels', - async (_arg) => { + async (_) => { const response = await ModelsService.listModels(); + const deserializedModels = reduce( response.models, (modelsAccumulator, model, modelName) => { @@ -19,6 +23,8 @@ export const receivedModels = createAppAsyncThunk( {} as Record<string, Model> ); + models.info({ response }, `Received ${size(response.models)} models`); + return deserializedModels; } ); diff --git a/invokeai/frontend/web/src/services/thunks/schema.ts b/invokeai/frontend/web/src/services/thunks/schema.ts index 7da8514427..bc93fa0fae 100644 --- a/invokeai/frontend/web/src/services/thunks/schema.ts +++ b/invokeai/frontend/web/src/services/thunks/schema.ts @@ -1,16 +1,19 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; +import { log } from 'app/logging/useLogger'; import { parsedOpenAPISchema } from 'features/nodes/store/nodesSlice'; import { OpenAPIV3 } from 'openapi-types'; +const schemaLog = log.child({ namespace: 'schema' }); + export const receivedOpenAPISchema = createAsyncThunk( 'nodes/receivedOpenAPISchema', async (_, { dispatch }): Promise<OpenAPIV3.Document> => { const response = await fetch(`openapi.json`); - const openAPISchema = (await response.json()) as OpenAPIV3.Document; + const openAPISchema = await response.json(); - console.debug('OpenAPI schema: ', openAPISchema); + schemaLog.info({ openAPISchema }, 'Received OpenAPI schema'); - dispatch(parsedOpenAPISchema(openAPISchema)); + dispatch(parsedOpenAPISchema(openAPISchema as OpenAPIV3.Document)); return openAPISchema; } diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts index ba796f7467..0641437d52 100644 --- a/invokeai/frontend/web/src/services/thunks/session.ts +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -3,6 +3,9 @@ import { SessionsService } from 'services/api'; import { buildLinearGraph as buildGenerateGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph'; import { isAnyOf, isFulfilled } from '@reduxjs/toolkit'; import { buildNodesGraph } from 'features/nodes/util/nodesGraphBuilder/buildNodesGraph'; +import { log } from 'app/logging/useLogger'; + +const sessionLog = log.child({ namespace: 'session' }); export const generateGraphBuilt = createAppAsyncThunk( 'api/generateGraphBuilt', @@ -43,12 +46,12 @@ type SessionCreatedArg = { export const sessionCreated = createAppAsyncThunk( 'api/sessionCreated', async (arg: SessionCreatedArg, { dispatch, getState }) => { - console.log('Session created, graph: ', arg.graph); - const response = await SessionsService.createSession({ requestBody: arg.graph, }); + sessionLog.info({ arg, response }, `Session created (${response.id})`); + return response; } ); @@ -74,6 +77,8 @@ export const nodeAdded = createAppAsyncThunk( sessionId: arg.sessionId, }); + sessionLog.info({ arg, response }, `Node added (${response})`); + return response; } ); @@ -91,6 +96,8 @@ export const sessionInvoked = createAppAsyncThunk( all: true, }); + sessionLog.info({ arg, response }, `Session invoked (${sessionId})`); + return response; } ); @@ -111,6 +118,8 @@ export const sessionCanceled = createAppAsyncThunk( sessionId, }); + sessionLog.info({ arg, response }, `Session canceled (${sessionId})`); + return response; } ); @@ -127,6 +136,11 @@ export const listedSessions = createAppAsyncThunk( async (arg: SessionsListedArg, _thunkApi) => { const response = await SessionsService.listSessions(arg); + sessionLog.info( + { arg, response }, + `Sessions listed (${response.items.length})` + ); + return response; } ); diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index b0811ad877..f117d4f2de 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -1421,6 +1421,15 @@ redux-thunk "^2.4.2" reselect "^4.1.8" +"@roarr/browser-log-writer@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@roarr/browser-log-writer/-/browser-log-writer-1.1.5.tgz#755ff62ddaa297bb3488067408a7085db382352b" + integrity sha512-yLn//DRjh1/rUgZpZkwmT/5RqHYfkdOwGXWXnKBR3l/HE04DIhSVeYin3sc8aWHBa7s7WglQpYX/uw/WI6POpw== + dependencies: + boolean "^3.1.4" + globalthis "^1.0.2" + liqe "^3.6.0" + "@rollup/pluginutils@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" @@ -2077,7 +2086,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.10.0, ajv@^6.12.4, ajv@~6.12.6: +ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@~6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2307,6 +2316,11 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +boolean@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + boxen@^5.0.0: version "5.1.2" resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" @@ -2609,7 +2623,7 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^2.16.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1: +commander@^2.16.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -2828,6 +2842,11 @@ deepmerge@^2.1.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + defaults@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" @@ -3031,6 +3050,11 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -3442,11 +3466,28 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-json-stringify@^2.7.10: + version "2.7.13" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz#277aa86c2acba4d9851bd6108ed657aa327ed8c0" + integrity sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA== + dependencies: + ajv "^6.11.0" + deepmerge "^4.2.2" + rfdc "^1.2.0" + string-similarity "^4.0.1" + fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-printf@^1.6.9: + version "1.6.9" + resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" + integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== + dependencies: + boolean "^3.1.4" + fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" @@ -3768,7 +3809,7 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.3: +globalthis@^1.0.2, globalthis@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== @@ -4465,6 +4506,14 @@ lint-staged@^13.2.2: string-argv "^0.3.1" yaml "^2.2.2" +liqe@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/liqe/-/liqe-3.6.0.tgz#2d05376e93ff9f4bfdb3e76481f6456d165b499f" + integrity sha512-CYVQr0bk5CCTkX3wW2MdyEWdr9FHLpiE/1cQXQ36Sdjn5gv7JIpm9jnkovFwiVzumw7f6JDFXpljwUY+fAcFYQ== + dependencies: + nearley "^2.20.1" + ts-error "^1.0.6" + listr2@^5.0.7: version "5.0.8" resolved "https://registry.yarnpkg.com/listr2/-/listr2-5.0.8.tgz#a9379ffeb4bd83a68931a65fb223a11510d6ba23" @@ -4714,6 +4763,11 @@ module-lookup-amd@^7.0.1: requirejs "^2.3.5" requirejs-config-file "^4.0.0" +moo@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" + integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -4734,6 +4788,16 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +nearley@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== + dependencies: + commander "^2.19.0" + moo "^0.5.0" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + neo-async@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -5215,6 +5279,19 @@ quote-unquote@^1.0.0: resolved "https://registry.yarnpkg.com/quote-unquote/-/quote-unquote-1.0.0.tgz#67a9a77148effeaf81a4d428404a710baaac8a0b" integrity sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg== +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + rc@1.2.8, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -5554,12 +5631,17 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.3.0: +rfdc@^1.2.0, rfdc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== @@ -5578,6 +5660,18 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +roarr@^7.15.0: + version "7.15.0" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.0.tgz#09b792f0cd31b4a7f91030bb1c47550ceec98ee4" + integrity sha512-CV9WefQfUXTX6wr8CrEMhfNef3sjIt9wNhE/5PNu4tNWsaoDNDXqq+OGn/RW9A1UPb0qc7FQlswXRaJJJsqn8A== + dependencies: + boolean "^3.1.4" + fast-json-stringify "^2.7.10" + fast-printf "^1.6.9" + globalthis "^1.0.2" + safe-stable-stringify "^2.4.1" + semver-compare "^1.0.0" + rollup-plugin-visualizer@^5.9.0: version "5.9.0" resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.0.tgz#013ac54fb6a9d7c9019e7eb77eced673399e5a0b" @@ -5630,6 +5724,11 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-stable-stringify@^2.4.1: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + sass-lookup@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/sass-lookup/-/sass-lookup-3.0.0.tgz#3b395fa40569738ce857bc258e04df2617c48cac" @@ -5644,6 +5743,11 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== + semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" @@ -5810,6 +5914,11 @@ string-argv@^0.3.1, string-argv@~0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== +string-similarity@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" + integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== + string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -6032,6 +6141,11 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +ts-error@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/ts-error/-/ts-error-1.0.6.tgz#277496f2a28de6c184cfce8dfd5cdd03a4e6b0fc" + integrity sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA== + ts-graphviz@^1.5.0: version "1.6.1" resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.6.1.tgz#f44525c048cb8c8c188b7324d2a91015fd31ceaf"