Compare commits

..

1 Commits

Author SHA1 Message Date
AAGaming
b94a595659 Add WIP Function Patching API 2022-05-21 00:02:59 -04:00
23 changed files with 3019 additions and 12640 deletions

View File

@@ -1,3 +0,0 @@
{
"extends": ["@commitlint/config-conventional"]
}

View File

@@ -1,29 +0,0 @@
name: Release
on:
push:
branches:
- main
jobs:
release:
name: Release
runs-on: ubuntu-22.04
steps:
- name: Setup | Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup | Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- name: Setup | Dependencies
run: npm ci
- name: Test
run: npm test
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm exec semantic-release

3
.gitignore vendored
View File

@@ -35,6 +35,3 @@ Thumbs.db
dist/ dist/
research/ research/
# PNPM lockfile
pnpm-lock.yaml

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit "${1}"

View File

@@ -1,17 +0,0 @@
{
"branches": ["main", "dev"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [CI SKIP]"
}
]
]
}

View File

@@ -1,13 +0,0 @@
# [0.2.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v0.1.0...v0.2.0) (2022-06-04)
### Features
* **router:** expose GetQuickAccessTab and rename QuickAccessTabs to QuickAccessTab ([bf0c2b1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/bf0c2b17bfc4e67a8aa90cfee6a91bd1482720d4))
# [0.1.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v0.0.6...v0.1.0) (2022-06-04)
### Features
* **router:** types for steam router ([62bf0ea](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/62bf0eaffa83d85245a038ffe3819315bd02f045))

5
globals.d.ts vendored
View File

@@ -1,5 +0,0 @@
declare global {
interface Window {
SP_REACT: typeof React;
}
}

12424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "decky-frontend-lib", "name": "decky-frontend-lib",
"version": "0.2.0", "version": "0.0.2",
"description": "A library for building decky plugins", "description": "A library for building decky plugins",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@@ -9,9 +9,7 @@
"build": "shx rm -rf dist && tsc -b", "build": "shx rm -rf dist && tsc -b",
"dev": "tsc -b -w", "dev": "tsc -b -w",
"prepack": "npm run build", "prepack": "npm run build",
"test": "echo 'No tests for now!'", "test": "jest"
"postinstall": "husky install",
"commit": "git-cz"
}, },
"files": [ "files": [
"/lib", "/lib",
@@ -34,26 +32,12 @@
"url": "https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues" "url": "https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues"
}, },
"homepage": "https://github.com/SteamDeckHomebrew/decky-frontend-lib#readme", "homepage": "https://github.com/SteamDeckHomebrew/decky-frontend-lib#readme",
"config": {
"commitizen": {
"path": "@commitlint/cz-commitlint"
}
},
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.0.2",
"@commitlint/config-conventional": "^17.0.2",
"@commitlint/cz-commitlint": "^17.0.0",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1",
"@types/jest": "^27.4.1", "@types/jest": "^27.4.1",
"@types/react": "16.14.0", "@types/react": "16.14.0",
"@types/react-router": "5.1.18",
"commitizen": "^4.2.4",
"husky": "^8.0.1",
"import-sort-style-module": "^6.0.0", "import-sort-style-module": "^6.0.0",
"jest": "^27.5.1", "jest": "^27.5.1",
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"semantic-release": "^19.0.2",
"shx": "^0.3.4", "shx": "^0.3.4",
"ts-jest": "^27.1.4", "ts-jest": "^27.1.4",
"typescript": "^4.6.3" "typescript": "^4.6.3"

2899
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,27 +3,14 @@ import { FC } from 'react';
import { CommonUIModule } from '../webpack'; import { CommonUIModule } from '../webpack';
interface ButtonProps { interface ButtonProps {
className?: string; label?: string;
noFocusRing?: boolean; description?: string;
disabled?: boolean; layout?: 'below';
onClick?(e: MouseEvent): void; onClick?(e: MouseEvent): void;
onPointerDown?(e: PointerEvent): void; disabled?: boolean;
onPointerUp?(e: PointerEvent): void; bottomSeparator?: boolean;
onPointerCancel?(e: PointerEvent): void;
onMouseDown?(e: PointerEvent): void;
onMouseUp?(e: MouseEvent): void;
onTouchStart?(e: TouchEvent): void;
onTouchEnd?(e: TouchEvent): void;
onTouchCancel?(e: TouchEvent): void;
onSubmit?(e: SubmitEvent): void;
} }
export const DialogButton = Object.values(CommonUIModule).find( export const Button = Object.values(CommonUIModule).find((mod: any) =>
(mod: any) => mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
mod?.render?.toString()?.includes('Object.assign({type:"button"') && ) as FC<ButtonProps>;
mod?.render?.toString()?.includes('DialogButton'),
) as any;
// Button isn't exported, so call DialogButton to grab it
export const Button = DialogButton!.render({}).type as FC<ButtonProps>; // its actually a forwarded ref but that doesn't really matter in usage

View File

@@ -1,17 +0,0 @@
import { FC } from 'react';
import { CommonUIModule } from '../webpack';
interface ButtonItemProps {
label?: string;
description?: string;
layout?: 'below';
icon?: JSX.Element;
onClick?(e: MouseEvent): void;
disabled?: boolean;
bottomSeparator?: boolean;
}
export const ButtonItem = Object.values(CommonUIModule).find((mod: any) =>
mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
) as FC<ButtonItemProps>;

11
src/deck-components/Menu.tsx Executable file → Normal file
View File

@@ -1,16 +1,7 @@
import { FC, ReactNode } from 'react'; import { FC } from 'react';
import { findModuleChild } from '../webpack'; import { findModuleChild } from '../webpack';
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];
}
}
});
interface MenuProps { interface MenuProps {
label: string; label: string;
onCancel?(): void; onCancel?(): void;

24
src/deck-components/Modal.tsx Executable file → Normal file
View File

@@ -1,28 +1,12 @@
import { FC, ReactNode } from 'react'; import { ReactNode } from 'react';
import { findModuleChild } from '../webpack'; import { findModuleChild } from '../webpack';
// TODO: there is another argument, figure out what it does export const showModal: (children: ReactNode, parent: EventTarget) => void = findModuleChild((m) => {
export const showModal: (children: ReactNode, parent?: EventTarget) => void = findModuleChild((m) => {
if (typeof m !== 'object') return undefined; if (typeof m !== 'object') return undefined;
for (let prop in m) { for (let prop in m) {
if (typeof m[prop] === 'function' && m[prop].toString().includes('bHideMainWindowForPopouts:!0')) { if (typeof m[prop] === 'function' && m[prop].toString().includes('stopPropagation))')) {
return m[prop]; return m[prop];
} }
} }
}); });
interface ModalRootProps {
onMiddleButton?(): void,
onCancel?(): void;
onOK?(): void;
bAllowFullSize?: boolean;
}
export const ModalRoot = 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<ModalRootProps>;

View File

@@ -1,23 +0,0 @@
import { FC } from 'react';
import { findModuleChild } from '../webpack';
interface PanelSectionProps {
title?: string;
spinner?: boolean;
}
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<PanelSectionProps>;
export const PanelSectionRow = Object.values(mod).filter(
(exp: any) => !exp?.toString()?.includes('.PanelSection'),
)[0] as FC;

View File

@@ -1,37 +0,0 @@
import { Module, findModuleChild } from '../webpack';
export enum SideMenu {
None,
Main,
QuickAccess,
}
export enum QuickAccessTab {
Notifications,
RemotePlayTogetherControls,
VoiceChat,
Friends,
Settings,
Perf,
Help,
Decky,
}
interface Router {
CloseSideMenus(): void;
OpenQuickAccessMenu(quickAccessTab: QuickAccessTab): void;
GetQuickAccessTab(): QuickAccessTab;
NavigateToExternalWeb(url: string): void;
ToggleSideMenu(sideMenu: SideMenu): void;
CloseSideMenus(): void;
OpenSideMenu(sideMenu: SideMenu): void;
OpenPowerMenu(unknown?: any): void;
get RunningApps(): any;
}
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;

View File

@@ -1,8 +0,0 @@
import { FC } from 'react';
import { IconsModule } from '../webpack';
// TODO type this and other icons?
export const Spinner = Object.values(IconsModule).find((mod: any) =>
mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString())
) as FC<{}>;

View File

@@ -1,9 +0,0 @@
import { FC } 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("PreloadThrobber")) return m[prop]
}
}) as FC<{}>;

5
src/deck-components/index.ts Executable file → Normal file
View File

@@ -1,11 +1,6 @@
export * from './Button'; export * from './Button';
export * from './ButtonItem';
export * from './Menu'; export * from './Menu';
export * from './Modal'; export * from './Modal';
export * from './Panel';
export * from './Router';
export * from './Slider'; export * from './Slider';
export * from './Spinner';
export * from './static-classes'; export * from './static-classes';
export * from './SteamSpinner';
export * from './Toggle'; export * from './Toggle';

View File

@@ -1,6 +1,3 @@
import type { ComponentType } from 'react';
import { RouteProps } from 'react-router';
export interface Plugin { export interface Plugin {
title: JSX.Element; title: JSX.Element;
icon: JSX.Element; icon: JSX.Element;
@@ -20,13 +17,7 @@ interface ServerResponseError {
type ServerResponse<TRes> = ServerResponseSuccess<TRes> | ServerResponseError; type ServerResponse<TRes> = ServerResponseSuccess<TRes> | ServerResponseError;
interface RouterHook {
addRoute(path: string, component: ComponentType, props?: Omit<RouteProps, 'path' | 'children'>): void;
removeRoute(path: string): void;
}
export interface ServerAPI { export interface ServerAPI {
routerHook: RouterHook;
callPluginMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>; callPluginMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
callServerMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>; callServerMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
fetchNoCors<TRes = {}>(url: string, request: RequestInfo): Promise<ServerResponse<TRes>>; fetchNoCors<TRes = {}>(url: string, request: RequestInfo): Promise<ServerResponse<TRes>>;

View File

@@ -1,41 +1,35 @@
import * as React from "react"; export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
// this shouldn't need to be redeclared but it does for some reason }
declare global { declare global {
interface Window { interface Window {
SP_REACT: typeof React; SP_REACT: any;
} }
} }
export function fakeRenderComponent(fun: Function): any { export function fakeRenderComponent(fun: Function): any {
const hooks = (window.SP_REACT as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current; const hooks = window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current;
// TODO: add more hooks // TODO: add more hooks
let oldHooks = { let oldHooks = {
useContext: hooks.useContext, useContext: hooks.useContext,
useCallback: hooks.useCallback, useCallback: hooks.useCallback,
useLayoutEffect: hooks.useLayoutEffect,
useEffect: hooks.useEffect, useEffect: hooks.useEffect,
useMemo: hooks.useMemo,
useRef: hooks.useRef,
useState: hooks.useState, useState: hooks.useState,
} }
hooks.useCallback = (cb: Function) => cb; hooks.useCallback = (cb: Function) => cb;
hooks.useContext = (cb: any) => cb._currentValue; hooks.useContext = (cb: Function) => cb;
hooks.useLayoutEffect = (_: Function) => {}//cb(); hooks.useEffect = (cb: Function) => cb();
hooks.useMemo = (cb: Function, _: any[]) => cb;
hooks.useEffect = (_: Function) => {}//cb();
hooks.useRef = (val: any) => ({current: val || {}});
hooks.useState = (v: any) => { hooks.useState = (v: any) => {
let val = v; let val = v;
return [val, (n: any) => val = n]; return [val, (n: any) => val = n];
}; };
const res = fun(hooks); const res = fun();
Object.assign(hooks, oldHooks); Object.assign(hooks, oldHooks);
@@ -56,8 +50,8 @@ export function beforePatch(obj: any, name: string, fnc: Function): void {
export function afterPatch(obj: any, name: string, fnc: Function): void { export function afterPatch(obj: any, name: string, fnc: Function): void {
const orig = obj[name]; const orig = obj[name];
obj[name] = function (...args: any[]) { obj[name] = function (...args: any[]) {
let ret = orig.call(this, ...args); let ret = orig.apply(...args);
ret = fnc.call(this, args, ret); ret = fnc(ret);
return ret; return ret;
} }
Object.assign(obj[name], orig); Object.assign(obj[name], orig);
@@ -65,23 +59,7 @@ export function afterPatch(obj: any, name: string, fnc: Function): void {
obj[name].__deckyOrig = orig; obj[name].__deckyOrig = orig;
} }
export function replacePatch(obj: any, name: string, fnc: Function): void {
const orig = obj[name];
obj[name] = function (...args: any[]) {
const ret = fnc.call(this, args);
if (ret == 'CALL_ORIGINAL') return orig.call(this, ...args);
return ret;
};
Object.assign(obj[name], orig);
obj[name].toString = () => orig.toString();
obj[name].__deckyOrig = orig;
}
// TODO allow one method to be patched and unpatched multiple times independently using IDs in a Map or something // TODO allow one method to be patched and unpatched multiple times independently using IDs in a Map or something
export function unpatch(obj: any, name: any): void { export function unpatch(obj: any, name: any): void {
obj[name] = obj[name].__deckyOrig; obj[name] = obj[name].__deckyOrig;
} }
export function getReactInstance(o: HTMLElement | Element | Node) {
return o[Object.keys(o).find(k => k.startsWith('__reactInternalInstance')) as string]
}

View File

@@ -5,7 +5,7 @@ declare global {
} }
// TODO // TODO
export type Module = any; type Module = any;
type FilterFn = (module: any) => boolean; type FilterFn = (module: any) => boolean;
type FindFn = (module: any) => any; type FindFn = (module: any) => any;
@@ -58,18 +58,7 @@ export const CommonUIModule = allModules.find((m: Module) => {
return false; return false;
}); });
export const IconsModule = findModule((m: Module) => { export const Router = findModuleChild((m: Module) => {
if (typeof m !== 'object') return false; if (typeof m !== "object") return undefined;
for (let prop in m) { for (let prop in m) { if (m[prop]?.Navigate && m[prop]?.NavigationManager) return m[prop]}
if (m[prop]?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(m[prop].toString())) return true; })
}
return false;
});
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;
});

View File

@@ -3,8 +3,7 @@
"outDir": "dist", "outDir": "dist",
"module": "ESNext", "module": "ESNext",
"target": "ES2020", "target": "ES2020",
"jsx": "react", "jsx": "react-jsx",
"jsxFactory": "window.SP_REACT.createElement",
"declaration": true, "declaration": true,
"moduleResolution": "node", "moduleResolution": "node",
"noUnusedLocals": true, "noUnusedLocals": true,
@@ -18,6 +17,6 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src", "globals.d.ts"], "include": ["src"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }