@codexo/exojs-react 0.15.2 → 0.16.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.
@@ -1,83 +1,76 @@
1
- import { ApplicationStatus } from '@codexo/exojs';
2
- import { useState, useEffect } from 'react';
3
- import { useExoApp } from './useExoApp.js';
1
+ import { useExoApp } from "./useExoApp.js";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { ApplicationState } from "@codexo/exojs";
4
4
 
5
+ //#region src/useScene.ts
5
6
  /**
6
- * Creates an instance of `SceneClass`, activates it on the ExoJS
7
- * {@link Application}, and returns it once the scene is live.
8
- *
9
- * On first call (engine not yet started) this hook calls `app.start(scene)`,
10
- * which initializes the render backend and begins the per-frame loop. On
11
- * subsequent dep-change remounts it calls `app.scene.setScene(scene)` to
12
- * switch scenes without restarting the engine.
13
- *
14
- * The scene is cleared (`setScene(null)`) when the component unmounts or
15
- * when `deps` change mirroring `useEffect` semantics.
16
- *
17
- * A failure in `app.start()`/`app.scene.setScene()` (e.g. a scene's `onLoad`
18
- * rejects) is caught and routed to {@link Application.onError} rather than
19
- * left as an unhandled promise rejection subscribe via
20
- * `app.onError.add(...)` or the {@link import('./ExoCanvas').ExoCanvas}
21
- * `onError` prop to observe it.
22
- *
23
- * @param SceneClass - Constructor for the scene to instantiate.
24
- * @param deps - Extra deps that trigger scene replacement when changed, in
25
- * addition to the stable `app` reference (same semantics as `useEffect`).
26
- * @returns The active scene instance, or `null` while it is loading.
27
- *
28
- * @example
29
- * ```tsx
30
- * function GameScreen() {
31
- * const scene = useScene(MyGameScene);
32
- * if (!scene) return null;
33
- * return <ScoreHud scene={scene} />;
34
- * }
35
- * ```
36
- */
37
- // eslint-disable-next-line @typescript-eslint/naming-convention
38
- function useScene(SceneClass, deps = []) {
39
- const app = useExoApp();
40
- const [scene, setScene] = useState(null);
41
- useEffect(() => {
42
- let cancelled = false;
43
- const s = new SceneClass();
44
- const apply = async () => {
45
- try {
46
- if (app.status === ApplicationStatus.Stopped) {
47
- // First activation — initialize the backend and start the frame loop.
48
- await app.start(s);
49
- }
50
- else {
51
- // Engine already running — switch scenes without restarting.
52
- await app.scene.setScene(s);
53
- }
54
- if (!cancelled) {
55
- setScene(s);
56
- }
57
- }
58
- catch (error) {
59
- // Route to Application.onError instead of leaving an unhandled
60
- // rejection — app.start()/setScene() reject rather than dispatching
61
- // onError themselves.
62
- app.onError.dispatch(error instanceof Error ? error : new Error(String(error)));
63
- }
64
- };
65
- void apply();
66
- return () => {
67
- cancelled = true;
68
- setScene(null);
69
- // Best-effort scene clear; the Application.destroy() called by
70
- // ExoCanvas cleanup will also handle any remaining active scene.
71
- void app.scene.setScene(null).catch((error) => {
72
- app.onError.dispatch(error instanceof Error ? error : new Error(String(error)));
73
- });
74
- };
75
- // SceneClass is intentionally excluded from deps: a new class reference
76
- // (e.g. inline arrow class) on every render would recreate the scene
77
- // each frame. Pass an explicit deps array to react to changes.
78
- }, [app, ...deps]);
79
- return scene;
80
- }
7
+ * Activates `SceneClass` on the ExoJS {@link Application} and returns the
8
+ * resulting instance once it is live. `SceneClass` must be registered in
9
+ * `ApplicationOptions.scenes` (passed to {@link import('./useExoApplication').useExoApplication}
10
+ * / {@link import('./ExoCanvas').ExoCanvas}) - unregistered targets reject in
11
+ * development builds.
12
+ *
13
+ * On first call (engine not yet started) this hook calls `app.start(SceneClass)`,
14
+ * which initializes the render backend and begins the per-frame loop. On
15
+ * subsequent dep-change remounts it calls `app.scenes.change(SceneClass)` to
16
+ * switch scenes without restarting the engine, constructing a fresh instance.
17
+ *
18
+ * Effects that run while startup is still in flight - React StrictMode
19
+ * double-mounts every effect in development - join that `app.start()` call
20
+ * instead of racing a second navigation against it, and only activate
21
+ * `SceneClass` afterwards if startup did not already leave it active. A
22
+ * StrictMode double mount therefore activates the scene exactly once.
23
+ *
24
+ * A failure in `app.start()`/`app.scenes.change()` (e.g. a scene's `load()`
25
+ * rejects) is caught and routed to {@link Application.onError} rather than
26
+ * left as an unhandled promise rejection - subscribe via
27
+ * `app.onError.add(...)` or the {@link import('./ExoCanvas').ExoCanvas}
28
+ * `onError` prop to observe it.
29
+ *
30
+ * @param SceneClass - Constructor for the scene to activate.
31
+ * @param deps - Extra deps that trigger scene replacement when changed, in
32
+ * addition to the stable `app` reference (same semantics as `useEffect`).
33
+ * @returns The active scene instance, or `null` while it is loading.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * function GameScreen() {
38
+ * const scene = useScene(MyGameScene);
39
+ * if (!scene) return null;
40
+ * return <ScoreHud scene={scene} />;
41
+ * }
42
+ * ```
43
+ */
44
+ const useScene = (SceneClass, deps = []) => {
45
+ const app = useExoApp();
46
+ const [scene, setScene] = useState(null);
47
+ const generationRef = useRef(0);
48
+ useEffect(() => {
49
+ let cancelled = false;
50
+ const generation = ++generationRef.current;
51
+ const isStale = () => cancelled || generationRef.current !== generation;
52
+ const target = SceneClass;
53
+ const apply = async () => {
54
+ try {
55
+ if (app.state === ApplicationState.Stopped || app.state === ApplicationState.Loading) {
56
+ await app.start(target);
57
+ if (isStale()) return;
58
+ if (!(app.scenes.currentScene instanceof SceneClass)) await app.scenes.change(target);
59
+ } else await app.scenes.change(target);
60
+ if (!isStale()) setScene(app.scenes.currentScene);
61
+ } catch (error) {
62
+ if (!isStale()) app.onError.dispatch(error instanceof Error ? error : new Error(String(error)));
63
+ }
64
+ };
65
+ apply();
66
+ return () => {
67
+ cancelled = true;
68
+ setScene(null);
69
+ };
70
+ }, [app, ...deps]);
71
+ return scene;
72
+ };
81
73
 
74
+ //#endregion
82
75
  export { useScene };
83
- //# sourceMappingURL=useScene.js.map
76
+ //# sourceMappingURL=useScene.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useScene.js","sources":["../../../src/useScene.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BG;AACH;SACgB,QAAQ,CAAkB,UAAuB,EAAE,OAAuB,EAAE,EAAA;AAC1F,IAAA,MAAM,GAAG,GAAG,SAAS,EAAE;IACvB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAW,IAAI,CAAC;IAElD,SAAS,CAAC,MAAK;QACb,IAAI,SAAS,GAAG,KAAK;AACrB,QAAA,MAAM,CAAC,GAAG,IAAI,UAAU,EAAE;AAE1B,QAAA,MAAM,KAAK,GAAG,YAA0B;AACtC,YAAA,IAAI;gBACF,IAAI,GAAG,CAAC,MAAM,KAAK,iBAAiB,CAAC,OAAO,EAAE;;AAE5C,oBAAA,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;gBACpB;qBAAO;;oBAEL,MAAM,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC7B;gBAEA,IAAI,CAAC,SAAS,EAAE;oBACd,QAAQ,CAAC,CAAC,CAAC;gBACb;YACF;YAAE,OAAO,KAAK,EAAE;;;;gBAId,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,YAAY,KAAK,GAAG,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACjF;AACF,QAAA,CAAC;QAED,KAAK,KAAK,EAAE;AAEZ,QAAA,OAAO,MAAK;YACV,SAAS,GAAG,IAAI;YAChB,QAAQ,CAAC,IAAI,CAAC;;;AAGd,YAAA,KAAK,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,KAAI;gBACrD,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,YAAY,KAAK,GAAG,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AACjF,YAAA,CAAC,CAAC;AACJ,QAAA,CAAC;;;;IAIH,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAElB,IAAA,OAAO,KAAK;AACd;;;;"}
1
+ {"version":3,"file":"useScene.js","names":[],"sources":["../../src/useScene.ts"],"sourcesContent":["import { ApplicationState, type Scene, type SceneConstructor } from '@codexo/exojs';\nimport { type DependencyList, useEffect, useRef, useState } from 'react';\n\nimport { useExoApp } from './useExoApp';\n\n/**\n * Activates `SceneClass` on the ExoJS {@link Application} and returns the\n * resulting instance once it is live. `SceneClass` must be registered in\n * `ApplicationOptions.scenes` (passed to {@link import('./useExoApplication').useExoApplication}\n * / {@link import('./ExoCanvas').ExoCanvas}) - unregistered targets reject in\n * development builds.\n *\n * On first call (engine not yet started) this hook calls `app.start(SceneClass)`,\n * which initializes the render backend and begins the per-frame loop. On\n * subsequent dep-change remounts it calls `app.scenes.change(SceneClass)` to\n * switch scenes without restarting the engine, constructing a fresh instance.\n *\n * Effects that run while startup is still in flight - React StrictMode\n * double-mounts every effect in development - join that `app.start()` call\n * instead of racing a second navigation against it, and only activate\n * `SceneClass` afterwards if startup did not already leave it active. A\n * StrictMode double mount therefore activates the scene exactly once.\n *\n * A failure in `app.start()`/`app.scenes.change()` (e.g. a scene's `load()`\n * rejects) is caught and routed to {@link Application.onError} rather than\n * left as an unhandled promise rejection - subscribe via\n * `app.onError.add(...)` or the {@link import('./ExoCanvas').ExoCanvas}\n * `onError` prop to observe it.\n *\n * @param SceneClass - Constructor for the scene to activate.\n * @param deps - Extra deps that trigger scene replacement when changed, in\n * addition to the stable `app` reference (same semantics as `useEffect`).\n * @returns The active scene instance, or `null` while it is loading.\n *\n * @example\n * ```tsx\n * function GameScreen() {\n * const scene = useScene(MyGameScene);\n * if (!scene) return null;\n * return <ScoreHud scene={scene} />;\n * }\n * ```\n */\nexport const useScene = <T extends Scene>(SceneClass: new () => T, deps: DependencyList = []): T | null => {\n const app = useExoApp();\n const [scene, setScene] = useState<T | null>(null);\n // Bumped on every effect run so an async `apply()` can tell whether a newer\n // run has since taken over - the flag below only covers a run whose own\n // cleanup has fired.\n const generationRef = useRef(0);\n\n useEffect(() => {\n let cancelled = false;\n const generation = ++generationRef.current;\n // Only the newest, still-mounted run may touch component/app state: an\n // earlier one's activation result is no longer what the component asked\n // for, and neither is its failure.\n const isStale = (): boolean => cancelled || generationRef.current !== generation;\n // This hook's contract has always been zero-arg activation only (no data\n // parameter) - `T extends Scene` (Data defaults to void), but that generic\n // `T` can't be distributed through the navigation call's conditional types\n // (InferSceneData/ChangeSceneArgs) inside this function body, so it's pinned\n // to its concrete void-data instantiation here.\n const target = SceneClass as SceneConstructor;\n\n const apply = async (): Promise<void> => {\n try {\n if (app.state === ApplicationState.Stopped || app.state === ApplicationState.Loading) {\n // Stopped: first activation, which initializes the backend and starts\n // the frame loop. Loading: an earlier effect's `start()` is still in\n // flight - including its own initial scene navigation, which\n // `scenes.change()` would collide with (navigation never queues, it\n // rejects). `start()` joins that in-flight run instead, ignoring the\n // target passed here.\n await app.start(target);\n\n if (isStale()) {\n return;\n }\n\n // Startup activates its own target, which is this one whenever the\n // joined `start()` came from an effect for the same scene (the\n // StrictMode double-mount case) - activating again would needlessly\n // tear the scene down and rebuild it. A `start()` that targeted a\n // different scene, or none at all, still needs the switch.\n if (!(app.scenes.currentScene instanceof SceneClass)) {\n await app.scenes.change(target);\n }\n } else {\n // The engine is already running: a scene switch without a restart.\n await app.scenes.change(target);\n }\n\n if (!isStale()) {\n setScene(app.scenes.currentScene as T);\n }\n } catch (error) {\n // Route to Application.onError instead of leaving an unhandled\n // rejection - app.start()/change() reject rather than dispatching\n // onError themselves. A superseded run stays silent: its failure is no\n // longer this component's state, exactly like its success.\n if (!isStale()) {\n app.onError.dispatch(error instanceof Error ? error : new Error(String(error)));\n }\n }\n };\n\n void apply();\n\n return () => {\n cancelled = true;\n setScene(null);\n // No public API switches the director back to scene-less mid-lifetime\n // (navigation always targets a registered\n // constructor). Application.destroy() (called by ExoCanvas cleanup)\n // tears down whatever scene is still active.\n };\n // SceneClass is intentionally excluded from deps: a new class reference\n // (e.g. inline arrow class) on every render would recreate the scene\n // each frame. Pass an explicit deps array to react to changes - which is\n // also why the spread is here and why the lint rule can't verify this list.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [app, ...deps]);\n\n return scene;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,YAA6B,YAAyB,OAAuB,CAAC,MAAgB;CACzG,MAAM,MAAM,UAAU;CACtB,MAAM,CAAC,OAAO,YAAY,SAAmB,IAAI;CAIjD,MAAM,gBAAgB,OAAO,CAAC;CAE9B,gBAAgB;EACd,IAAI,YAAY;EAChB,MAAM,aAAa,EAAE,cAAc;EAInC,MAAM,gBAAyB,aAAa,cAAc,YAAY;EAMtE,MAAM,SAAS;EAEf,MAAM,QAAQ,YAA2B;GACvC,IAAI;IACF,IAAI,IAAI,UAAU,iBAAiB,WAAW,IAAI,UAAU,iBAAiB,SAAS;KAOpF,MAAM,IAAI,MAAM,MAAM;KAEtB,IAAI,QAAQ,GACV;KAQF,IAAI,EAAE,IAAI,OAAO,wBAAwB,aACvC,MAAM,IAAI,OAAO,OAAO,MAAM;IAElC,OAEE,MAAM,IAAI,OAAO,OAAO,MAAM;IAGhC,IAAI,CAAC,QAAQ,GACX,SAAS,IAAI,OAAO,YAAiB;GAEzC,SAAS,OAAO;IAKd,IAAI,CAAC,QAAQ,GACX,IAAI,QAAQ,SAAS,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;GAElF;EACF;EAEA,AAAK,MAAM;EAEX,aAAa;GACX,YAAY;GACZ,SAAS,IAAI;EAKf;CAMF,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC;CAEjB,OAAO;AACT"}
@@ -5,7 +5,7 @@ import type { Signal } from '@codexo/exojs';
5
5
  * Built on `useSyncExternalStore`, so reads stay tear-free under concurrent
6
6
  * rendering.
7
7
  *
8
- * The signal itself is only used to know *when* to re-read `getSnapshot` is
8
+ * The signal itself is only used to know *when* to re-read - `getSnapshot` is
9
9
  * responsible for producing the actual value (usually a getter on the engine
10
10
  * object the signal lives on).
11
11
  *
@@ -20,9 +20,10 @@ import type { Signal } from '@codexo/exojs';
20
20
  * ```
21
21
  *
22
22
  * @param signal - The signal to subscribe to. `null`/`undefined` is accepted
23
- * (e.g. before an `Application` exists) the hook simply does not subscribe
23
+ * (e.g. before an `Application` exists) - the hook simply does not subscribe
24
24
  * to anything and `getSnapshot` still runs on every render.
25
25
  * @param getSnapshot - Reads the current value. Called on mount and again after
26
26
  * every dispatch of `signal`.
27
27
  */
28
- export declare function useSignal<Args extends unknown[], T>(signal: Signal<Args> | null | undefined, getSnapshot: () => T): T;
28
+ export declare const useSignal: <Args extends unknown[], T>(signal: Signal<Args> | null | undefined, getSnapshot: () => T) => T;
29
+ //# sourceMappingURL=useSignal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSignal.d.ts","sourceRoot":"","sources":["../../src/useSignal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAG5C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,SAAS,GAAI,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC,EAAE,QAAQ,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,SAAS,EAAE,aAAa,MAAM,CAAC,KAAG,CAoBpH,CAAC"}
@@ -1,45 +1,43 @@
1
- import { useCallback, useSyncExternalStore } from 'react';
1
+ import { useCallback, useSyncExternalStore } from "react";
2
2
 
3
+ //#region src/useSignal.ts
3
4
  /**
4
- * Subscribes to an ExoJS {@link Signal} and returns the latest value computed by
5
- * `getSnapshot`, re-rendering the component every time the signal dispatches.
6
- * Built on `useSyncExternalStore`, so reads stay tear-free under concurrent
7
- * rendering.
8
- *
9
- * The signal itself is only used to know *when* to re-read `getSnapshot` is
10
- * responsible for producing the actual value (usually a getter on the engine
11
- * object the signal lives on).
12
- *
13
- * @example
14
- * ```tsx
15
- * function FrameCounter() {
16
- * const app = useExoApp();
17
- * // Re-renders on every `onFrame` dispatch (i.e. every engine frame).
18
- * const frameCount = useSignal(app.onFrame, () => app.frameCount);
19
- * return <span>Frame: {frameCount}</span>;
20
- * }
21
- * ```
22
- *
23
- * @param signal - The signal to subscribe to. `null`/`undefined` is accepted
24
- * (e.g. before an `Application` exists) the hook simply does not subscribe
25
- * to anything and `getSnapshot` still runs on every render.
26
- * @param getSnapshot - Reads the current value. Called on mount and again after
27
- * every dispatch of `signal`.
28
- */
29
- function useSignal(signal, getSnapshot) {
30
- const subscribe = useCallback((onStoreChange) => {
31
- if (!signal) {
32
- // No signal to subscribe to (e.g. before an Application exists) — nothing to unsubscribe either.
33
- // eslint-disable-next-line @typescript-eslint/no-empty-function -- intentional no-op unsubscribe
34
- return () => { };
35
- }
36
- signal.add(onStoreChange);
37
- return () => {
38
- signal.remove(onStoreChange);
39
- };
40
- }, [signal]);
41
- return useSyncExternalStore(subscribe, getSnapshot);
42
- }
5
+ * Subscribes to an ExoJS {@link Signal} and returns the latest value computed by
6
+ * `getSnapshot`, re-rendering the component every time the signal dispatches.
7
+ * Built on `useSyncExternalStore`, so reads stay tear-free under concurrent
8
+ * rendering.
9
+ *
10
+ * The signal itself is only used to know *when* to re-read - `getSnapshot` is
11
+ * responsible for producing the actual value (usually a getter on the engine
12
+ * object the signal lives on).
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * function FrameCounter() {
17
+ * const app = useExoApp();
18
+ * // Re-renders on every `onFrame` dispatch (i.e. every engine frame).
19
+ * const frameCount = useSignal(app.onFrame, () => app.frameCount);
20
+ * return <span>Frame: {frameCount}</span>;
21
+ * }
22
+ * ```
23
+ *
24
+ * @param signal - The signal to subscribe to. `null`/`undefined` is accepted
25
+ * (e.g. before an `Application` exists) - the hook simply does not subscribe
26
+ * to anything and `getSnapshot` still runs on every render.
27
+ * @param getSnapshot - Reads the current value. Called on mount and again after
28
+ * every dispatch of `signal`.
29
+ */
30
+ const useSignal = (signal, getSnapshot) => {
31
+ const subscribe = useCallback((onStoreChange) => {
32
+ if (!signal) return () => {};
33
+ signal.add(onStoreChange);
34
+ return () => {
35
+ signal.remove(onStoreChange);
36
+ };
37
+ }, [signal]);
38
+ return useSyncExternalStore(subscribe, getSnapshot);
39
+ };
43
40
 
41
+ //#endregion
44
42
  export { useSignal };
45
- //# sourceMappingURL=useSignal.js.map
43
+ //# sourceMappingURL=useSignal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSignal.js","sources":["../../../src/useSignal.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACG,SAAU,SAAS,CAA4B,MAAuC,EAAE,WAAoB,EAAA;AAChH,IAAA,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,aAAyB,KAAkB;QAC1C,IAAI,CAAC,MAAM,EAAE;;;AAGX,YAAA,OAAO,MAAK,EAAE,CAAC;QACjB;AAEA,QAAA,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC;AAEzB,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC;AAC9B,QAAA,CAAC;AACH,IAAA,CAAC,EACD,CAAC,MAAM,CAAC,CACT;AAED,IAAA,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,CAAC;AACrD;;;;"}
1
+ {"version":3,"file":"useSignal.js","names":[],"sources":["../../src/useSignal.ts"],"sourcesContent":["import type { Signal } from '@codexo/exojs';\nimport { useCallback, useSyncExternalStore } from 'react';\n\n/**\n * Subscribes to an ExoJS {@link Signal} and returns the latest value computed by\n * `getSnapshot`, re-rendering the component every time the signal dispatches.\n * Built on `useSyncExternalStore`, so reads stay tear-free under concurrent\n * rendering.\n *\n * The signal itself is only used to know *when* to re-read - `getSnapshot` is\n * responsible for producing the actual value (usually a getter on the engine\n * object the signal lives on).\n *\n * @example\n * ```tsx\n * function FrameCounter() {\n * const app = useExoApp();\n * // Re-renders on every `onFrame` dispatch (i.e. every engine frame).\n * const frameCount = useSignal(app.onFrame, () => app.frameCount);\n * return <span>Frame: {frameCount}</span>;\n * }\n * ```\n *\n * @param signal - The signal to subscribe to. `null`/`undefined` is accepted\n * (e.g. before an `Application` exists) - the hook simply does not subscribe\n * to anything and `getSnapshot` still runs on every render.\n * @param getSnapshot - Reads the current value. Called on mount and again after\n * every dispatch of `signal`.\n */\nexport const useSignal = <Args extends unknown[], T>(signal: Signal<Args> | null | undefined, getSnapshot: () => T): T => {\n const subscribe = useCallback(\n (onStoreChange: () => void): (() => void) => {\n if (!signal) {\n // No signal to subscribe to (e.g. before an Application exists) - nothing to unsubscribe either.\n return () => {\n // Nothing was subscribed, so there is nothing to unsubscribe.\n };\n }\n\n signal.add(onStoreChange);\n\n return () => {\n signal.remove(onStoreChange);\n };\n },\n [signal],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,aAAwC,QAAyC,gBAA4B;CACxH,MAAM,YAAY,aACf,kBAA4C;EAC3C,IAAI,CAAC,QAEH,aAAa,CAEb;EAGF,OAAO,IAAI,aAAa;EAExB,aAAa;GACX,OAAO,OAAO,aAAa;EAC7B;CACF,GACA,CAAC,MAAM,CACT;CAEA,OAAO,qBAAqB,WAAW,WAAW;AACpD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexo/exojs-react",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "React integration for ExoJS.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "peerDependencies": {
29
- "@codexo/exojs": "0.15.x",
29
+ "@codexo/exojs": "0.16.x",
30
30
  "react": ">=18.0.0",
31
31
  "react-dom": ">=18.0.0"
32
32
  },
@@ -36,18 +36,22 @@
36
36
  }
37
37
  },
38
38
  "devDependencies": {
39
- "@types/react": "^18.0.0",
40
- "@codexo/exojs-config": "0.0.0",
41
- "@codexo/exojs": "0.15.2"
39
+ "@testing-library/dom": "^10.4.1",
40
+ "@testing-library/react": "^16.3.2",
41
+ "@types/react": "^19.2.17",
42
+ "eslint": "~10.9.1",
43
+ "eslint-config-prettier": "^10.1.8",
44
+ "@codexo/exojs": "0.16.0",
45
+ "@codexo/exojs-config": "0.0.0"
42
46
  },
43
47
  "license": "MIT",
44
48
  "publishConfig": {
45
49
  "access": "public"
46
50
  },
47
51
  "scripts": {
48
- "build": "tsx ../../node_modules/rollup/dist/bin/rollup -c --environment EXOJS_ENV:production",
49
- "build:dev": "tsx ../../node_modules/rollup/dist/bin/rollup -c --environment EXOJS_ENV:development",
50
- "typecheck": "tsc --noEmit",
51
- "lint": "eslint \"src/**/*.{ts,tsx}\""
52
+ "build": "tsx ../../scripts/build-extension.ts",
53
+ "build:dev": "tsx ../../scripts/build-extension.ts --dev",
54
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc --noEmit -p tsconfig.examples.json",
55
+ "lint": "eslint --max-warnings=0 ."
52
56
  }
53
57
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}