diff --git a/package.json b/package.json index df370565..8650594c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@decky/frontend", + "name": "@decky/ui", "version": "4.0.0", "description": "A library for interacting with the Steam frontend in Decky plugins and elsewhere.", "main": "dist/index.js", diff --git a/src/deck-components/Button.tsx b/src/components/Button.tsx similarity index 100% rename from src/deck-components/Button.tsx rename to src/components/Button.tsx diff --git a/src/deck-components/ButtonItem.tsx b/src/components/ButtonItem.tsx similarity index 88% rename from src/deck-components/ButtonItem.tsx rename to src/components/ButtonItem.tsx index 4fcd2401..bc6980aa 100644 --- a/src/deck-components/ButtonItem.tsx +++ b/src/components/ButtonItem.tsx @@ -8,9 +8,8 @@ export interface ButtonItemProps extends ItemProps { disabled?: boolean; } export const ButtonItem = - (CommonUIModule.ButtonField || Object.values(CommonUIModule).find( (mod: any) => mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') || mod?.render?.toString()?.includes('childrenContainerWidth:"min"'), - )) as FC; + ) as FC; diff --git a/src/deck-components/Carousel.ts b/src/components/Carousel.ts similarity index 67% rename from src/deck-components/Carousel.ts rename to src/components/Carousel.ts index ecfb760e..f8311e87 100644 --- a/src/deck-components/Carousel.ts +++ b/src/components/Carousel.ts @@ -1,6 +1,6 @@ import { HTMLAttributes, ReactNode, RefAttributes, VFC } from 'react'; -import { findModuleChild } from '../webpack'; +import { Export, findModuleExport } from '../webpack'; export interface CarouselProps extends HTMLAttributes { autoFocus?: boolean; @@ -20,9 +20,4 @@ export interface CarouselProps extends HTMLAttributes { scrollToAlignment?: 'center'; } -export const Carousel = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.render?.toString().includes('setFocusedColumn:')) return m[prop]; - } -}) as VFC>; +export const Carousel = findModuleExport((e: Export) => e.render?.toString().includes('setFocusedColumn:')) as VFC>; diff --git a/src/components/ControlsList.tsx b/src/components/ControlsList.tsx new file mode 100644 index 00000000..9c3fa8f6 --- /dev/null +++ b/src/components/ControlsList.tsx @@ -0,0 +1,9 @@ +import { Export, findModuleExport } from '../webpack'; +import { FC } from 'react'; + +export interface ControlsListProps { + alignItems?: 'left' | 'right' | 'center'; + spacing?: 'standard' | 'extra'; +} + +export const ControlsList: FC = findModuleExport((e: Export) => e?.toString && e.toString().includes('().ControlsListChild') && e.toString().includes('().ControlsListOuterPanel')); \ No newline at end of file diff --git a/src/deck-components/Dialog.tsx b/src/components/Dialog.tsx similarity index 100% rename from src/deck-components/Dialog.tsx rename to src/components/Dialog.tsx diff --git a/src/deck-components/DialogCheckbox.tsx b/src/components/DialogCheckbox.tsx similarity index 100% rename from src/deck-components/DialogCheckbox.tsx rename to src/components/DialogCheckbox.tsx diff --git a/src/deck-components/Dropdown.tsx b/src/components/Dropdown.tsx similarity index 100% rename from src/deck-components/Dropdown.tsx rename to src/components/Dropdown.tsx diff --git a/src/deck-components/Field.tsx b/src/components/Field.tsx similarity index 77% rename from src/deck-components/Field.tsx rename to src/components/Field.tsx index efe4703b..bc1877dc 100644 --- a/src/deck-components/Field.tsx +++ b/src/components/Field.tsx @@ -1,6 +1,6 @@ import { FC, ReactNode, RefAttributes } from 'react'; -import { findModuleChild } from '../webpack'; +import { Export, findModuleExport } from '../webpack'; import { FooterLegendProps } from './FooterLegend'; export interface FieldProps extends FooterLegendProps { @@ -23,9 +23,4 @@ export interface FieldProps extends FooterLegendProps { onClick?: (e: CustomEvent | MouseEvent) => void; } -export const Field = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.render?.toString().includes('"shift-children-below"')) return m[prop]; - } -}) as FC>; +export const Field = findModuleExport((e: Export) => e?.render?.toString().includes('"shift-children-below"')) as FC>; diff --git a/src/components/FocusRing.ts b/src/components/FocusRing.ts new file mode 100644 index 00000000..5313a0f7 --- /dev/null +++ b/src/components/FocusRing.ts @@ -0,0 +1,13 @@ +import { ElementType, FC, ReactNode } from 'react'; + +import { Export, findModuleExport } from '../webpack'; + +export interface FocusRingProps { + className?: string; + rootClassName?: string; + render?: ElementType; + children?: ReactNode; + NavigationManager?: any; +} + +export const FocusRing = findModuleExport((e: Export) => e?.toString()?.includes('.GetShowDebugFocusRing())')) as FC; diff --git a/src/deck-components/Focusable.tsx b/src/components/Focusable.tsx similarity index 54% rename from src/deck-components/Focusable.tsx rename to src/components/Focusable.tsx index 3ec8445c..704f9119 100644 --- a/src/deck-components/Focusable.tsx +++ b/src/components/Focusable.tsx @@ -1,6 +1,6 @@ import { HTMLAttributes, ReactNode, RefAttributes, VFC } from 'react'; -import { findModuleChild } from '../webpack'; +import { Export, findModuleExport } from '../webpack'; import { FooterLegendProps } from './FooterLegend'; export interface FocusableProps extends HTMLAttributes, FooterLegendProps { @@ -13,10 +13,4 @@ export interface FocusableProps extends HTMLAttributes, FooterLe onCancel?: (e: CustomEvent) => void; } -export const Focusable = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",')) - return m[prop]; - } -}) as VFC>; +export const Focusable = findModuleExport((e: Export) => e?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",')) as VFC>; diff --git a/src/deck-components/FooterLegend.ts b/src/components/FooterLegend.ts similarity index 100% rename from src/deck-components/FooterLegend.ts rename to src/components/FooterLegend.ts diff --git a/src/deck-components/Item.tsx b/src/components/Item.tsx similarity index 100% rename from src/deck-components/Item.tsx rename to src/components/Item.tsx diff --git a/src/components/Marquee.tsx b/src/components/Marquee.tsx new file mode 100644 index 00000000..bf6aee95 --- /dev/null +++ b/src/components/Marquee.tsx @@ -0,0 +1,18 @@ +import { CSSProperties, FC } from 'react'; + +import { Export, findModuleExport } from '../webpack'; + +export interface MarqueeProps { + play?: boolean; + direction?: 'left' | 'right'; + speed?: number; + delay?: number; + fadeLength?: number; + center?: boolean; + resetOnPause?: boolean; + style?: CSSProperties; + className?: string; + children: React.ReactNode; +} + +export const Marquee: FC = findModuleExport((e: Export) => e?.toString && e.toString().includes('.Marquee') && e.toString().includes('--fade-length')); diff --git a/src/components/Menu.tsx b/src/components/Menu.tsx new file mode 100755 index 00000000..c3d08310 --- /dev/null +++ b/src/components/Menu.tsx @@ -0,0 +1,57 @@ +import { FC, ReactNode } from 'react'; + +import { fakeRenderComponent } from '../utils'; +import { Export, findModuleExport } from '../webpack'; +import { FooterLegendProps } from './FooterLegend'; + +export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleExport( + (e: Export) => typeof e === 'function' && e.toString().includes('stopPropagation))'), +); + +export interface MenuProps extends FooterLegendProps { + label: string; + onCancel?(): void; + cancelText?: string; + children?: ReactNode; +} + +export const Menu: FC = findModuleExport( + (e: Export) => e?.prototype?.HideIfSubmenu && e?.prototype?.HideMenu, +); + +export interface MenuGroupProps { + label: string; + disabled?: boolean; + children?: ReactNode; +} + +export const MenuGroup: FC = findModuleExport( + (e: Export) => + (e?.toString()?.includes?.('bInGamepadUI:') && + fakeRenderComponent(() => e({ overview: { appid: 7 } }))?.type?.prototype?.RenderSubMenu) || + (e?.prototype?.RenderSubMenu && e?.prototype?.ShowSubMenu), +); +export interface MenuItemProps extends FooterLegendProps { + bInteractableItem?: boolean; + onClick?(evt: Event): void; + onSelected?(evt: Event): void; + onMouseEnter?(evt: MouseEvent): void; + onMoveRight?(): void; + selected?: boolean; + disabled?: boolean; + bPlayAudio?: boolean; + tone?: 'positive' | 'emphasis' | 'destructive'; + children?: ReactNode; +} + +export const MenuItem: FC = findModuleExport( + (e: Export) => + e?.render?.toString()?.includes('bPlayAudio:') || (e?.prototype?.OnOKButton && e?.prototype?.OnMouseEnter), +); + +/* +all().map(m => { +if (typeof m !== "object") return undefined; +for (let prop in m) { if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) return m[prop]} +}).find(x => x) +*/ diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100755 index 00000000..b242c7df --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,119 @@ +import { FC, ReactNode } from 'react'; + +import { findSP } from '../utils'; +import { Export, findModule, findModuleByExport, findModuleExport } from '../webpack'; + +// All of the popout options + strTitle are related. Proper usage is not yet known... +export interface ShowModalProps { + browserContext?: unknown; + bForcePopOut?: boolean; + bHideActionIcons?: boolean; + bHideMainWindowForPopouts?: boolean; + bNeverPopOut?: boolean; + fnOnClose?: () => void; // Seems to be the same as "closeModal" callback, but only when the modal is a popout. Will no longer work after "Update" invocation! + popupHeight?: number; + popupWidth?: number; + promiseRenderComplete?: Promise; // Invoked once the render is complete. Currently, it seems to be used as image loading success/error callback... + strTitle?: string; +} + +export interface ShowModalResult { + // This method will not invoke any of the variations of "closeModal" callbacks! + Close: () => void; + + // This method will replace the modal element completely and will not update the callback chains, + // meaning that "closeModal" and etc. will not automatically close the modal anymore (also "fnOnClose" + // will not be even called upon close anymore)! You have to manually call the "Close" method when, for example, + // the "closeModal" is invoked in the newly updated modal: + // { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} /> + Update: (modal: ReactNode) => void; +} + +const showModalRaw: ( + modal: ReactNode, + parent?: EventTarget, + title?: string, + props?: ShowModalProps, + unknown1?: unknown, + hideActions?: { bHideActions?: boolean }, + modalManager?: unknown, +) => ShowModalResult = findModuleExport( + (e: Export) => + typeof e === 'function' && e.toString().includes('props.bDisableBackgroundDismiss') && !e?.prototype?.Cancel, +); + +export const showModal = ( + modal: ReactNode, + parent?: EventTarget, + props: ShowModalProps = { + strTitle: 'Decky Dialog', + bHideMainWindowForPopouts: false, + }, +): ShowModalResult => { + return showModalRaw(modal, parent || findSP(), props.strTitle, props, undefined, { + bHideActions: props.bHideActionIcons, + }); +}; + +export interface ModalRootProps { + children?: ReactNode; + onCancel?(): void; + closeModal?(): void; + onOK?(): void; + onEscKeypress?(): void; + className?: string; + modalClassName?: string; + bAllowFullSize?: boolean; + bDestructiveWarning?: boolean; + bDisableBackgroundDismiss?: boolean; + bHideCloseIcon?: boolean; + bOKDisabled?: boolean; + bCancelDisabled?: boolean; +} + +export interface ConfirmModalProps extends ModalRootProps { + onMiddleButton?(): void; // setting this prop will enable the middle button + strTitle?: ReactNode; + strDescription?: ReactNode; + strOKButtonText?: ReactNode; + strCancelButtonText?: ReactNode; + strMiddleButtonText?: ReactNode; + bAlertDialog?: boolean; // This will open a modal with only OK button enabled + bMiddleDisabled?: boolean; +} + +export const ConfirmModal = findModuleExport( + (e: Export) => !e?.prototype?.OK && e?.prototype?.Cancel && e?.prototype?.render, +) as FC; + +export const ModalRoot = Object.values( + findModule((m: any) => { + if (typeof m !== 'object') return false; + + for (let prop in m) { + if (m[prop]?.m_mapModalManager && Object.values(m)?.find((x: any) => x?.type)) { + return true; + } + } + + return false; + }) || {}, +)?.find((x: any) => x?.type?.toString()?.includes('((function(){')) as FC; + +interface SimpleModalProps { + active?: boolean; + children: ReactNode; +} + +const ModalModule = findModuleByExport((e: Export) => e?.toString().includes('.ModalPosition,fallback:'), 5); + +const ModalModuleProps = ModalModule ? Object.values(ModalModule) : []; + +export const SimpleModal = ModalModuleProps.find((prop) => { + const string = prop?.toString(); + return string?.includes('.ShowPortalModal()') && string?.includes('.OnElementReadyCallbacks.Register('); +}) as FC; + +export const ModalPosition = ModalModuleProps.find((prop) => + prop?.toString().includes('.ModalPosition,fallback:'), +) as FC; diff --git a/src/components/Panel.tsx b/src/components/Panel.tsx new file mode 100644 index 00000000..87647514 --- /dev/null +++ b/src/components/Panel.tsx @@ -0,0 +1,24 @@ +import { FC, ReactNode } from 'react'; + +import { Export, findModuleDetailsByExport } from '../webpack'; + +// TODO where did this go? +// export const Panel: FC<{ children?: ReactNode; }> = findModuleExport((e: Export) => { +// if (typeof mod !== 'object' || !mod.__esModule) return undefined; +// return mod.Panel; +// }); + +export interface PanelSectionProps { + title?: string; + spinner?: boolean; + children?: ReactNode; +} + +const [mod, panelSection] = findModuleDetailsByExport((e: Export) => e.toString()?.includes('.PanelSection')); + +export const PanelSection = panelSection as FC; + +export interface PanelSectionRowProps { + children?: ReactNode; +} +export const PanelSectionRow = Object.values(mod).filter((exp: any) => !exp?.toString()?.includes('.PanelSection'))[0] as FC; diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx new file mode 100644 index 00000000..a21b08f8 --- /dev/null +++ b/src/components/ProgressBar.tsx @@ -0,0 +1,29 @@ +import { ReactNode, VFC } from 'react'; + +import { Export, findModuleExport } from '../webpack'; +import { ItemProps } from './Item'; + +export interface ProgressBarItemProps extends ItemProps { + indeterminate?: boolean; + nTransitionSec?: number; + nProgress?: number; + focusable?: boolean; +} + +export interface ProgressBarProps { + indeterminate?: boolean; + nTransitionSec?: number; + nProgress?: number; + focusable?: boolean; +} + +export interface ProgressBarWithInfoProps extends ProgressBarItemProps { + sTimeRemaining?: ReactNode; + sOperationText?: ReactNode; +} + +export const ProgressBar = findModuleExport((e: Export) => e?.toString()?.includes('.ProgressBar,"standard"==')) as VFC; + +export const ProgressBarWithInfo = findModuleExport((e: Export) => e?.toString()?.includes('.ProgressBarFieldStatus},')) as VFC; + +export const ProgressBarItem = findModuleExport((e: Export) => e?.toString()?.includes('"indeterminate","nTransitionSec"')) as VFC; diff --git a/src/components/Scroll.tsx b/src/components/Scroll.tsx new file mode 100644 index 00000000..84fe422a --- /dev/null +++ b/src/components/Scroll.tsx @@ -0,0 +1,11 @@ +import { FC, ReactNode } from 'react'; + +import { Export, findModuleByExport, findModuleExport } from '../webpack'; + +const ScrollingModule = findModuleByExport((e: Export) => e?.render?.toString?.().includes("{case\"x\":")); + +const ScrollingModuleProps = ScrollingModule ? Object.values(ScrollingModule) : []; + +export const ScrollPanel = ScrollingModuleProps.find((prop: any) => prop?.render?.toString?.().includes("{case\"x\":")) as FC<{ children?: ReactNode }>; + +export const ScrollPanelGroup: FC<{ children?: ReactNode }> = findModuleExport((e: Export) => e?.render?.toString().includes(".FocusVisibleChild()),[])")); \ No newline at end of file diff --git a/src/deck-components/SidebarNavigation.tsx b/src/components/SidebarNavigation.tsx similarity index 62% rename from src/deck-components/SidebarNavigation.tsx rename to src/components/SidebarNavigation.tsx index 167178f4..95b7784c 100644 --- a/src/deck-components/SidebarNavigation.tsx +++ b/src/components/SidebarNavigation.tsx @@ -1,6 +1,6 @@ import { ReactNode, VFC } from 'react'; -import { Module, findModuleChild } from '../webpack'; +import { Export, findModuleExport } from '../webpack'; export interface SidebarNavigationPage { title: ReactNode; @@ -23,11 +23,4 @@ export interface SidebarNavigationProps { onPageRequested?: (page: string) => void; } -export const SidebarNavigation = findModuleChild((mod: Module) => { - for (let prop in mod) { - if (mod[prop]?.toString()?.includes('"disableRouteReporting"')) { - return mod[prop]; - } - } - return null; -}) as VFC; +export const SidebarNavigation = findModuleExport((e: Export) => e?.toString()?.includes('"disableRouteReporting"')) as VFC; diff --git a/src/deck-components/SliderField.tsx b/src/components/SliderField.tsx similarity index 100% rename from src/deck-components/SliderField.tsx rename to src/components/SliderField.tsx diff --git a/src/deck-components/Spinner.tsx b/src/components/Spinner.tsx similarity index 100% rename from src/deck-components/Spinner.tsx rename to src/components/Spinner.tsx diff --git a/src/components/SteamSpinner.tsx b/src/components/SteamSpinner.tsx new file mode 100755 index 00000000..96f82b06 --- /dev/null +++ b/src/components/SteamSpinner.tsx @@ -0,0 +1,5 @@ +import { FC, SVGAttributes } from 'react'; + +import { Export, findModuleExport } from '../webpack'; + +export const SteamSpinner = findModuleExport((e: Export) => e?.toString?.()?.includes('Steam Spinner') && e?.toString?.()?.includes('src')) as FC>; diff --git a/src/deck-components/Tabs.tsx b/src/components/Tabs.tsx similarity index 94% rename from src/deck-components/Tabs.tsx rename to src/components/Tabs.tsx index 2a911708..e5b2979d 100644 --- a/src/deck-components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -1,7 +1,7 @@ import { FC, ReactNode, createElement, useEffect, useState } from 'react'; import { fakeRenderComponent, findInReactTree, sleep } from '../utils'; -import { findModule } from '../webpack'; +import { Export, findModuleByExport } from '../webpack'; import { FooterLegendProps } from './FooterLegend'; import { SteamSpinner } from './SteamSpinner'; @@ -98,13 +98,7 @@ const getTabs = async () => { let oldTabs: any; try { - const oldTabsModule = findModule((m: any) => { - if (typeof m !== 'object') return false; - for (let prop in m) { - if (m[prop]?.Unbleed) return true; - } - return false; - }); + const oldTabsModule = findModuleByExport((e: Export) => e.Unbleed); if (oldTabsModule) oldTabs = Object.values(oldTabsModule).find((x: any) => x?.type?.toString()?.includes('((function(){')); } catch (e) { diff --git a/src/deck-components/TextField.tsx b/src/components/TextField.tsx similarity index 100% rename from src/deck-components/TextField.tsx rename to src/components/TextField.tsx diff --git a/src/deck-components/Toggle.tsx b/src/components/Toggle.tsx similarity index 100% rename from src/deck-components/Toggle.tsx rename to src/components/Toggle.tsx diff --git a/src/deck-components/ToggleField.tsx b/src/components/ToggleField.tsx similarity index 100% rename from src/deck-components/ToggleField.tsx rename to src/components/ToggleField.tsx diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100755 index 00000000..da3f355b --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,25 @@ +export * from './Button'; +export * from './ButtonItem'; +export * from './Carousel'; +export * from './ControlsList'; +export * from './Dialog'; +export * from './DialogCheckbox'; +export * from './Dropdown'; +export * from './Field'; +export * from './Focusable'; +export * from './FocusRing'; +export * from './FooterLegend'; +export * from './Marquee'; +export * from './Menu'; +export * from './Modal'; +export * from './Panel'; +export * from './ProgressBar'; +export * from './SidebarNavigation'; +export * from './SliderField'; +export * from './Spinner'; +export * from './SteamSpinner'; +export * from './Tabs'; +export * from './TextField'; +export * from './Toggle'; +export * from './ToggleField'; +export * from './Scroll'; \ No newline at end of file diff --git a/src/custom-components/ColorPickerModal.tsx b/src/custom-components/ColorPickerModal.tsx index a783d4a8..6fa514a6 100644 --- a/src/custom-components/ColorPickerModal.tsx +++ b/src/custom-components/ColorPickerModal.tsx @@ -1,6 +1,7 @@ import { CSSProperties, FC, useState } from 'react'; -import { ConfirmModal, SliderField, gamepadSliderClasses } from '../deck-components'; +import { ConfirmModal, SliderField } from '../components'; +import { gamepadSliderClasses } from '../utils/static-classes'; interface ColorPickerModalProps { closeModal: () => void; diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index ec6723d7..25b1fb1b 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,6 +1,6 @@ import { Fragment, JSXElementConstructor, ReactElement, ReactNode, useEffect, useState } from 'react'; -import { Field, FieldProps, Focusable, GamepadButton } from '../deck-components'; +import { Field, FieldProps, Focusable, GamepadButton } from '../components'; /** * A ReorderableList entry of type . diff --git a/src/custom-components/SuspensefulImage.tsx b/src/custom-components/SuspensefulImage.tsx index 551eecd9..ba5e737b 100644 --- a/src/custom-components/SuspensefulImage.tsx +++ b/src/custom-components/SuspensefulImage.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { FC, ImgHTMLAttributes, useState } from 'react'; -import { Spinner } from '../deck-components'; +import { Spinner } from '../components'; interface SuspensefulImageProps extends ImgHTMLAttributes { suspenseWidth?: string | number; diff --git a/src/deck-components/ControlsList.tsx b/src/deck-components/ControlsList.tsx deleted file mode 100644 index 8c974f43..00000000 --- a/src/deck-components/ControlsList.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { findModuleChild } from '../webpack'; -import { FC } from 'react'; - -export interface ControlsListProps { - alignItems?: 'left' | 'right' | 'center'; - spacing?: 'standard' | 'extra'; -} - -export const ControlsList: FC = findModuleChild((m) => { - if (typeof m !== 'object') return; - for (const prop in m) { - if (m[prop]?.toString && m[prop].toString().includes('().ControlsListChild') && m[prop].toString().includes('().ControlsListOuterPanel')) { - return m[prop]; - } - } - return; -}); diff --git a/src/deck-components/FocusRing.ts b/src/deck-components/FocusRing.ts deleted file mode 100644 index 93290ab8..00000000 --- a/src/deck-components/FocusRing.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ElementType, FC, ReactNode } from 'react'; - -import { findModuleChild } from '../webpack'; - -export interface FocusRingProps { - className?: string; - rootClassName?: string; - render?: ElementType; - children?: ReactNode; - NavigationManager?: any; -} - -export const FocusRing = findModuleChild((m: any) => { - if (typeof m !== 'object') return false; - for (let prop in m) { - if (m[prop]?.toString()?.includes('.GetShowDebugFocusRing())')) return m[prop]; - } - return false; -}) as FC; diff --git a/src/deck-components/Marquee.tsx b/src/deck-components/Marquee.tsx deleted file mode 100644 index b285fd76..00000000 --- a/src/deck-components/Marquee.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { CSSProperties, FC } from 'react'; - -import { findModuleChild } from '../webpack'; - -export interface MarqueeProps { - play?: boolean; - direction?: 'left' | 'right'; - speed?: number; - delay?: number; - fadeLength?: number; - center?: boolean; - resetOnPause?: boolean; - style?: CSSProperties; - className?: string; - children: React.ReactNode; -} - -export const Marquee: FC = findModuleChild((m) => { - if (typeof m !== 'object') return; - for (const prop in m) { - if (m[prop]?.toString && m[prop].toString().includes('.Marquee') && m[prop].toString().includes('--fade-length')) { - return m[prop]; - } - } - return; -}); diff --git a/src/deck-components/Menu.tsx b/src/deck-components/Menu.tsx deleted file mode 100755 index b439f54c..00000000 --- a/src/deck-components/Menu.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { FC, ReactNode } from 'react'; - -import { fakeRenderComponent } from '../utils'; -import { findModuleChild } from '../webpack'; -import { FooterLegendProps } from './FooterLegend'; - -export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (typeof m[prop] === 'function' && m[prop].toString().includes('stopPropagation))')) { - return m[prop]; - } - } -}); - -export interface MenuProps extends FooterLegendProps { - label: string; - onCancel?(): void; - cancelText?: string; - children?: ReactNode; -} - -export const Menu: FC = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - - for (let prop in m) { - if (m[prop]?.prototype?.HideIfSubmenu && m[prop]?.prototype?.HideMenu) { - return m[prop]; - } - } -}); - -export interface MenuGroupProps { - label: string; - disabled?: boolean; - children?: ReactNode; -} - -export const MenuGroup: FC = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - - for (let prop in m) { - if ( - (m[prop]?.toString()?.includes?.('bInGamepadUI:') && - fakeRenderComponent(() => m[prop]({overview: {appid: 7}}))?.type?.prototype?.RenderSubMenu) || - (m[prop]?.prototype?.RenderSubMenu && m[prop]?.prototype?.ShowSubMenu) - ) { - return m[prop]; - } - } -}); - -export interface MenuItemProps extends FooterLegendProps { - bInteractableItem?: boolean; - onClick?(evt: Event): void; - onSelected?(evt: Event): void; - onMouseEnter?(evt: MouseEvent): void; - onMoveRight?(): void; - selected?: boolean; - disabled?: boolean; - bPlayAudio?: boolean; - tone?: 'positive' | 'emphasis' | 'destructive'; - children?: ReactNode; -} - -export const MenuItem: FC = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - - for (let prop in m) { - if ( - m[prop]?.render?.toString()?.includes('bPlayAudio:') || - (m[prop]?.prototype?.OnOKButton && m[prop]?.prototype?.OnMouseEnter) - ) { - return m[prop]; - } - } -}); - -/* -all().map(m => { -if (typeof m !== "object") return undefined; -for (let prop in m) { if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) return m[prop]} -}).find(x => x) -*/ diff --git a/src/deck-components/Modal.tsx b/src/deck-components/Modal.tsx deleted file mode 100755 index f0bea761..00000000 --- a/src/deck-components/Modal.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { FC, ReactNode } from 'react'; - -import { findSP } from '../utils'; -import { findModule, findModuleChild } from '../webpack'; - -// All of the popout options + strTitle are related. Proper usage is not yet known... -export interface ShowModalProps { - browserContext?: unknown; // This is another Deck Object that is yet to be found - bForcePopOut?: boolean; - bHideActionIcons?: boolean; - bHideMainWindowForPopouts?: boolean; - bNeverPopOut?: boolean; - fnOnClose?: () => void; // Seems to be the same as "closeModal" callback, but only when the modal is a popout. Will no longer work after "Update" invocation! - popupHeight?: number; - popupWidth?: number; - promiseRenderComplete?: Promise; // Invoked once the render is complete. Currently, it seems to be used as image loading success/error callback... - strTitle?: string; -} - -export interface ShowModalResult { - // This method will not invoke any of the variations of "closeModal" callbacks! - Close: () => void; - - // This method will replace the modal element completely and will not update the callback chains, - // meaning that "closeModal" and etc. will not automatically close the modal anymore (also "fnOnClose" - // will not be even called upon close anymore)! You have to manually call the "Close" method when, for example, - // the "closeModal" is invoked in the newly updated modal: - // { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} /> - Update: (modal: ReactNode) => void; -} - -const showModalRaw: - | (( - modal: ReactNode, - parent?: EventTarget, - title?: string, - props?: ShowModalProps, - unknown1?: unknown, - hideActions?: { bHideActions?: boolean }, - modalManager?: unknown, - ) => ShowModalResult) - | void = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if ( - typeof m[prop] === 'function' && - m[prop].toString().includes('props.bDisableBackgroundDismiss') && - !m[prop]?.prototype?.Cancel - ) { - return m[prop]; - } - } -}); - -const oldShowModalRaw: ((modal: ReactNode, parent?: EventTarget, props?: ShowModalProps) => ShowModalResult) | void = - findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (typeof m[prop] === 'function' && m[prop].toString().includes('bHideMainWindowForPopouts:!0')) { - return m[prop]; - } - } - }); - -export const showModal = ( - modal: ReactNode, - parent?: EventTarget, - props: ShowModalProps = { - strTitle: 'Decky Dialog', - bHideMainWindowForPopouts: false, - }, -): ShowModalResult => { - if (showModalRaw) { - return showModalRaw(modal, parent || findSP(), props.strTitle, props, undefined, { - bHideActions: props.bHideActionIcons, - }); - } else if (oldShowModalRaw) { - return oldShowModalRaw(modal, parent || findSP(), props); - } else { - throw new Error('[DFL:Modals]: Cannot find showModal function'); - } -}; - -export interface ModalRootProps { - children?: ReactNode; - onCancel?(): void; - closeModal?(): void; - onOK?(): void; - onEscKeypress?(): void; - className?: string; - modalClassName?: string; - bAllowFullSize?: boolean; - bDestructiveWarning?: boolean; - bDisableBackgroundDismiss?: boolean; - bHideCloseIcon?: boolean; - bOKDisabled?: boolean; - bCancelDisabled?: boolean; -} - -export interface ConfirmModalProps extends ModalRootProps { - onMiddleButton?(): void; // setting this prop will enable the middle button - strTitle?: ReactNode; - strDescription?: ReactNode; - strOKButtonText?: ReactNode; - strCancelButtonText?: ReactNode; - strMiddleButtonText?: ReactNode; - bAlertDialog?: boolean; // This will open a modal with only OK button enabled - bMiddleDisabled?: boolean; -} - -export const ConfirmModal = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (!m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) { - return m[prop]; - } - } -}) as FC; - -// new as of december 2022 on beta -export const ModalRoot = (Object.values( - findModule((m: any) => { - if (typeof m !== 'object') return false; - - for (let prop in m) { - if (m[prop]?.m_mapModalManager && Object.values(m)?.find((x: any) => x?.type)) { - return true; - } - } - - return false; - }) || {}, -)?.find((x: any) => x?.type?.toString()?.includes('((function(){')) || - // before december 2022 beta - Object.values( - findModule((m: any) => { - if (typeof m !== 'object') return false; - - for (let prop in m) { - if (m[prop]?.toString()?.includes('"ModalManager","DialogWrapper"')) { - return true; - } - } - - return false; - }) || {}, - )?.find((x: any) => x?.type?.toString()?.includes('((function(){')) || - // old - findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) { - return m[prop]; - } - } - })) as FC; - -interface SimpleModalProps { - active?: boolean; - children: ReactNode; -} - -const ModalModule = findModule((mod: any) => { - if (typeof mod !== 'object') return false; - for (let prop in mod) { - if (Object.keys(mod).length > 4 && mod[prop]?.toString().includes('.ModalPosition,fallback:')) return true; - } - return false; -}); - -const ModalModuleProps = ModalModule ? Object.values(ModalModule) : []; - -export const SimpleModal = ModalModuleProps.find(prop => { - const string = prop?.toString() - return string?.includes(".ShowPortalModal()") && string?.includes(".OnElementReadyCallbacks.Register(") -}) as FC; - -export const ModalPosition = ModalModuleProps.find(prop => prop?.toString().includes(".ModalPosition,fallback:")) as FC; diff --git a/src/deck-components/Panel.tsx b/src/deck-components/Panel.tsx deleted file mode 100644 index b5416d0c..00000000 --- a/src/deck-components/Panel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { FC, ReactNode } from 'react'; - -import { findModuleChild } from '../webpack'; - -export const Panel: FC<{ children?: ReactNode; }> = findModuleChild((mod) => { - if (typeof mod !== 'object' || !mod.__esModule) return undefined; - return mod.Panel; -}) - -export interface PanelSectionProps { - title?: string; - spinner?: boolean; - children?: ReactNode; -} - -const [panelSection, mod] = findModuleChild((mod: any) => { - for (let prop in mod) { - if (mod[prop]?.toString()?.includes('.PanelSection')) { - return [mod[prop], mod]; - } - } - return null; -}); - -export const PanelSection = panelSection as FC; - -export interface PanelSectionRowProps { - children?: ReactNode; -} -// New as of Feb 22 2023 Beta || Old -export const PanelSectionRow = - (mod.PanelSectionRow || - Object.values(mod).filter((exp: any) => !exp?.toString()?.includes('.PanelSection'))[0]) as FC; diff --git a/src/deck-components/ProgressBar.tsx b/src/deck-components/ProgressBar.tsx deleted file mode 100644 index 6dcc9bf4..00000000 --- a/src/deck-components/ProgressBar.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { ReactNode, VFC } from 'react'; - -import { findModuleChild } from '../webpack'; -import { ItemProps } from './Item'; - -export interface ProgressBarItemProps extends ItemProps { - indeterminate?: boolean; - nTransitionSec?: number; - nProgress?: number; - focusable?: boolean; -} - -export interface ProgressBarProps { - indeterminate?: boolean; - nTransitionSec?: number; - nProgress?: number; - focusable?: boolean; -} - -export interface ProgressBarWithInfoProps extends ProgressBarItemProps { - sTimeRemaining?: ReactNode; - sOperationText?: ReactNode; -} - -export const ProgressBar = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.toString()?.includes('.ProgressBar,"standard"==')) return m[prop]; - } -}) as VFC; - -export const ProgressBarWithInfo = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.toString()?.includes('.ProgressBarFieldStatus},')) return m[prop]; - } -}) as VFC; - -export const ProgressBarItem = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.toString()?.includes('"indeterminate","nTransitionSec"')) return m[prop]; - } -}) as VFC; diff --git a/src/deck-components/Scroll.tsx b/src/deck-components/Scroll.tsx deleted file mode 100644 index 21f72228..00000000 --- a/src/deck-components/Scroll.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { FC, ReactNode } from 'react'; - -import { findModule, findModuleChild } from '../webpack'; - -const ScrollingModule = findModule((mod) => { - if (typeof mod !== 'object') return false; - for (let prop in mod) { - if (mod[prop]?.render?.toString?.().includes("{case\"x\":")) return true; - } - return false; -}); - -const ScrollingModuleProps = ScrollingModule ? Object.values(ScrollingModule) : []; - -export const ScrollPanel = ScrollingModuleProps.find((prop: any) => prop?.render?.toString?.().includes("{case\"x\":")) as FC<{ children?: ReactNode }>; - -export const ScrollPanelGroup: FC<{ children?: ReactNode }> = findModuleChild((mod) => { - if (typeof mod !== 'object') return undefined; - for (let prop in mod) { - if (mod[prop]?.render?.toString().includes(".FocusVisibleChild()),[])")) return mod[prop]; - } -}); \ No newline at end of file diff --git a/src/deck-components/SteamSpinner.tsx b/src/deck-components/SteamSpinner.tsx deleted file mode 100755 index 368a8d6a..00000000 --- a/src/deck-components/SteamSpinner.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { FC, SVGAttributes } from 'react'; - -import { findModuleChild } from '../webpack'; - -export const SteamSpinner = findModuleChild((m) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if ( - m[prop]?.toString?.()?.includes('Steam Spinner') && m[prop]?.toString?.()?.includes('src') - ) - return m[prop]; - } -}) as FC>; diff --git a/src/deck-components/SteamClient.ts b/src/globals/SteamClient.ts similarity index 99% rename from src/deck-components/SteamClient.ts rename to src/globals/SteamClient.ts index ae6e2376..19db6cab 100644 --- a/src/deck-components/SteamClient.ts +++ b/src/globals/SteamClient.ts @@ -1,3 +1,7 @@ +declare global { + var SteamClient: SteamClient; +} + export interface Apps { RegisterForAppOverviewChanges: any; RegisterForAppDetails: any; diff --git a/src/globals/index.ts b/src/globals/index.ts new file mode 100644 index 00000000..6b483a7c --- /dev/null +++ b/src/globals/index.ts @@ -0,0 +1,2 @@ +export * from "./SteamClient"; +export * from "./stores"; \ No newline at end of file diff --git a/src/deck-components/index.ts b/src/globals/stores.ts old mode 100755 new mode 100644 similarity index 66% rename from src/deck-components/index.ts rename to src/globals/stores.ts index 65807d44..e0d88896 --- a/src/deck-components/index.ts +++ b/src/globals/stores.ts @@ -1,37 +1,5 @@ -export * from './Button'; -export * from './ButtonItem'; -export * from './Carousel'; -export * from './ControlsList'; -export * from './Dialog'; -export * from './DialogCheckbox'; -export * from './Dropdown'; -export * from './Field'; -export * from './Focusable'; -export * from './FocusRing'; -export * from './FooterLegend'; -export * from './Marquee'; -export * from './Menu'; -export * from './Modal'; -export * from './Panel'; -export * from './ProgressBar'; -export * from './Router'; -export * from './SidebarNavigation'; -export * from './SliderField'; -export * from './Spinner'; -export * from './static-classes'; -export * from './SteamSpinner'; -export * from './Tabs'; -export * from './TextField'; -export * from './Toggle'; -export * from './ToggleField'; -export * from './SteamClient'; -export * from './Scroll'; - -import { AppDetails, LogoPosition, SteamAppOverview, SteamClient } from './SteamClient'; - +import { AppDetails, LogoPosition, SteamAppOverview } from './SteamClient'; declare global { - var SteamClient: SteamClient; - interface Window { LocalizationManager: { m_mapTokens: Map; diff --git a/src/index.ts b/src/index.ts index dda21df0..f8f288f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ // export * from './deck-libs'; export * from './custom-components'; export * from './custom-hooks'; -export * from './deck-components'; +export * from './components'; export * from './deck-hooks'; +export * from './modules'; +export * from './globals'; export * from './plugin'; export * from './webpack'; export * from './utils'; diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 00000000..77517e35 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,94 @@ +const bgStyle1 = 'background: #16a085; color: black;'; + +export const log = (name: string, ...args: any[]) => { + console.log( + `%c @decky/ui %c ${name} %c`, + bgStyle1, + 'background: #1abc9c; color: black;', + 'background: transparent;', + ...args, + ); +}; + +export const group = (name: string, ...args: any[]) => { + console.group( + `%c @decky/ui %c ${name} %c`, + bgStyle1, + 'background: #1abc9c; color: black;', + 'background: transparent;', + ...args, + ); +}; + +export const groupEnd = (name: string, ...args: any[]) => { + console.groupEnd(); + if (args?.length > 0) console.log( + `^ %c @decky/ui %c ${name} %c`, + bgStyle1, + 'background: #1abc9c; color: black;', + 'background: transparent;', + ...args, + ); +}; + +export const debug = (name: string, ...args: any[]) => { + console.debug( + `%c @decky/ui %c ${name} %c`, + bgStyle1, + 'background: #1abc9c; color: black;', + 'color: blue;', + ...args, + ); +}; + +export const warn = (name: string, ...args: any[]) => { + console.warn( + `%c @decky/ui %c ${name} %c`, + bgStyle1, + 'background: #ffbb00; color: black;', + 'color: blue;', + ...args, + ); +}; + +export const error = (name: string, ...args: any[]) => { + console.error( + `%c @decky/ui %c ${name} %c`, + bgStyle1, + 'background: #FF0000;', + 'background: transparent;', + ...args, + ); +}; + +class Logger { + constructor(private name: string) { + this.name = name; + } + + log(...args: any[]) { + log(this.name, ...args); + } + + debug(...args: any[]) { + debug(this.name, ...args); + } + + warn(...args: any[]) { + warn(this.name, ...args); + } + + error(...args: any[]) { + error(this.name, ...args); + } + + group(...args: any[]) { + group(this.name, ...args); + } + + groupEnd(...args: any[]) { + groupEnd(this.name, ...args); + } +} + +export default Logger; \ No newline at end of file diff --git a/src/deck-components/Router.tsx b/src/modules/Router.tsx similarity index 89% rename from src/deck-components/Router.tsx rename to src/modules/Router.tsx index c7e6186a..6333a3d0 100644 --- a/src/deck-components/Router.tsx +++ b/src/modules/Router.tsx @@ -1,5 +1,5 @@ import { sleep } from '../utils'; -import { Module, findModuleChild } from '../webpack'; +import { Export, findModuleExport } from '../webpack'; export enum SideMenu { None, @@ -101,12 +101,7 @@ export interface Router { get MainRunningApp(): AppOverview | undefined; } -export const Router = findModuleChild((m: Module) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.Navigate && m[prop]?.NavigationManager) return m[prop]; - } -}) as Router; +export const Router = findModuleExport((e: Export) => e.Navigate && e.NavigationManager) as Router; export interface Navigation { Navigate(path: string): void; @@ -133,14 +128,7 @@ try { if (!Router.NavigateToAppProperties || (Router as unknown as any).deckyShim) { function initInternalNavigators() { try { - InternalNavigators = findModuleChild((m: any) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.GetNavigator && m[prop]?.SetNavigator) { - return m[prop]; - } - } - })?.GetNavigator(); + InternalNavigators = findModuleExport((e: Export) => e.GetNavigator && e.SetNavigator)?.GetNavigator(); } catch (e) { console.error('[DFL:Router]: Failed to init internal navigators, trying again'); } diff --git a/src/modules/index.ts b/src/modules/index.ts new file mode 100644 index 00000000..6478e35f --- /dev/null +++ b/src/modules/index.ts @@ -0,0 +1 @@ +export * from './Router'; \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index a1fd37d0..eb2bff3a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './patcher'; export * from './react'; +export * from './static-classes'; declare global { var FocusNavController: any; diff --git a/src/utils/react.ts b/src/utils/react.ts index 5c5e6451..6597553e 100644 --- a/src/utils/react.ts +++ b/src/utils/react.ts @@ -67,15 +67,15 @@ export function wrapReactClass(node: any, prop: any = 'type') { export function getReactRoot(o: HTMLElement | Element | Node) { return ( - o[Object.keys(o).find((k) => k.startsWith('__reactContainer$')) as string] || - o['_reactRootContainer']?._internalRoot?.current + // @ts-expect-error 7053 + o[Object.keys(o).find((k) => k.startsWith('__reactContainer$')) as string] || o['_reactRootContainer']?._internalRoot?.current ); } export function getReactInstance(o: HTMLElement | Element | Node) { return ( - o[Object.keys(o).find((k) => k.startsWith('__reactFiber')) as string] || - o[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string] + // @ts-expect-error 7053 + o[Object.keys(o).find((k) => k.startsWith('__reactFiber')) as string] || o[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string] ); } diff --git a/src/deck-components/static-classes.ts b/src/utils/static-classes.ts similarity index 100% rename from src/deck-components/static-classes.ts rename to src/utils/static-classes.ts diff --git a/src/webpack.ts b/src/webpack.ts index 4a9d2270..4b26dd37 100644 --- a/src/webpack.ts +++ b/src/webpack.ts @@ -1,65 +1,99 @@ +import Logger from "./logger"; + declare global { interface Window { - webpackJsonp: any; webpackChunksteamui: any; } } -// TODO +const logger = new Logger("Webpack"); + +// In most case an object with getters for each property. Look for the first call to r.d in the module, usually near or at the top. export type Module = any; +export type Export = any; type FilterFn = (module: any) => boolean; +type ExportFilterFn = (moduleExport: any, exportName?: any) => boolean; type FindFn = (module: any) => any; -export let webpackCache: any = {}; -let hasWebpack5 = false; - -if (window.webpackJsonp && !window.webpackJsonp.deckyShimmed) { - // Webpack 4, currently on stable - const wpRequire = window.webpackJsonp.push([ - [], - { get_require: (mod: any, _exports: any, wpRequire: any) => (mod.exports = wpRequire) }, - [['get_require']], - ]); +export let modules: any = []; - delete wpRequire.m.get_require; - delete wpRequire.c.get_require; - webpackCache = wpRequire.c; -} else { +function initModuleCache() { + const startTime = performance.now(); + logger.group("Webpack Module Init"); // Webpack 5, currently on beta - hasWebpack5 = true; - const id = Math.random(); - let initReq: any; + // Generate a fake module ID + const id = Math.random(); // really should be an int and not a float but who cares + let webpackRequire!: ((id: any) => Module) & {m: object}; + // Insert our module in a new chunk. + // The module will then be called with webpack's internal require function as its first argument window.webpackChunksteamui.push([ [id], {}, (r: any) => { - initReq = r; + webpackRequire = r; }, ]); - for (let i of Object.keys(initReq.m)) { + + logger.log("Initializing all modules. Errors here likely do not matter, as they are usually just failing module side effects."); + + // Loop over every module ID + for (let i of Object.keys(webpackRequire.m)) { try { - webpackCache[i] = initReq(i); + const module = webpackRequire(i); + if (module) { + modules.push(module); + } } catch (e) { - console.debug("[DFL:Webpack]: Ignoring require error for module", i, e); + logger.debug("Ignoring require error for module", i, e); } } + + logger.groupEnd(`Modules initialized in ${performance.now() - startTime}ms...`); } -export const allModules: Module[] = hasWebpack5 - ? Object.values(webpackCache).filter((x) => x) - : Object.keys(webpackCache) - .map((x) => webpackCache[x].exports) - .filter((x) => x); +initModuleCache(); export const findModule = (filter: FilterFn) => { - for (const m of allModules) { + for (const m of modules) { if (m.default && filter(m.default)) return m.default; if (filter(m)) return m; } }; +export const findModuleDetailsByExport = (filter: ExportFilterFn, minExports?: number): [module: Module | undefined, moduleExport: any, exportName: any] => { + for (const m of modules) { + if (!m) continue; + for (const mod of [m.default, m]) { + if (typeof mod !== 'object') continue; + if (minExports && Object.keys(mod).length < minExports) continue; + for (let exportName in mod) { + if (mod?.[exportName]) { + const filterRes = filter(mod[exportName], exportName); + if (filterRes) { + return [mod, mod[exportName], exportName]; + } else { + continue; + } + } + } + } + } + return [undefined, undefined, undefined]; +} + +export const findModuleByExport = (filter: ExportFilterFn, minExports?: number) => { + return findModuleDetailsByExport(filter, minExports)?.[0]; +} + +export const findModuleExport = (filter: ExportFilterFn, minExports?: number) => { + return findModuleDetailsByExport(filter, minExports)?.[1]; +} + +/** + * @deprecated use findModuleExport instead + */ export const findModuleChild = (filter: FindFn) => { - for (const m of allModules) { + for (const m of modules) { for (const mod of [m.default, m]) { const filterRes = filter(mod); if (filterRes) { @@ -74,7 +108,7 @@ export const findModuleChild = (filter: FindFn) => { export const findAllModules = (filter: FilterFn) => { const out = []; - for (const m of allModules) { + for (const m of modules) { if (m.default && filter(m.default)) out.push(m.default); if (filter(m)) out.push(m); } @@ -82,7 +116,7 @@ export const findAllModules = (filter: FilterFn) => { return out; }; -export const CommonUIModule = allModules.find((m: Module) => { +export const CommonUIModule = modules.find((m: Module) => { if (typeof m !== 'object') return false; for (let prop in m) { if (m[prop]?.contextType?._currentValue && Object.keys(m).length > 60) return true; @@ -90,18 +124,6 @@ export const CommonUIModule = allModules.find((m: Module) => { return false; }); -export const IconsModule = findModule((m: Module) => { - if (typeof m !== 'object') return false; - for (let prop in m) { - if (m[prop]?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(m[prop].toString())) return true; - } - return false; -}); +export const IconsModule = findModuleByExport(e => e?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(e.toString())); -export const ReactRouter = allModules.find((m: Module) => { - if (typeof m !== 'object') return undefined; - for (let prop in m) { - if (m[prop]?.computeRootMatch) return true; - } - return false; -}); +export const ReactRouter = findModuleByExport(e => e.computeRootMatch); diff --git a/tsconfig.json b/tsconfig.json index a92a3cbb..20a23180 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "noImplicitThis": true, "noImplicitAny": true, "strict": true, - "suppressImplicitAnyIndexErrors": true, + "removeComments": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true },