@pkhex-everywhere/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @pkhex-everywhere/react
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c1bc8fa: First release.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [c1bc8fa]
12
+ - @pkhex-everywhere/engine@0.1.0
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @pkhex-everywhere/react
2
+
3
+ React hooks for [`@pkhex-everywhere/engine`](https://www.npmjs.com/package/@pkhex-everywhere/engine). Hooks suspend until their data arrives and refetch when a command changes it.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @pkhex-everywhere/engine @pkhex-everywhere/react
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```tsx
14
+ import { Suspense } from 'react'
15
+ import { createEngine } from '@pkhex-everywhere/engine'
16
+ import { EngineProvider, useParty } from '@pkhex-everywhere/react'
17
+
18
+ const engine = createEngine()
19
+
20
+ function Party() {
21
+ const { party } = useParty()
22
+ return (
23
+ <ul>
24
+ {party.map((pokemon) => (
25
+ <li key={pokemon.id}>
26
+ {pokemon.species} Lv. {pokemon.level}
27
+ </li>
28
+ ))}
29
+ </ul>
30
+ )
31
+ }
32
+
33
+ export function App() {
34
+ return (
35
+ <EngineProvider engine={engine}>
36
+ <Suspense fallback="Loading…">
37
+ <Party />
38
+ </Suspense>
39
+ </EngineProvider>
40
+ )
41
+ }
42
+ ```
43
+
44
+ Load a save with `useLoadedGame().load(base64, fileName)` or `engine.game.load` before rendering hooks that read it.
45
+
46
+ ## License
47
+
48
+ GPL-3.0-or-later.
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from 'react';
2
+ import { EngineError, type Engine } from '@pkhex-everywhere/engine';
3
+ import { QueryCache } from './cache';
4
+ interface EngineContextValue {
5
+ engine: Engine;
6
+ cache: QueryCache;
7
+ }
8
+ interface EngineProviderProps {
9
+ engine: Engine;
10
+ onUnhandledError?: (error: EngineError) => void;
11
+ children?: ReactNode;
12
+ }
13
+ export declare function EngineProvider({ engine, onUnhandledError, children }: EngineProviderProps): import("react").JSX.Element;
14
+ export declare function useEngineContext(): EngineContextValue;
15
+ export declare function useEngine(): Engine;
16
+ export {};
@@ -0,0 +1,21 @@
1
+ type Entry = {
2
+ status: 'pending';
3
+ promise: Promise<void>;
4
+ } | {
5
+ status: 'success';
6
+ value: unknown;
7
+ } | {
8
+ status: 'error';
9
+ error: unknown;
10
+ };
11
+ export declare class QueryCache {
12
+ private readonly queries;
13
+ private readonly listeners;
14
+ get(key: string, topics: readonly string[], fetch: () => Promise<unknown>): Entry;
15
+ peek(key: string): Entry | undefined;
16
+ subscribe: (listener: () => void) => () => undefined;
17
+ invalidate: (changed: readonly string[]) => void;
18
+ private load;
19
+ private settle;
20
+ }
21
+ export {};
@@ -0,0 +1,43 @@
1
+ import type { Base64, ItemHandle, PokemonHandle, PokemonPatch, TrainerGender } from '@pkhex-everywhere/engine';
2
+ export declare function useBox(): {
3
+ addEncounter: (index: number) => Promise<import("@pkhex-everywhere/engine").AddedPokemon>;
4
+ addFromFile: (bytes: Base64) => Promise<import("@pkhex-everywhere/engine").AddedPokemon>;
5
+ box: import("@pkhex-everywhere/engine").PokemonSummary[];
6
+ };
7
+ export declare function useEvents(): {
8
+ giveTickets: (includeOldSeaMap: boolean) => Promise<string[]>;
9
+ setFlag: (index: number, value: boolean) => Promise<void>;
10
+ setWork: (index: number, value: number) => Promise<void>;
11
+ events: import("@pkhex-everywhere/engine").SaveEvents | null;
12
+ };
13
+ export declare function useLoadedGame(): {
14
+ close: () => Promise<void>;
15
+ export: () => Promise<import("@pkhex-everywhere/engine").ExportedSave>;
16
+ file: () => Promise<import("@pkhex-everywhere/engine").LoadedSave | null>;
17
+ load: (data: Base64, fileName: string) => Promise<void>;
18
+ loadBlank: (version: number) => Promise<void>;
19
+ game: import("@pkhex-everywhere/engine").SaveSummary | null;
20
+ };
21
+ export declare function useInventory(): {
22
+ setItem: (at: ItemHandle, count: number) => Promise<void>;
23
+ inventory: import("@pkhex-everywhere/engine").Pouch[];
24
+ };
25
+ export declare function useParty(): {
26
+ party: import("@pkhex-everywhere/engine").PokemonSummary[];
27
+ };
28
+ export declare function usePokemon(at: PokemonHandle): {
29
+ addToBox: () => Promise<import("@pkhex-everywhere/engine").AddedPokemon>;
30
+ clone: () => Promise<void>;
31
+ commit: () => Promise<import("@pkhex-everywhere/engine").PokemonId>;
32
+ edit: () => Promise<void>;
33
+ setLevel: (level: number) => Promise<void>;
34
+ update: (patch: PokemonPatch) => Promise<void>;
35
+ pokemon: import("@pkhex-everywhere/engine").PokemonSummary;
36
+ };
37
+ export declare function useTrainer(): {
38
+ setBattlePoints: (value: number) => Promise<void>;
39
+ setGender: (gender: TrainerGender) => Promise<void>;
40
+ setMoney: (amount: number) => Promise<void>;
41
+ setName: (name: string) => Promise<void>;
42
+ trainer: import("@pkhex-everywhere/engine").TrainerCard;
43
+ };
@@ -0,0 +1,4 @@
1
+ export { EngineProvider, useEngine } from './EngineProvider';
2
+ export { useQuery, type CallArgs, type CallResult } from './useQuery';
3
+ export * from './generated/hooks';
4
+ export { usePokemonDetails } from './usePokemonDetails';
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { EngineError, affects, queryTopics } from "@pkhex-everywhere/engine";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/cache.ts
5
+ var QueryCache = class {
6
+ queries = /* @__PURE__ */ new Map();
7
+ listeners = /* @__PURE__ */ new Set();
8
+ get(key, topics, fetch) {
9
+ const existing = this.queries.get(key);
10
+ if (existing) return existing.entry;
11
+ const query = {
12
+ topics,
13
+ fetch,
14
+ generation: 0
15
+ };
16
+ this.queries.set(key, query);
17
+ this.load(key, query);
18
+ return query.entry;
19
+ }
20
+ peek(key) {
21
+ return this.queries.get(key)?.entry;
22
+ }
23
+ subscribe = (listener) => {
24
+ this.listeners.add(listener);
25
+ return () => void this.listeners.delete(listener);
26
+ };
27
+ invalidate = (changed) => {
28
+ for (const [key, query] of this.queries) {
29
+ if (!affects(changed, query.topics)) continue;
30
+ if (query.entry.status === "error") this.queries.delete(key);
31
+ else this.load(key, query);
32
+ }
33
+ };
34
+ load(key, query) {
35
+ const generation = ++query.generation;
36
+ const promise = query.fetch().then((value) => this.settle(key, query, generation, {
37
+ status: "success",
38
+ value
39
+ }), (error) => this.settle(key, query, generation, {
40
+ status: "error",
41
+ error
42
+ }));
43
+ if (query.entry?.status !== "success") query.entry = {
44
+ status: "pending",
45
+ promise
46
+ };
47
+ }
48
+ settle(key, query, generation, entry) {
49
+ if (query.generation !== generation || this.queries.get(key) !== query) return;
50
+ query.entry = entry;
51
+ this.listeners.forEach((listener) => listener());
52
+ }
53
+ };
54
+ //#endregion
55
+ //#region src/EngineProvider.tsx
56
+ var EngineContext = createContext(null);
57
+ var reported = /* @__PURE__ */ new WeakSet();
58
+ function EngineProvider({ engine, onUnhandledError, children }) {
59
+ const [cache] = useState(() => new QueryCache());
60
+ useEffect(() => engine.subscribe(["*"], cache.invalidate), [engine, cache]);
61
+ const report = useRef(onUnhandledError);
62
+ report.current = onUnhandledError;
63
+ const reports = onUnhandledError !== void 0;
64
+ useEffect(() => {
65
+ if (!reports) return;
66
+ const handle = (event) => {
67
+ const { reason } = event;
68
+ if (!(reason instanceof EngineError) || reported.has(reason)) return;
69
+ reported.add(reason);
70
+ event.preventDefault();
71
+ report.current?.(reason);
72
+ };
73
+ window.addEventListener("unhandledrejection", handle);
74
+ return () => window.removeEventListener("unhandledrejection", handle);
75
+ }, [reports]);
76
+ return /* @__PURE__ */ jsx(EngineContext, {
77
+ value: {
78
+ engine,
79
+ cache
80
+ },
81
+ children
82
+ });
83
+ }
84
+ function useEngineContext() {
85
+ const context = useContext(EngineContext);
86
+ if (!context) throw new Error("Engine hooks must be used inside an EngineProvider.");
87
+ return context;
88
+ }
89
+ function useEngine() {
90
+ return useEngineContext().engine;
91
+ }
92
+ //#endregion
93
+ //#region src/useQuery.ts
94
+ function useQuery(call, ...args) {
95
+ const { engine, cache } = useEngineContext();
96
+ const key = JSON.stringify([call, args]);
97
+ const entry = useSyncExternalStore(cache.subscribe, () => cache.peek(key)) ?? cache.get(key, queryTopics[call], () => engine.call(call, args));
98
+ if (entry.status === "pending") throw entry.promise;
99
+ if (entry.status === "error") throw entry.error;
100
+ return entry.value;
101
+ }
102
+ //#endregion
103
+ //#region src/generated/hooks.ts
104
+ function useBox() {
105
+ const engine = useEngine();
106
+ return {
107
+ box: useQuery("box.get"),
108
+ ...useMemo(() => ({
109
+ addEncounter: (index) => engine.box.addEncounter(index),
110
+ addFromFile: (bytes) => engine.box.addFromFile(bytes)
111
+ }), [engine])
112
+ };
113
+ }
114
+ function useEvents() {
115
+ const engine = useEngine();
116
+ return {
117
+ events: useQuery("events.get"),
118
+ ...useMemo(() => ({
119
+ giveTickets: (includeOldSeaMap) => engine.events.giveTickets(includeOldSeaMap),
120
+ setFlag: (index, value) => engine.events.setFlag(index, value),
121
+ setWork: (index, value) => engine.events.setWork(index, value)
122
+ }), [engine])
123
+ };
124
+ }
125
+ function useLoadedGame() {
126
+ const engine = useEngine();
127
+ return {
128
+ game: useQuery("game.get"),
129
+ ...useMemo(() => ({
130
+ close: () => engine.game.close(),
131
+ export: () => engine.game.export(),
132
+ file: () => engine.game.file(),
133
+ load: (data, fileName) => engine.game.load(data, fileName),
134
+ loadBlank: (version) => engine.game.loadBlank(version)
135
+ }), [engine])
136
+ };
137
+ }
138
+ function useInventory() {
139
+ const engine = useEngine();
140
+ return {
141
+ inventory: useQuery("inventory.get"),
142
+ ...useMemo(() => ({ setItem: (at, count) => engine.inventory.setItem(at, count) }), [engine])
143
+ };
144
+ }
145
+ function useParty() {
146
+ return { party: useQuery("party.get") };
147
+ }
148
+ function usePokemon(at) {
149
+ const engine = useEngine();
150
+ const pokemon = useQuery("pokemon.get", at);
151
+ const key = JSON.stringify(at);
152
+ return {
153
+ pokemon,
154
+ ...useMemo(() => ({
155
+ addToBox: () => engine.pokemon.addToBox(),
156
+ clone: () => engine.pokemon.clone(at),
157
+ commit: () => engine.pokemon.commit(),
158
+ edit: () => engine.pokemon.edit(at),
159
+ setLevel: (level) => engine.pokemon.setLevel(at, level),
160
+ update: (patch) => engine.pokemon.update(at, patch)
161
+ }), [engine, key])
162
+ };
163
+ }
164
+ function useTrainer() {
165
+ const engine = useEngine();
166
+ return {
167
+ trainer: useQuery("trainer.get"),
168
+ ...useMemo(() => ({
169
+ setBattlePoints: (value) => engine.trainer.setBattlePoints(value),
170
+ setGender: (gender) => engine.trainer.setGender(gender),
171
+ setMoney: (amount) => engine.trainer.setMoney(amount),
172
+ setName: (name) => engine.trainer.setName(name)
173
+ }), [engine])
174
+ };
175
+ }
176
+ //#endregion
177
+ //#region src/usePokemonDetails.ts
178
+ function usePokemonDetails(at) {
179
+ const engine = useEngine();
180
+ const details = useQuery("pokemon.details", at);
181
+ const key = JSON.stringify(at);
182
+ return {
183
+ details,
184
+ update: useMemo(() => (patch) => engine.pokemon.update(at, patch), [engine, key])
185
+ };
186
+ }
187
+ //#endregion
188
+ export { EngineProvider, useBox, useEngine, useEvents, useInventory, useLoadedGame, useParty, usePokemon, usePokemonDetails, useQuery, useTrainer };
@@ -0,0 +1,5 @@
1
+ import type { PokemonHandle, PokemonPatch } from '@pkhex-everywhere/engine';
2
+ export declare function usePokemonDetails(at: PokemonHandle): {
3
+ details: import("@pkhex-everywhere/engine").EditablePokemon;
4
+ update: (patch: PokemonPatch) => Promise<void>;
5
+ };
@@ -0,0 +1,7 @@
1
+ import { type CallName, type EngineClient, type QueryName } from '@pkhex-everywhere/engine';
2
+ type Lookup<T, Path extends string> = Path extends `${infer Head}.${infer Rest}` ? Head extends keyof T ? Lookup<T[Head], Rest> : never : Path extends keyof T ? T[Path] : never;
3
+ type CallFunction<Name extends CallName> = Extract<Lookup<EngineClient, Name>, (...args: never[]) => Promise<unknown>>;
4
+ export type CallArgs<Name extends CallName> = Parameters<CallFunction<Name>>;
5
+ export type CallResult<Name extends CallName> = Awaited<ReturnType<CallFunction<Name>>>;
6
+ export declare function useQuery<Name extends QueryName>(call: Name, ...args: CallArgs<Name>): CallResult<Name>;
7
+ export {};
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@pkhex-everywhere/react",
3
+ "version": "0.1.0",
4
+ "description": "React hooks for @pkhex-everywhere/engine.",
5
+ "license": "GPL-3.0-or-later",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/arleypadua/PKHeX.Everywhere.git",
9
+ "directory": "src/PKHeX.Everywhere.Engine.Sdk/packages/react"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "source": "./src/index.ts",
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "publishConfig": {
24
+ "provenance": true
25
+ },
26
+ "scripts": {
27
+ "build": "vite build && tsc -p tsconfig.build.json",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "vitest run"
30
+ },
31
+ "peerDependencies": {
32
+ "@pkhex-everywhere/engine": "*",
33
+ "react": "^19.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@testing-library/react": "^16.3.3",
37
+ "@types/react": "^19.3.0",
38
+ "@types/react-dom": "^19.3.0",
39
+ "jsdom": "^30.1.1",
40
+ "react": "^19.3.0",
41
+ "react-dom": "^19.3.0",
42
+ "typescript": "~7.0.2",
43
+ "vite": "^8.3.1",
44
+ "vitest": "^5.0.3"
45
+ }
46
+ }