@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 +12 -0
- package/README.md +48 -0
- package/dist/EngineProvider.d.ts +16 -0
- package/dist/cache.d.ts +21 -0
- package/dist/generated/hooks.d.ts +43 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +188 -0
- package/dist/usePokemonDetails.d.ts +5 -0
- package/dist/useQuery.d.ts +7 -0
- package/package.json +46 -0
package/CHANGELOG.md
ADDED
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 {};
|
package/dist/cache.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/index.d.ts
ADDED
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,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
|
+
}
|