jotai-state-tree 1.7.4 → 1.8.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/README.md +20 -0
- package/dist/{chunk-WEWWBVTQ.mjs → chunk-MGW4FLIA.mjs} +41 -10
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +41 -10
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +3 -3
- package/dist/react.d.ts +3 -3
- package/dist/react.js +40 -9
- package/dist/react.mjs +1 -1
- package/dist/{router-DzUdXVV7.d.mts → router-9U4hkUrl.d.mts} +5 -1
- package/dist/{router-DzUdXVV7.d.ts → router-9U4hkUrl.d.ts} +5 -1
- package/package.json +2 -2
- package/src/__tests__/performance.test.ts +15 -15
- package/src/__tests__/router.test.tsx +4 -0
- package/src/__tests__/utilities_extra.test.ts +89 -6
- package/src/router.ts +45 -9
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
A MobX-State-Tree (MST) compatible state management library powered by [Jotai](https://jotai.org/).
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/jotai-state-tree)
|
|
6
|
+
[](https://github.com/bmartel/jotai-state-tree/actions/workflows/release.yml)
|
|
7
|
+
[](https://github.com/bmartel/jotai-state-tree/actions/workflows/release.yml)
|
|
6
8
|
[](LICENSE)
|
|
7
9
|
|
|
8
10
|
`jotai-state-tree` combines the transactional, tree-structured state model of MobX-State-Tree with the lightweight, zero-leak, high-performance atomic updates of Jotai. It is designed to be an API-compatible, drop-in replacement for MobX-State-Tree, featuring perfect TypeScript type safety out of the box.
|
|
@@ -32,6 +34,24 @@ npm install jotai-state-tree jotai
|
|
|
32
34
|
|
|
33
35
|
---
|
|
34
36
|
|
|
37
|
+
## React Native Compatibility
|
|
38
|
+
|
|
39
|
+
`jotai-state-tree` is fully compatible with React Native projects.
|
|
40
|
+
|
|
41
|
+
### Prerequisites & JS Engine
|
|
42
|
+
- **React Native Version**: `>= 0.70` is required.
|
|
43
|
+
- **JavaScript Engine**: The library relies on native ES2021 `WeakRef` and `FinalizationRegistry` features for memory management. If you use the Hermes engine (default since React Native 0.70), it must be version `0.12.0` or newer.
|
|
44
|
+
|
|
45
|
+
### Using the Router in React Native
|
|
46
|
+
When running in React Native (or any non-browser environment), the built-in state router automatically disables DOM/browser integration and behaves as a fully-featured **in-memory router**. It maintains a navigation history stack internally, enabling you to use:
|
|
47
|
+
- `push(path)` / `replace(path)`
|
|
48
|
+
- `go(delta)` / `goBack()` / `goForward()`
|
|
49
|
+
- `RouteView` to reactively render screen components based on the active path
|
|
50
|
+
|
|
51
|
+
This allows you to manage native navigation state trees with full time-travel, middleware, and action recording support!
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
35
55
|
## Quick Start
|
|
36
56
|
|
|
37
57
|
```typescript
|
|
@@ -3516,6 +3516,7 @@ function createActionRecorder(target) {
|
|
|
3516
3516
|
// src/router.ts
|
|
3517
3517
|
import { createContext, useContext } from "react";
|
|
3518
3518
|
import { useAtomValue } from "jotai";
|
|
3519
|
+
var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.location !== "undefined" && typeof window.history !== "undefined";
|
|
3519
3520
|
var RouteDefinition = model("RouteDefinition", {
|
|
3520
3521
|
path: string,
|
|
3521
3522
|
name: string,
|
|
@@ -3643,7 +3644,9 @@ var RouterModel = model("RouterModel", {
|
|
|
3643
3644
|
})).volatile(() => ({
|
|
3644
3645
|
beforeNavigate: null,
|
|
3645
3646
|
afterNavigate: null,
|
|
3646
|
-
_popStateListener: null
|
|
3647
|
+
_popStateListener: null,
|
|
3648
|
+
_historyStack: [],
|
|
3649
|
+
_historyIndex: -1
|
|
3647
3650
|
})).actions((self) => {
|
|
3648
3651
|
return {
|
|
3649
3652
|
setGuards(before, after) {
|
|
@@ -3663,13 +3666,30 @@ var RouterModel = model("RouterModel", {
|
|
|
3663
3666
|
self.params = matched ? matched.params : {};
|
|
3664
3667
|
self.query = parsed.query;
|
|
3665
3668
|
self.currentRouteName = matched ? matched.route.name : null;
|
|
3666
|
-
if (
|
|
3669
|
+
if (isBrowser) {
|
|
3667
3670
|
const fullPath = pathname + search + hash;
|
|
3668
3671
|
if (action === "PUSH") {
|
|
3669
3672
|
window.history.pushState(state, "", fullPath);
|
|
3670
3673
|
} else if (action === "REPLACE") {
|
|
3671
3674
|
window.history.replaceState(state, "", fullPath);
|
|
3672
3675
|
}
|
|
3676
|
+
} else {
|
|
3677
|
+
const fullPath = pathname + search + hash;
|
|
3678
|
+
if (action === "PUSH") {
|
|
3679
|
+
self._historyStack = self._historyStack.slice(0, self._historyIndex + 1);
|
|
3680
|
+
self._historyStack.push(fullPath);
|
|
3681
|
+
self._historyIndex = self._historyStack.length - 1;
|
|
3682
|
+
} else if (action === "REPLACE") {
|
|
3683
|
+
if (self._historyIndex === -1) {
|
|
3684
|
+
self._historyStack = [fullPath];
|
|
3685
|
+
self._historyIndex = 0;
|
|
3686
|
+
} else {
|
|
3687
|
+
self._historyStack[self._historyIndex] = fullPath;
|
|
3688
|
+
}
|
|
3689
|
+
} else if (action === "INITIAL") {
|
|
3690
|
+
self._historyStack = [fullPath];
|
|
3691
|
+
self._historyIndex = 0;
|
|
3692
|
+
}
|
|
3673
3693
|
}
|
|
3674
3694
|
},
|
|
3675
3695
|
push: flow(function* (path, state) {
|
|
@@ -3743,23 +3763,34 @@ var RouterModel = model("RouterModel", {
|
|
|
3743
3763
|
}
|
|
3744
3764
|
}),
|
|
3745
3765
|
go(delta) {
|
|
3746
|
-
if (
|
|
3766
|
+
if (isBrowser) {
|
|
3747
3767
|
window.history.go(delta);
|
|
3768
|
+
} else {
|
|
3769
|
+
const nextIndex = self._historyIndex + delta;
|
|
3770
|
+
if (nextIndex >= 0 && nextIndex < self._historyStack.length) {
|
|
3771
|
+
self._historyIndex = nextIndex;
|
|
3772
|
+
const targetPath = self._historyStack[nextIndex];
|
|
3773
|
+
self.syncLocation(targetPath, "", "", "POP");
|
|
3774
|
+
}
|
|
3748
3775
|
}
|
|
3749
3776
|
},
|
|
3750
3777
|
goBack() {
|
|
3751
|
-
if (
|
|
3778
|
+
if (isBrowser) {
|
|
3752
3779
|
window.history.back();
|
|
3780
|
+
} else {
|
|
3781
|
+
self.go(-1);
|
|
3753
3782
|
}
|
|
3754
3783
|
},
|
|
3755
3784
|
goForward() {
|
|
3756
|
-
if (
|
|
3785
|
+
if (isBrowser) {
|
|
3757
3786
|
window.history.forward();
|
|
3787
|
+
} else {
|
|
3788
|
+
self.go(1);
|
|
3758
3789
|
}
|
|
3759
3790
|
}
|
|
3760
3791
|
};
|
|
3761
3792
|
}).afterCreate((self) => {
|
|
3762
|
-
if (
|
|
3793
|
+
if (isBrowser) {
|
|
3763
3794
|
const handlePopState = (event) => {
|
|
3764
3795
|
const parsed = parseUrl(window.location.pathname + window.location.search + window.location.hash);
|
|
3765
3796
|
const matched = matchRoutes(self.routes, parsed.pathname);
|
|
@@ -3797,7 +3828,7 @@ var RouterModel = model("RouterModel", {
|
|
|
3797
3828
|
if (res === false) {
|
|
3798
3829
|
revert();
|
|
3799
3830
|
} else if (typeof res === "string") {
|
|
3800
|
-
self.
|
|
3831
|
+
self.replace(res);
|
|
3801
3832
|
} else {
|
|
3802
3833
|
proceed();
|
|
3803
3834
|
}
|
|
@@ -3808,7 +3839,7 @@ var RouterModel = model("RouterModel", {
|
|
|
3808
3839
|
if (result === false) {
|
|
3809
3840
|
revert();
|
|
3810
3841
|
} else if (typeof result === "string") {
|
|
3811
|
-
self.
|
|
3842
|
+
self.replace(result);
|
|
3812
3843
|
} else {
|
|
3813
3844
|
proceed();
|
|
3814
3845
|
}
|
|
@@ -3821,7 +3852,7 @@ var RouterModel = model("RouterModel", {
|
|
|
3821
3852
|
self.setPopStateListener(handlePopState);
|
|
3822
3853
|
}
|
|
3823
3854
|
}).beforeDestroy((self) => {
|
|
3824
|
-
if (
|
|
3855
|
+
if (isBrowser && self._popStateListener) {
|
|
3825
3856
|
window.removeEventListener("popstate", self._popStateListener);
|
|
3826
3857
|
self.setPopStateListener(null);
|
|
3827
3858
|
}
|
|
@@ -3835,7 +3866,7 @@ function createRouter(config) {
|
|
|
3835
3866
|
initialPathname = parsed.pathname;
|
|
3836
3867
|
initialSearch = parsed.search;
|
|
3837
3868
|
initialHash = parsed.hash;
|
|
3838
|
-
} else if (
|
|
3869
|
+
} else if (isBrowser) {
|
|
3839
3870
|
initialPathname = window.location.pathname;
|
|
3840
3871
|
initialSearch = window.location.search;
|
|
3841
3872
|
initialHash = window.location.hash;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as
|
|
2
|
-
export {
|
|
1
|
+
import { I as IType, a as ISimpleType, b as ILiteralType, c as IEnumerationType, d as IFrozenType, e as IIdentifierType, f as IIdentifierNumberType, M as ModelProperties, g as IModelType, h as MixinConfig, i as IMixin, j as IAnyType, k as IArrayType, l as IMapType, m as IOptionalType, n as IMaybeType, o as IMaybeNullType, p as IUnionType, U as UnionOptions, q as ILateType, r as IAnyModelType, R as ReferenceOptions, s as IReferenceType, t as ISafeReferenceType, u as IRefinementType, v as IDisposer, S as SnapshotIn, w as Instance } from './router-9U4hkUrl.mjs';
|
|
2
|
+
export { C as CustomTypeOptions, x as IActionRecorder, y as IActionRecording, z as IAnyComplexType, A as IAnyMixin, B as IHistoryEntry, D as IJsonPatch, E as IMSTArray, F as IMSTMap, G as IReversibleJsonPatch, H as IStateTreeNode, J as ITimeTravelManager, K as IUndoManager, L as IUndoManagerOptions, N as IValidationContext, O as IValidationError, P as IValidationResult, Q as ModelInstance, T as ModelSelf, V as RouteDefinition, W as RouterModel, X as SnapshotOut, Y as applyPatch, Z as applySnapshot, _ as cleanupStaleEntries, $ as clearAllRegistries, a0 as clone, a1 as cloneDeep, a2 as createActionRecorder, a3 as createRouter, a4 as createTimeTravelManager, a5 as createUndoManager, a6 as destroy, a7 as detach, a8 as findAll, a9 as findFirst, aa as freeze, ab as getEnv, ac as getGlobalStore, ad as getIdentifier, ae as getMembers, af as getOrCreatePath, ag as getParent, ah as getParentOfType, ai as getPath, aj as getPathParts, ak as getRegistryStats, al as getRelativePath, am as getRoot, an as getSnapshot, ao as getTreeStats, ap as getType, aq as hasParent, ar as haveSameRoot, as as isAlive, at as isAncestor, au as isFrozen, av as isRoot, aw as isStateTreeNode, ax as isValidReference, ay as onAction, az as onLifecycleChange, aA as onPatch, aB as onSnapshot, aC as recordPatches, aD as resetGlobalStore, aE as resolveIdentifier, aF as resolvePath, aG as setGlobalStore, aH as tryGetParent, aI as tryResolve, aJ as unfreeze, aK as walk } from './router-9U4hkUrl.mjs';
|
|
3
3
|
import 'jotai/vanilla/internals';
|
|
4
4
|
import 'jotai';
|
|
5
5
|
import 'react';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as
|
|
2
|
-
export {
|
|
1
|
+
import { I as IType, a as ISimpleType, b as ILiteralType, c as IEnumerationType, d as IFrozenType, e as IIdentifierType, f as IIdentifierNumberType, M as ModelProperties, g as IModelType, h as MixinConfig, i as IMixin, j as IAnyType, k as IArrayType, l as IMapType, m as IOptionalType, n as IMaybeType, o as IMaybeNullType, p as IUnionType, U as UnionOptions, q as ILateType, r as IAnyModelType, R as ReferenceOptions, s as IReferenceType, t as ISafeReferenceType, u as IRefinementType, v as IDisposer, S as SnapshotIn, w as Instance } from './router-9U4hkUrl.js';
|
|
2
|
+
export { C as CustomTypeOptions, x as IActionRecorder, y as IActionRecording, z as IAnyComplexType, A as IAnyMixin, B as IHistoryEntry, D as IJsonPatch, E as IMSTArray, F as IMSTMap, G as IReversibleJsonPatch, H as IStateTreeNode, J as ITimeTravelManager, K as IUndoManager, L as IUndoManagerOptions, N as IValidationContext, O as IValidationError, P as IValidationResult, Q as ModelInstance, T as ModelSelf, V as RouteDefinition, W as RouterModel, X as SnapshotOut, Y as applyPatch, Z as applySnapshot, _ as cleanupStaleEntries, $ as clearAllRegistries, a0 as clone, a1 as cloneDeep, a2 as createActionRecorder, a3 as createRouter, a4 as createTimeTravelManager, a5 as createUndoManager, a6 as destroy, a7 as detach, a8 as findAll, a9 as findFirst, aa as freeze, ab as getEnv, ac as getGlobalStore, ad as getIdentifier, ae as getMembers, af as getOrCreatePath, ag as getParent, ah as getParentOfType, ai as getPath, aj as getPathParts, ak as getRegistryStats, al as getRelativePath, am as getRoot, an as getSnapshot, ao as getTreeStats, ap as getType, aq as hasParent, ar as haveSameRoot, as as isAlive, at as isAncestor, au as isFrozen, av as isRoot, aw as isStateTreeNode, ax as isValidReference, ay as onAction, az as onLifecycleChange, aA as onPatch, aB as onSnapshot, aC as recordPatches, aD as resetGlobalStore, aE as resolveIdentifier, aF as resolvePath, aG as setGlobalStore, aH as tryGetParent, aI as tryResolve, aJ as unfreeze, aK as walk } from './router-9U4hkUrl.js';
|
|
3
3
|
import 'jotai/vanilla/internals';
|
|
4
4
|
import 'jotai';
|
|
5
5
|
import 'react';
|
package/dist/index.js
CHANGED
|
@@ -4338,6 +4338,7 @@ function createActionRecorder(target) {
|
|
|
4338
4338
|
// src/router.ts
|
|
4339
4339
|
var import_react = require("react");
|
|
4340
4340
|
var import_jotai4 = require("jotai");
|
|
4341
|
+
var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.location !== "undefined" && typeof window.history !== "undefined";
|
|
4341
4342
|
var RouteDefinition = model("RouteDefinition", {
|
|
4342
4343
|
path: string,
|
|
4343
4344
|
name: string,
|
|
@@ -4465,7 +4466,9 @@ var RouterModel = model("RouterModel", {
|
|
|
4465
4466
|
})).volatile(() => ({
|
|
4466
4467
|
beforeNavigate: null,
|
|
4467
4468
|
afterNavigate: null,
|
|
4468
|
-
_popStateListener: null
|
|
4469
|
+
_popStateListener: null,
|
|
4470
|
+
_historyStack: [],
|
|
4471
|
+
_historyIndex: -1
|
|
4469
4472
|
})).actions((self) => {
|
|
4470
4473
|
return {
|
|
4471
4474
|
setGuards(before, after) {
|
|
@@ -4485,13 +4488,30 @@ var RouterModel = model("RouterModel", {
|
|
|
4485
4488
|
self.params = matched ? matched.params : {};
|
|
4486
4489
|
self.query = parsed.query;
|
|
4487
4490
|
self.currentRouteName = matched ? matched.route.name : null;
|
|
4488
|
-
if (
|
|
4491
|
+
if (isBrowser) {
|
|
4489
4492
|
const fullPath = pathname + search + hash;
|
|
4490
4493
|
if (action === "PUSH") {
|
|
4491
4494
|
window.history.pushState(state, "", fullPath);
|
|
4492
4495
|
} else if (action === "REPLACE") {
|
|
4493
4496
|
window.history.replaceState(state, "", fullPath);
|
|
4494
4497
|
}
|
|
4498
|
+
} else {
|
|
4499
|
+
const fullPath = pathname + search + hash;
|
|
4500
|
+
if (action === "PUSH") {
|
|
4501
|
+
self._historyStack = self._historyStack.slice(0, self._historyIndex + 1);
|
|
4502
|
+
self._historyStack.push(fullPath);
|
|
4503
|
+
self._historyIndex = self._historyStack.length - 1;
|
|
4504
|
+
} else if (action === "REPLACE") {
|
|
4505
|
+
if (self._historyIndex === -1) {
|
|
4506
|
+
self._historyStack = [fullPath];
|
|
4507
|
+
self._historyIndex = 0;
|
|
4508
|
+
} else {
|
|
4509
|
+
self._historyStack[self._historyIndex] = fullPath;
|
|
4510
|
+
}
|
|
4511
|
+
} else if (action === "INITIAL") {
|
|
4512
|
+
self._historyStack = [fullPath];
|
|
4513
|
+
self._historyIndex = 0;
|
|
4514
|
+
}
|
|
4495
4515
|
}
|
|
4496
4516
|
},
|
|
4497
4517
|
push: flow(function* (path, state) {
|
|
@@ -4565,23 +4585,34 @@ var RouterModel = model("RouterModel", {
|
|
|
4565
4585
|
}
|
|
4566
4586
|
}),
|
|
4567
4587
|
go(delta) {
|
|
4568
|
-
if (
|
|
4588
|
+
if (isBrowser) {
|
|
4569
4589
|
window.history.go(delta);
|
|
4590
|
+
} else {
|
|
4591
|
+
const nextIndex = self._historyIndex + delta;
|
|
4592
|
+
if (nextIndex >= 0 && nextIndex < self._historyStack.length) {
|
|
4593
|
+
self._historyIndex = nextIndex;
|
|
4594
|
+
const targetPath = self._historyStack[nextIndex];
|
|
4595
|
+
self.syncLocation(targetPath, "", "", "POP");
|
|
4596
|
+
}
|
|
4570
4597
|
}
|
|
4571
4598
|
},
|
|
4572
4599
|
goBack() {
|
|
4573
|
-
if (
|
|
4600
|
+
if (isBrowser) {
|
|
4574
4601
|
window.history.back();
|
|
4602
|
+
} else {
|
|
4603
|
+
self.go(-1);
|
|
4575
4604
|
}
|
|
4576
4605
|
},
|
|
4577
4606
|
goForward() {
|
|
4578
|
-
if (
|
|
4607
|
+
if (isBrowser) {
|
|
4579
4608
|
window.history.forward();
|
|
4609
|
+
} else {
|
|
4610
|
+
self.go(1);
|
|
4580
4611
|
}
|
|
4581
4612
|
}
|
|
4582
4613
|
};
|
|
4583
4614
|
}).afterCreate((self) => {
|
|
4584
|
-
if (
|
|
4615
|
+
if (isBrowser) {
|
|
4585
4616
|
const handlePopState = (event) => {
|
|
4586
4617
|
const parsed = parseUrl(window.location.pathname + window.location.search + window.location.hash);
|
|
4587
4618
|
const matched = matchRoutes(self.routes, parsed.pathname);
|
|
@@ -4619,7 +4650,7 @@ var RouterModel = model("RouterModel", {
|
|
|
4619
4650
|
if (res === false) {
|
|
4620
4651
|
revert();
|
|
4621
4652
|
} else if (typeof res === "string") {
|
|
4622
|
-
self.
|
|
4653
|
+
self.replace(res);
|
|
4623
4654
|
} else {
|
|
4624
4655
|
proceed();
|
|
4625
4656
|
}
|
|
@@ -4630,7 +4661,7 @@ var RouterModel = model("RouterModel", {
|
|
|
4630
4661
|
if (result === false) {
|
|
4631
4662
|
revert();
|
|
4632
4663
|
} else if (typeof result === "string") {
|
|
4633
|
-
self.
|
|
4664
|
+
self.replace(result);
|
|
4634
4665
|
} else {
|
|
4635
4666
|
proceed();
|
|
4636
4667
|
}
|
|
@@ -4643,7 +4674,7 @@ var RouterModel = model("RouterModel", {
|
|
|
4643
4674
|
self.setPopStateListener(handlePopState);
|
|
4644
4675
|
}
|
|
4645
4676
|
}).beforeDestroy((self) => {
|
|
4646
|
-
if (
|
|
4677
|
+
if (isBrowser && self._popStateListener) {
|
|
4647
4678
|
window.removeEventListener("popstate", self._popStateListener);
|
|
4648
4679
|
self.setPopStateListener(null);
|
|
4649
4680
|
}
|
|
@@ -4657,7 +4688,7 @@ function createRouter(config) {
|
|
|
4657
4688
|
initialPathname = parsed.pathname;
|
|
4658
4689
|
initialSearch = parsed.search;
|
|
4659
4690
|
initialHash = parsed.hash;
|
|
4660
|
-
} else if (
|
|
4691
|
+
} else if (isBrowser) {
|
|
4661
4692
|
initialPathname = window.location.pathname;
|
|
4662
4693
|
initialSearch = window.location.search;
|
|
4663
4694
|
initialHash = window.location.hash;
|
package/dist/index.mjs
CHANGED
package/dist/react.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
export {
|
|
1
|
+
import React, { FC, ReactNode, ComponentType } from 'react';
|
|
2
|
+
import { ac as getGlobalStore, J as ITimeTravelManager, L as IUndoManagerOptions, K as IUndoManager } from './router-9U4hkUrl.mjs';
|
|
3
|
+
export { aL as RouterContext, aM as hasStateTreeNode, aN as useRouter } from './router-9U4hkUrl.mjs';
|
|
4
4
|
import 'jotai/vanilla/internals';
|
|
5
5
|
import 'jotai';
|
|
6
6
|
|
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
export {
|
|
1
|
+
import React, { FC, ReactNode, ComponentType } from 'react';
|
|
2
|
+
import { ac as getGlobalStore, J as ITimeTravelManager, L as IUndoManagerOptions, K as IUndoManager } from './router-9U4hkUrl.js';
|
|
3
|
+
export { aL as RouterContext, aM as hasStateTreeNode, aN as useRouter } from './router-9U4hkUrl.js';
|
|
4
4
|
import 'jotai/vanilla/internals';
|
|
5
5
|
import 'jotai';
|
|
6
6
|
|
package/dist/react.js
CHANGED
|
@@ -2467,6 +2467,7 @@ function maybeNull(type) {
|
|
|
2467
2467
|
}
|
|
2468
2468
|
|
|
2469
2469
|
// src/router.ts
|
|
2470
|
+
var isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.location !== "undefined" && typeof window.history !== "undefined";
|
|
2470
2471
|
var RouteDefinition = model("RouteDefinition", {
|
|
2471
2472
|
path: string,
|
|
2472
2473
|
name: string,
|
|
@@ -2594,7 +2595,9 @@ var RouterModel = model("RouterModel", {
|
|
|
2594
2595
|
})).volatile(() => ({
|
|
2595
2596
|
beforeNavigate: null,
|
|
2596
2597
|
afterNavigate: null,
|
|
2597
|
-
_popStateListener: null
|
|
2598
|
+
_popStateListener: null,
|
|
2599
|
+
_historyStack: [],
|
|
2600
|
+
_historyIndex: -1
|
|
2598
2601
|
})).actions((self) => {
|
|
2599
2602
|
return {
|
|
2600
2603
|
setGuards(before, after) {
|
|
@@ -2614,13 +2617,30 @@ var RouterModel = model("RouterModel", {
|
|
|
2614
2617
|
self.params = matched ? matched.params : {};
|
|
2615
2618
|
self.query = parsed.query;
|
|
2616
2619
|
self.currentRouteName = matched ? matched.route.name : null;
|
|
2617
|
-
if (
|
|
2620
|
+
if (isBrowser) {
|
|
2618
2621
|
const fullPath = pathname + search + hash;
|
|
2619
2622
|
if (action === "PUSH") {
|
|
2620
2623
|
window.history.pushState(state, "", fullPath);
|
|
2621
2624
|
} else if (action === "REPLACE") {
|
|
2622
2625
|
window.history.replaceState(state, "", fullPath);
|
|
2623
2626
|
}
|
|
2627
|
+
} else {
|
|
2628
|
+
const fullPath = pathname + search + hash;
|
|
2629
|
+
if (action === "PUSH") {
|
|
2630
|
+
self._historyStack = self._historyStack.slice(0, self._historyIndex + 1);
|
|
2631
|
+
self._historyStack.push(fullPath);
|
|
2632
|
+
self._historyIndex = self._historyStack.length - 1;
|
|
2633
|
+
} else if (action === "REPLACE") {
|
|
2634
|
+
if (self._historyIndex === -1) {
|
|
2635
|
+
self._historyStack = [fullPath];
|
|
2636
|
+
self._historyIndex = 0;
|
|
2637
|
+
} else {
|
|
2638
|
+
self._historyStack[self._historyIndex] = fullPath;
|
|
2639
|
+
}
|
|
2640
|
+
} else if (action === "INITIAL") {
|
|
2641
|
+
self._historyStack = [fullPath];
|
|
2642
|
+
self._historyIndex = 0;
|
|
2643
|
+
}
|
|
2624
2644
|
}
|
|
2625
2645
|
},
|
|
2626
2646
|
push: flow(function* (path, state) {
|
|
@@ -2694,23 +2714,34 @@ var RouterModel = model("RouterModel", {
|
|
|
2694
2714
|
}
|
|
2695
2715
|
}),
|
|
2696
2716
|
go(delta) {
|
|
2697
|
-
if (
|
|
2717
|
+
if (isBrowser) {
|
|
2698
2718
|
window.history.go(delta);
|
|
2719
|
+
} else {
|
|
2720
|
+
const nextIndex = self._historyIndex + delta;
|
|
2721
|
+
if (nextIndex >= 0 && nextIndex < self._historyStack.length) {
|
|
2722
|
+
self._historyIndex = nextIndex;
|
|
2723
|
+
const targetPath = self._historyStack[nextIndex];
|
|
2724
|
+
self.syncLocation(targetPath, "", "", "POP");
|
|
2725
|
+
}
|
|
2699
2726
|
}
|
|
2700
2727
|
},
|
|
2701
2728
|
goBack() {
|
|
2702
|
-
if (
|
|
2729
|
+
if (isBrowser) {
|
|
2703
2730
|
window.history.back();
|
|
2731
|
+
} else {
|
|
2732
|
+
self.go(-1);
|
|
2704
2733
|
}
|
|
2705
2734
|
},
|
|
2706
2735
|
goForward() {
|
|
2707
|
-
if (
|
|
2736
|
+
if (isBrowser) {
|
|
2708
2737
|
window.history.forward();
|
|
2738
|
+
} else {
|
|
2739
|
+
self.go(1);
|
|
2709
2740
|
}
|
|
2710
2741
|
}
|
|
2711
2742
|
};
|
|
2712
2743
|
}).afterCreate((self) => {
|
|
2713
|
-
if (
|
|
2744
|
+
if (isBrowser) {
|
|
2714
2745
|
const handlePopState = (event) => {
|
|
2715
2746
|
const parsed = parseUrl(window.location.pathname + window.location.search + window.location.hash);
|
|
2716
2747
|
const matched = matchRoutes(self.routes, parsed.pathname);
|
|
@@ -2748,7 +2779,7 @@ var RouterModel = model("RouterModel", {
|
|
|
2748
2779
|
if (res === false) {
|
|
2749
2780
|
revert();
|
|
2750
2781
|
} else if (typeof res === "string") {
|
|
2751
|
-
self.
|
|
2782
|
+
self.replace(res);
|
|
2752
2783
|
} else {
|
|
2753
2784
|
proceed();
|
|
2754
2785
|
}
|
|
@@ -2759,7 +2790,7 @@ var RouterModel = model("RouterModel", {
|
|
|
2759
2790
|
if (result === false) {
|
|
2760
2791
|
revert();
|
|
2761
2792
|
} else if (typeof result === "string") {
|
|
2762
|
-
self.
|
|
2793
|
+
self.replace(result);
|
|
2763
2794
|
} else {
|
|
2764
2795
|
proceed();
|
|
2765
2796
|
}
|
|
@@ -2772,7 +2803,7 @@ var RouterModel = model("RouterModel", {
|
|
|
2772
2803
|
self.setPopStateListener(handlePopState);
|
|
2773
2804
|
}
|
|
2774
2805
|
}).beforeDestroy((self) => {
|
|
2775
|
-
if (
|
|
2806
|
+
if (isBrowser && self._popStateListener) {
|
|
2776
2807
|
window.removeEventListener("popstate", self._popStateListener);
|
|
2777
2808
|
self.setPopStateListener(null);
|
|
2778
2809
|
}
|
package/dist/react.mjs
CHANGED
|
@@ -674,6 +674,8 @@ declare const RouterModel: IModelType<{
|
|
|
674
674
|
beforeNavigate: ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null;
|
|
675
675
|
afterNavigate: ((to: any) => void) | null;
|
|
676
676
|
_popStateListener: ((event: PopStateEvent) => void) | null;
|
|
677
|
+
_historyStack: string[];
|
|
678
|
+
_historyIndex: number;
|
|
677
679
|
}>;
|
|
678
680
|
declare function createRouter(config: {
|
|
679
681
|
routes: Array<{
|
|
@@ -721,8 +723,10 @@ declare function createRouter(config: {
|
|
|
721
723
|
beforeNavigate: ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null;
|
|
722
724
|
afterNavigate: ((to: any) => void) | null;
|
|
723
725
|
_popStateListener: ((event: PopStateEvent) => void) | null;
|
|
726
|
+
_historyStack: string[];
|
|
727
|
+
_historyIndex: number;
|
|
724
728
|
};
|
|
725
729
|
declare const RouterContext: React.Context<any>;
|
|
726
730
|
declare function useRouter(): any;
|
|
727
731
|
|
|
728
|
-
export {
|
|
732
|
+
export { clearAllRegistries as $, type IAnyMixin as A, type IHistoryEntry as B, type CustomTypeOptions as C, type IJsonPatch as D, type IMSTArray as E, type IMSTMap as F, type IReversibleJsonPatch as G, type IStateTreeNode as H, type IType as I, type ITimeTravelManager as J, type IUndoManager as K, type IUndoManagerOptions as L, type ModelProperties as M, type IValidationContext as N, type IValidationError as O, type IValidationResult as P, type ModelInstance as Q, type ReferenceOptions as R, type SnapshotIn as S, type ModelSelf as T, type UnionOptions as U, RouteDefinition as V, RouterModel as W, type SnapshotOut as X, applyPatch as Y, applySnapshot as Z, cleanupStaleEntries as _, type ISimpleType as a, clone as a0, cloneDeep as a1, createActionRecorder as a2, createRouter as a3, createTimeTravelManager as a4, createUndoManager as a5, destroy as a6, detach as a7, findAll as a8, findFirst as a9, onPatch as aA, onSnapshot as aB, recordPatches as aC, resetGlobalStore as aD, resolveIdentifier as aE, resolvePath as aF, setGlobalStore as aG, tryGetParent as aH, tryResolve as aI, unfreeze as aJ, walk as aK, RouterContext as aL, hasStateTreeNode as aM, useRouter as aN, freeze as aa, getEnv as ab, getGlobalStore as ac, getIdentifier as ad, getMembers as ae, getOrCreatePath as af, getParent as ag, getParentOfType as ah, getPath as ai, getPathParts as aj, getRegistryStats as ak, getRelativePath as al, getRoot as am, getSnapshot as an, getTreeStats as ao, getType as ap, hasParent as aq, haveSameRoot as ar, isAlive as as, isAncestor as at, isFrozen as au, isRoot as av, isStateTreeNode as aw, isValidReference as ax, onAction as ay, onLifecycleChange as az, type ILiteralType as b, type IEnumerationType as c, type IFrozenType as d, type IIdentifierType as e, type IIdentifierNumberType as f, type IModelType as g, type MixinConfig as h, type IMixin as i, type IAnyType as j, type IArrayType as k, type IMapType as l, type IOptionalType as m, type IMaybeType as n, type IMaybeNullType as o, type IUnionType as p, type ILateType as q, type IAnyModelType as r, type IReferenceType as s, type ISafeReferenceType as t, type IRefinementType as u, type IDisposer as v, type Instance as w, type IActionRecorder as x, type IActionRecording as y, type IAnyComplexType as z };
|
|
@@ -674,6 +674,8 @@ declare const RouterModel: IModelType<{
|
|
|
674
674
|
beforeNavigate: ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null;
|
|
675
675
|
afterNavigate: ((to: any) => void) | null;
|
|
676
676
|
_popStateListener: ((event: PopStateEvent) => void) | null;
|
|
677
|
+
_historyStack: string[];
|
|
678
|
+
_historyIndex: number;
|
|
677
679
|
}>;
|
|
678
680
|
declare function createRouter(config: {
|
|
679
681
|
routes: Array<{
|
|
@@ -721,8 +723,10 @@ declare function createRouter(config: {
|
|
|
721
723
|
beforeNavigate: ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null;
|
|
722
724
|
afterNavigate: ((to: any) => void) | null;
|
|
723
725
|
_popStateListener: ((event: PopStateEvent) => void) | null;
|
|
726
|
+
_historyStack: string[];
|
|
727
|
+
_historyIndex: number;
|
|
724
728
|
};
|
|
725
729
|
declare const RouterContext: React.Context<any>;
|
|
726
730
|
declare function useRouter(): any;
|
|
727
731
|
|
|
728
|
-
export {
|
|
732
|
+
export { clearAllRegistries as $, type IAnyMixin as A, type IHistoryEntry as B, type CustomTypeOptions as C, type IJsonPatch as D, type IMSTArray as E, type IMSTMap as F, type IReversibleJsonPatch as G, type IStateTreeNode as H, type IType as I, type ITimeTravelManager as J, type IUndoManager as K, type IUndoManagerOptions as L, type ModelProperties as M, type IValidationContext as N, type IValidationError as O, type IValidationResult as P, type ModelInstance as Q, type ReferenceOptions as R, type SnapshotIn as S, type ModelSelf as T, type UnionOptions as U, RouteDefinition as V, RouterModel as W, type SnapshotOut as X, applyPatch as Y, applySnapshot as Z, cleanupStaleEntries as _, type ISimpleType as a, clone as a0, cloneDeep as a1, createActionRecorder as a2, createRouter as a3, createTimeTravelManager as a4, createUndoManager as a5, destroy as a6, detach as a7, findAll as a8, findFirst as a9, onPatch as aA, onSnapshot as aB, recordPatches as aC, resetGlobalStore as aD, resolveIdentifier as aE, resolvePath as aF, setGlobalStore as aG, tryGetParent as aH, tryResolve as aI, unfreeze as aJ, walk as aK, RouterContext as aL, hasStateTreeNode as aM, useRouter as aN, freeze as aa, getEnv as ab, getGlobalStore as ac, getIdentifier as ad, getMembers as ae, getOrCreatePath as af, getParent as ag, getParentOfType as ah, getPath as ai, getPathParts as aj, getRegistryStats as ak, getRelativePath as al, getRoot as am, getSnapshot as an, getTreeStats as ao, getType as ap, hasParent as aq, haveSameRoot as ar, isAlive as as, isAncestor as at, isFrozen as au, isRoot as av, isStateTreeNode as aw, isValidReference as ax, onAction as ay, onLifecycleChange as az, type ILiteralType as b, type IEnumerationType as c, type IFrozenType as d, type IIdentifierType as e, type IIdentifierNumberType as f, type IModelType as g, type MixinConfig as h, type IMixin as i, type IAnyType as j, type IArrayType as k, type IMapType as l, type IOptionalType as m, type IMaybeType as n, type IMaybeNullType as o, type IUnionType as p, type ILateType as q, type IAnyModelType as r, type IReferenceType as s, type ISafeReferenceType as t, type IRefinementType as u, type IDisposer as v, type Instance as w, type IActionRecorder as x, type IActionRecording as y, type IAnyComplexType as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jotai-state-tree",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "MobX-State-Tree API compatible library powered by Jotai",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"semantic-release": "^25.0.2",
|
|
78
78
|
"tsup": "^8.0.0",
|
|
79
79
|
"typedoc": "^0.28.15",
|
|
80
|
-
"typescript": "^
|
|
80
|
+
"typescript": "^6.0.3",
|
|
81
81
|
"vitest": "^4.0.16"
|
|
82
82
|
},
|
|
83
83
|
"repository": {
|
|
@@ -57,8 +57,8 @@ describe("Performance", () => {
|
|
|
57
57
|
const elapsed = performance.now() - start;
|
|
58
58
|
|
|
59
59
|
expect(instances.length).toBe(10000);
|
|
60
|
-
// Should complete in reasonable time (less than 5 seconds on most machines)
|
|
61
|
-
expect(elapsed).toBeLessThan(
|
|
60
|
+
// Should complete in reasonable time (less than 5 seconds on most machines, relaxed for CI runners)
|
|
61
|
+
expect(elapsed).toBeLessThan(15000);
|
|
62
62
|
|
|
63
63
|
// Cleanup
|
|
64
64
|
instances.forEach((i) => destroy(i));
|
|
@@ -84,7 +84,7 @@ describe("Performance", () => {
|
|
|
84
84
|
const tree = Branch.create(createTree(10)); // 2^10 = 1024 leaf nodes
|
|
85
85
|
const elapsed = performance.now() - start;
|
|
86
86
|
|
|
87
|
-
expect(elapsed).toBeLessThan(
|
|
87
|
+
expect(elapsed).toBeLessThan(15000);
|
|
88
88
|
|
|
89
89
|
destroy(tree);
|
|
90
90
|
});
|
|
@@ -109,7 +109,7 @@ describe("Performance", () => {
|
|
|
109
109
|
const elapsed = performance.now() - start;
|
|
110
110
|
|
|
111
111
|
expect(list.items.length).toBe(10000);
|
|
112
|
-
expect(elapsed).toBeLessThan(
|
|
112
|
+
expect(elapsed).toBeLessThan(15000);
|
|
113
113
|
|
|
114
114
|
destroy(list);
|
|
115
115
|
});
|
|
@@ -138,7 +138,7 @@ describe("Performance", () => {
|
|
|
138
138
|
const elapsed = performance.now() - start;
|
|
139
139
|
|
|
140
140
|
expect(counter.value).toBe(10000);
|
|
141
|
-
expect(elapsed).toBeLessThan(
|
|
141
|
+
expect(elapsed).toBeLessThan(10000);
|
|
142
142
|
|
|
143
143
|
destroy(counter);
|
|
144
144
|
});
|
|
@@ -181,7 +181,7 @@ describe("Performance", () => {
|
|
|
181
181
|
const elapsed = performance.now() - start;
|
|
182
182
|
|
|
183
183
|
expect(list.items.length).toBe(500);
|
|
184
|
-
expect(elapsed).toBeLessThan(
|
|
184
|
+
expect(elapsed).toBeLessThan(15000);
|
|
185
185
|
|
|
186
186
|
destroy(list);
|
|
187
187
|
});
|
|
@@ -218,7 +218,7 @@ describe("Performance", () => {
|
|
|
218
218
|
|
|
219
219
|
const elapsed = performance.now() - start;
|
|
220
220
|
|
|
221
|
-
expect(elapsed).toBeLessThan(
|
|
221
|
+
expect(elapsed).toBeLessThan(15000);
|
|
222
222
|
|
|
223
223
|
destroy(instance);
|
|
224
224
|
});
|
|
@@ -252,7 +252,7 @@ describe("Performance", () => {
|
|
|
252
252
|
|
|
253
253
|
const elapsed = performance.now() - start;
|
|
254
254
|
|
|
255
|
-
expect(elapsed).toBeLessThan(
|
|
255
|
+
expect(elapsed).toBeLessThan(10000);
|
|
256
256
|
|
|
257
257
|
destroy(store);
|
|
258
258
|
});
|
|
@@ -294,7 +294,7 @@ describe("Performance", () => {
|
|
|
294
294
|
const elapsed = performance.now() - start;
|
|
295
295
|
|
|
296
296
|
expect(callCount).toBe(10000); // 100 listeners * 100 updates
|
|
297
|
-
expect(elapsed).toBeLessThan(
|
|
297
|
+
expect(elapsed).toBeLessThan(10000);
|
|
298
298
|
|
|
299
299
|
// Cleanup
|
|
300
300
|
disposers.forEach((d) => d());
|
|
@@ -334,7 +334,7 @@ describe("Performance", () => {
|
|
|
334
334
|
const elapsed = performance.now() - start;
|
|
335
335
|
|
|
336
336
|
expect(patchCount).toBe(10000);
|
|
337
|
-
expect(elapsed).toBeLessThan(
|
|
337
|
+
expect(elapsed).toBeLessThan(10000);
|
|
338
338
|
|
|
339
339
|
disposers.forEach((d) => d());
|
|
340
340
|
destroy(instance);
|
|
@@ -369,7 +369,7 @@ describe("Performance", () => {
|
|
|
369
369
|
const elapsed = performance.now() - start;
|
|
370
370
|
|
|
371
371
|
expect(clones.length).toBe(10);
|
|
372
|
-
expect(elapsed).toBeLessThan(
|
|
372
|
+
expect(elapsed).toBeLessThan(15000);
|
|
373
373
|
|
|
374
374
|
// Cleanup
|
|
375
375
|
destroy(original);
|
|
@@ -633,8 +633,8 @@ describe("Stress Tests", () => {
|
|
|
633
633
|
const elapsed = performance.now() - start;
|
|
634
634
|
|
|
635
635
|
expect(lastResult).toBe("Alice");
|
|
636
|
-
// Reference resolution should be extremely fast (less than 1 second for 50k calls)
|
|
637
|
-
expect(elapsed).toBeLessThan(
|
|
636
|
+
// Reference resolution should be extremely fast (less than 1 second for 50k calls, relaxed for CI)
|
|
637
|
+
expect(elapsed).toBeLessThan(5000);
|
|
638
638
|
|
|
639
639
|
destroy(user);
|
|
640
640
|
destroy(post);
|
|
@@ -678,8 +678,8 @@ describe("Stress Tests", () => {
|
|
|
678
678
|
const elapsed = performance.now() - start;
|
|
679
679
|
|
|
680
680
|
expect(matchCount).toBe(10000);
|
|
681
|
-
// Resolving 10,000 distinct references should be fast (less than 1.5 seconds)
|
|
682
|
-
expect(elapsed).toBeLessThan(
|
|
681
|
+
// Resolving 10,000 distinct references should be fast (less than 1.5 seconds, relaxed for CI)
|
|
682
|
+
expect(elapsed).toBeLessThan(8000);
|
|
683
683
|
|
|
684
684
|
// Cleanup
|
|
685
685
|
posts.forEach((p) => destroy(p));
|
|
@@ -421,6 +421,8 @@ describe('State Router', () => {
|
|
|
421
421
|
popStateCallback(new PopStateEvent('popstate', { state: null }));
|
|
422
422
|
});
|
|
423
423
|
expect(router.pathname).toBe('/users/redirected');
|
|
424
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(undefined, '', '/users/redirected');
|
|
425
|
+
expect(window.history.pushState).not.toHaveBeenCalled();
|
|
424
426
|
|
|
425
427
|
// 2. PopState promise rejection (should revert URL)
|
|
426
428
|
vi.stubGlobal('location', { pathname: '/files/error', search: '', hash: '' });
|
|
@@ -533,6 +535,8 @@ describe('State Router', () => {
|
|
|
533
535
|
popStateCallback(new PopStateEvent('popstate', { state: null }));
|
|
534
536
|
});
|
|
535
537
|
expect(r4.pathname).toBe('/users/redirected');
|
|
538
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(undefined, '', '/users/redirected');
|
|
539
|
+
expect(window.history.pushState).not.toHaveBeenCalled();
|
|
536
540
|
|
|
537
541
|
// 5. popstate with async beforeNavigate resolving to false
|
|
538
542
|
const r5 = createRouter({
|
|
@@ -277,15 +277,98 @@ describe('Utility Types Extra', () => {
|
|
|
277
277
|
expect(OnlyPostProcessor.validate({ name: 'bob' }, []).valid).toBe(true);
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
-
it('router in node environment (
|
|
280
|
+
it('router in node environment (in-memory stack routing)', () => {
|
|
281
|
+
const routes = [
|
|
282
|
+
{ path: '/', name: 'home' },
|
|
283
|
+
{ path: '/about', name: 'about' },
|
|
284
|
+
{ path: '/users/:id', name: 'user-profile' },
|
|
285
|
+
{ path: '/contact', name: 'contact' },
|
|
286
|
+
{ path: '/help', name: 'help' },
|
|
287
|
+
];
|
|
281
288
|
const r = createRouter({
|
|
282
|
-
routes
|
|
289
|
+
routes,
|
|
290
|
+
initialUrl: '/',
|
|
283
291
|
});
|
|
292
|
+
|
|
293
|
+
expect(r.pathname).toBe('/');
|
|
294
|
+
expect((r as any)._historyStack).toEqual(['/']);
|
|
295
|
+
expect((r as any)._historyIndex).toBe(0);
|
|
296
|
+
|
|
297
|
+
// Push new path
|
|
298
|
+
r.syncLocation('/about', '', '', 'PUSH');
|
|
299
|
+
expect(r.pathname).toBe('/about');
|
|
300
|
+
expect((r as any)._historyStack).toEqual(['/', '/about']);
|
|
301
|
+
expect((r as any)._historyIndex).toBe(1);
|
|
302
|
+
|
|
303
|
+
// Push another path
|
|
304
|
+
r.syncLocation('/users/123', '', '', 'PUSH');
|
|
305
|
+
expect(r.pathname).toBe('/users/123');
|
|
306
|
+
expect(r.params).toEqual({ id: '123' });
|
|
307
|
+
expect((r as any)._historyStack).toEqual(['/', '/about', '/users/123']);
|
|
308
|
+
expect((r as any)._historyIndex).toBe(2);
|
|
309
|
+
|
|
310
|
+
// Go back
|
|
311
|
+
r.goBack();
|
|
312
|
+
expect(r.pathname).toBe('/about');
|
|
313
|
+
expect((r as any)._historyIndex).toBe(1);
|
|
314
|
+
|
|
315
|
+
// Go forward
|
|
316
|
+
r.goForward();
|
|
317
|
+
expect(r.pathname).toBe('/users/123');
|
|
318
|
+
expect((r as any)._historyIndex).toBe(2);
|
|
319
|
+
|
|
320
|
+
// Go back 2 steps
|
|
321
|
+
r.go(-2);
|
|
284
322
|
expect(r.pathname).toBe('/');
|
|
285
|
-
expect((
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
323
|
+
expect((r as any)._historyIndex).toBe(0);
|
|
324
|
+
|
|
325
|
+
// Push from middle (should truncate forward stack)
|
|
326
|
+
r.syncLocation('/contact', '', '', 'PUSH');
|
|
327
|
+
expect(r.pathname).toBe('/contact');
|
|
328
|
+
expect((r as any)._historyStack).toEqual(['/', '/contact']);
|
|
329
|
+
expect((r as any)._historyIndex).toBe(1);
|
|
330
|
+
|
|
331
|
+
// Replace current path
|
|
332
|
+
r.syncLocation('/help', '', '', 'REPLACE');
|
|
333
|
+
expect(r.pathname).toBe('/help');
|
|
334
|
+
expect((r as any)._historyStack).toEqual(['/', '/help']);
|
|
335
|
+
expect((r as any)._historyIndex).toBe(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('router in environment where window is defined but document is undefined (e.g., React Native remote debugger)', () => {
|
|
339
|
+
const originalWindow = (global as any).window;
|
|
340
|
+
try {
|
|
341
|
+
// Mock window without document (React Native remote debugger environment)
|
|
342
|
+
(global as any).window = {};
|
|
343
|
+
|
|
344
|
+
const routes = [
|
|
345
|
+
{ path: '/', name: 'home' },
|
|
346
|
+
{ path: '/about', name: 'about' },
|
|
347
|
+
];
|
|
348
|
+
const r = createRouter({
|
|
349
|
+
routes,
|
|
350
|
+
initialUrl: '/',
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
expect(r.pathname).toBe('/');
|
|
354
|
+
expect((r as any)._historyStack).toEqual(['/']);
|
|
355
|
+
|
|
356
|
+
// Should not throw or crash and behave as in-memory router
|
|
357
|
+
expect(() => r.syncLocation('/about', '', '', 'PUSH')).not.toThrow();
|
|
358
|
+
expect(r.pathname).toBe('/about');
|
|
359
|
+
expect((r as any)._historyStack).toEqual(['/', '/about']);
|
|
360
|
+
expect((r as any)._historyIndex).toBe(1);
|
|
361
|
+
|
|
362
|
+
expect(() => r.goBack()).not.toThrow();
|
|
363
|
+
expect(r.pathname).toBe('/');
|
|
364
|
+
expect((r as any)._historyIndex).toBe(0);
|
|
365
|
+
} finally {
|
|
366
|
+
if (originalWindow === undefined) {
|
|
367
|
+
delete (global as any).window;
|
|
368
|
+
} else {
|
|
369
|
+
(global as any).window = originalWindow;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
289
372
|
});
|
|
290
373
|
});
|
|
291
374
|
|
package/src/router.ts
CHANGED
|
@@ -7,6 +7,12 @@ import { optional, maybeNull } from "./utilities";
|
|
|
7
7
|
import { flow } from "./lifecycle";
|
|
8
8
|
import { getStateTreeNode, hasStateTreeNode, getGlobalStore } from "./tree";
|
|
9
9
|
|
|
10
|
+
const isBrowser =
|
|
11
|
+
typeof window !== "undefined" &&
|
|
12
|
+
typeof window.document !== "undefined" &&
|
|
13
|
+
typeof window.location !== "undefined" &&
|
|
14
|
+
typeof window.history !== "undefined";
|
|
15
|
+
|
|
10
16
|
// ============================================================================
|
|
11
17
|
// Route Definition Model
|
|
12
18
|
// ============================================================================
|
|
@@ -169,6 +175,8 @@ export const RouterModel = model("RouterModel", {
|
|
|
169
175
|
beforeNavigate: null as ((from: any, to: any) => boolean | string | Promise<boolean | string> | undefined) | null,
|
|
170
176
|
afterNavigate: null as ((to: any) => void) | null,
|
|
171
177
|
_popStateListener: null as ((event: PopStateEvent) => void) | null,
|
|
178
|
+
_historyStack: [] as string[],
|
|
179
|
+
_historyIndex: -1,
|
|
172
180
|
}))
|
|
173
181
|
.actions((self) => {
|
|
174
182
|
return {
|
|
@@ -194,13 +202,30 @@ export const RouterModel = model("RouterModel", {
|
|
|
194
202
|
self.query = parsed.query;
|
|
195
203
|
self.currentRouteName = matched ? matched.route.name : null;
|
|
196
204
|
|
|
197
|
-
if (
|
|
205
|
+
if (isBrowser) {
|
|
198
206
|
const fullPath = pathname + search + hash;
|
|
199
207
|
if (action === "PUSH") {
|
|
200
208
|
window.history.pushState(state, "", fullPath);
|
|
201
209
|
} else if (action === "REPLACE") {
|
|
202
210
|
window.history.replaceState(state, "", fullPath);
|
|
203
211
|
}
|
|
212
|
+
} else {
|
|
213
|
+
const fullPath = pathname + search + hash;
|
|
214
|
+
if (action === "PUSH") {
|
|
215
|
+
self._historyStack = self._historyStack.slice(0, self._historyIndex + 1);
|
|
216
|
+
self._historyStack.push(fullPath);
|
|
217
|
+
self._historyIndex = self._historyStack.length - 1;
|
|
218
|
+
} else if (action === "REPLACE") {
|
|
219
|
+
if (self._historyIndex === -1) {
|
|
220
|
+
self._historyStack = [fullPath];
|
|
221
|
+
self._historyIndex = 0;
|
|
222
|
+
} else {
|
|
223
|
+
self._historyStack[self._historyIndex] = fullPath;
|
|
224
|
+
}
|
|
225
|
+
} else if (action === "INITIAL") {
|
|
226
|
+
self._historyStack = [fullPath];
|
|
227
|
+
self._historyIndex = 0;
|
|
228
|
+
}
|
|
204
229
|
}
|
|
205
230
|
},
|
|
206
231
|
|
|
@@ -287,26 +312,37 @@ export const RouterModel = model("RouterModel", {
|
|
|
287
312
|
}),
|
|
288
313
|
|
|
289
314
|
go(delta: number) {
|
|
290
|
-
if (
|
|
315
|
+
if (isBrowser) {
|
|
291
316
|
window.history.go(delta);
|
|
317
|
+
} else {
|
|
318
|
+
const nextIndex = self._historyIndex + delta;
|
|
319
|
+
if (nextIndex >= 0 && nextIndex < self._historyStack.length) {
|
|
320
|
+
self._historyIndex = nextIndex;
|
|
321
|
+
const targetPath = self._historyStack[nextIndex];
|
|
322
|
+
(self as any).syncLocation(targetPath, "", "", "POP");
|
|
323
|
+
}
|
|
292
324
|
}
|
|
293
325
|
},
|
|
294
326
|
|
|
295
327
|
goBack() {
|
|
296
|
-
if (
|
|
328
|
+
if (isBrowser) {
|
|
297
329
|
window.history.back();
|
|
330
|
+
} else {
|
|
331
|
+
(self as any).go(-1);
|
|
298
332
|
}
|
|
299
333
|
},
|
|
300
334
|
|
|
301
335
|
goForward() {
|
|
302
|
-
if (
|
|
336
|
+
if (isBrowser) {
|
|
303
337
|
window.history.forward();
|
|
338
|
+
} else {
|
|
339
|
+
(self as any).go(1);
|
|
304
340
|
}
|
|
305
341
|
}
|
|
306
342
|
};
|
|
307
343
|
})
|
|
308
344
|
.afterCreate((self) => {
|
|
309
|
-
if (
|
|
345
|
+
if (isBrowser) {
|
|
310
346
|
const handlePopState = (event: PopStateEvent) => {
|
|
311
347
|
const parsed = parseUrl(window.location.pathname + window.location.search + window.location.hash);
|
|
312
348
|
const matched = matchRoutes(self.routes, parsed.pathname);
|
|
@@ -349,7 +385,7 @@ export const RouterModel = model("RouterModel", {
|
|
|
349
385
|
if (res === false) {
|
|
350
386
|
revert();
|
|
351
387
|
} else if (typeof res === "string") {
|
|
352
|
-
self.
|
|
388
|
+
self.replace(res);
|
|
353
389
|
} else {
|
|
354
390
|
proceed();
|
|
355
391
|
}
|
|
@@ -360,7 +396,7 @@ export const RouterModel = model("RouterModel", {
|
|
|
360
396
|
if (result === false) {
|
|
361
397
|
revert();
|
|
362
398
|
} else if (typeof result === "string") {
|
|
363
|
-
self.
|
|
399
|
+
self.replace(result);
|
|
364
400
|
} else {
|
|
365
401
|
proceed();
|
|
366
402
|
}
|
|
@@ -375,7 +411,7 @@ export const RouterModel = model("RouterModel", {
|
|
|
375
411
|
}
|
|
376
412
|
})
|
|
377
413
|
.beforeDestroy((self) => {
|
|
378
|
-
if (
|
|
414
|
+
if (isBrowser && self._popStateListener) {
|
|
379
415
|
window.removeEventListener("popstate", self._popStateListener);
|
|
380
416
|
self.setPopStateListener(null);
|
|
381
417
|
}
|
|
@@ -400,7 +436,7 @@ export function createRouter(config: {
|
|
|
400
436
|
initialPathname = parsed.pathname;
|
|
401
437
|
initialSearch = parsed.search;
|
|
402
438
|
initialHash = parsed.hash;
|
|
403
|
-
} else if (
|
|
439
|
+
} else if (isBrowser) {
|
|
404
440
|
initialPathname = window.location.pathname;
|
|
405
441
|
initialSearch = window.location.search;
|
|
406
442
|
initialHash = window.location.hash;
|