From c635038005cf01d97050cf1ec35d3cca6704c13b Mon Sep 17 00:00:00 2001 From: KoSHeroff Date: Sat, 25 Jul 2026 19:32:18 +0500 Subject: [PATCH] feat: add runCommand/useRunCommand for page-triggered commands runCommand(command) posts on the "command" channel so the mod can run it as the player. Only works from a server-declared trusted origin; dropped otherwise. useRunCommand() returns a stable callback. - add runCommand + useRunCommand + tests - bump to 1.6.0 --- package-lock.json | 4 ++-- package.json | 2 +- src/hooks/useRunCommand.ts | 13 +++++++++++++ src/index.ts | 4 ++++ src/runCommand.ts | 16 ++++++++++++++++ test/runCommand.test.ts | 24 ++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/hooks/useRunCommand.ts create mode 100644 src/runCommand.ts create mode 100644 test/runCommand.test.ts diff --git a/package-lock.json b/package-lock.json index cad7fbc..4063467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@webgui/react", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@webgui/react", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "devDependencies": { "@testing-library/dom": "^10.4.1", diff --git a/package.json b/package.json index a498f35..0de6398 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@webgui/react", - "version": "1.5.0", + "version": "1.6.0", "description": "React hooks and types for the WebGUI Minecraft mod — access window.webgui client data from any SPA.", "keywords": [ "minecraft", diff --git a/src/hooks/useRunCommand.ts b/src/hooks/useRunCommand.ts new file mode 100644 index 0000000..ea9d7ce --- /dev/null +++ b/src/hooks/useRunCommand.ts @@ -0,0 +1,13 @@ +import { useCallback } from 'react'; +import { runCommand } from '../runCommand'; + +/** + * Returns a stable {@link runCommand} callback. + * + * @example + * const run = useRunCommand(); + * + */ +export function useRunCommand(): (command: string) => void { + return useCallback((command: string) => runCommand(command), []); +} diff --git a/src/index.ts b/src/index.ts index 52f0c8e..30b2b8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,9 @@ export type { // Utils export { isInMod, isReady } from './utils'; +// Commands +export { runCommand } from './runCommand'; + // Hooks export { useWebGUIClient } from './hooks/useWebGUIClient'; export { useWebGUIEntity } from './hooks/useWebGUIEntity'; @@ -24,3 +27,4 @@ export { useWebGUISelector } from './hooks/useWebGUISelector'; export { useCloseGui } from './hooks/useCloseGui'; export { useWebGUIToken } from './hooks/useWebGUIToken'; export { useWebGUIEvent } from './hooks/useWebGUIEvent'; +export { useRunCommand } from './hooks/useRunCommand'; diff --git a/src/runCommand.ts b/src/runCommand.ts new file mode 100644 index 0000000..e5f6980 --- /dev/null +++ b/src/runCommand.ts @@ -0,0 +1,16 @@ +/** + * Runs a Minecraft command as the player, exactly as if they had typed it in chat. + * + * Only works when the page is served from an origin the server declared trusted + * (`trustedCommandOrigins` in the mod's server config). From any other origin the mod + * silently drops the request. Fire-and-forget: command feedback appears in the game, not here. + * + * @param command the command, with or without a leading slash (e.g. `"give @s minecraft:diamond 1"`) + * + * @example + * runCommand('give @s minecraft:diamond 1'); + */ +export function runCommand(command: string): void { + if (typeof globalThis.window === 'undefined') return; + globalThis.window.webgui?.postToGame({ channel: 'command', command }); +} diff --git a/test/runCommand.test.ts b/test/runCommand.test.ts new file mode 100644 index 0000000..3432aa4 --- /dev/null +++ b/test/runCommand.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { installWebgui } from './helpers'; +import { runCommand } from '../src/runCommand'; + +describe('runCommand', () => { + beforeEach(() => { + delete (window as unknown as { webgui?: unknown }).webgui; + }); + + it('posts the command on the command channel', () => { + const ns = installWebgui(); + runCommand('give @s minecraft:diamond 1'); + + expect(ns.postToGame).toHaveBeenCalledTimes(1); + expect(ns.postToGame.mock.calls[0][0]).toEqual({ + channel: 'command', + command: 'give @s minecraft:diamond 1', + }); + }); + + it('is a no-op when not running in the mod', () => { + expect(() => runCommand('spawn')).not.toThrow(); + }); +});