@reactra/router 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/index.d.ts +508 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1049 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +162 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +158 -0
- package/dist/middleware.js.map +1 -0
- package/dist/portal.d.ts +35 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/portal.js +49 -0
- package/dist/portal.js.map +1 -0
- package/dist/scrollManager.d.ts +69 -0
- package/dist/scrollManager.d.ts.map +1 -0
- package/dist/scrollManager.js +189 -0
- package/dist/scrollManager.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// @reactra/router — scroll restoration (Wave 3, §2b).
|
|
2
|
+
//
|
|
3
|
+
// Owner spec: reactra-router-spec.md §3.5 (per-page modes) + §13.1
|
|
4
|
+
// (RouterConfig `scrollRestoration` block).
|
|
5
|
+
//
|
|
6
|
+
// Scope of this module: the **app-wide default mode** (`auto` / `top` /
|
|
7
|
+
// `manual`) + storage management. Per-page `meta { scrollRestoration:
|
|
8
|
+
// "manual" }` overrides need the meta classifier (phase-2.md §2a — still
|
|
9
|
+
// open), so this stage ships the global default. When the classifier
|
|
10
|
+
// lands, the navigate-integration call site reads the per-route override
|
|
11
|
+
// from the matched route's resolved meta and passes that mode in here
|
|
12
|
+
// instead of `cfg.defaultMode`.
|
|
13
|
+
//
|
|
14
|
+
// Storage shape:
|
|
15
|
+
// sessionStorage[cfg.scrollKey] = JSON { [historyKey]: { x, y } }
|
|
16
|
+
//
|
|
17
|
+
// `historyKey` is a stable id per history entry — assigned to
|
|
18
|
+
// `history.state.key` on push, persisted across the back/forward stack,
|
|
19
|
+
// and read back on `popstate`. This is the same approach Remix /
|
|
20
|
+
// Next.js use, with the same trade-off: a stray `history.replaceState`
|
|
21
|
+
// that overwrites our key without preserving it loses scroll for that
|
|
22
|
+
// entry — we treat that as the user's choice (e.g. an analytics lib
|
|
23
|
+
// rewriting state).
|
|
24
|
+
//
|
|
25
|
+
// Mode behaviour (Router §3.5 table):
|
|
26
|
+
//
|
|
27
|
+
// auto | push: scroll to top | pop: restore saved (top if none)
|
|
28
|
+
// top | push: scroll to top | pop: scroll to top
|
|
29
|
+
// manual | push: no scroll | pop: no scroll (app controls)
|
|
30
|
+
//
|
|
31
|
+
// In test / Node environments where `window` / `sessionStorage` are
|
|
32
|
+
// undefined, every method is a no-op so the same code runs cleanly
|
|
33
|
+
// under `bun test`.
|
|
34
|
+
// ─── Storage helpers ──────────────────────────────────────────────────────
|
|
35
|
+
const hasSessionStorage = () => typeof globalThis !== "undefined" &&
|
|
36
|
+
typeof globalThis.sessionStorage !== "undefined";
|
|
37
|
+
const readMap = (storageKey) => {
|
|
38
|
+
if (!hasSessionStorage())
|
|
39
|
+
return {};
|
|
40
|
+
try {
|
|
41
|
+
const raw = sessionStorage.getItem(storageKey);
|
|
42
|
+
if (raw === null)
|
|
43
|
+
return {};
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
// Defensive: a foreign actor may have written non-object data.
|
|
46
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const writeMap = (storageKey, map) => {
|
|
53
|
+
if (!hasSessionStorage())
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
sessionStorage.setItem(storageKey, JSON.stringify(map));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Quota exceeded / disabled storage — silently swallow; scroll
|
|
60
|
+
// restoration is a polish feature, not a correctness invariant.
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
// ─── Key generation ───────────────────────────────────────────────────────
|
|
64
|
+
let counter = 0;
|
|
65
|
+
/**
|
|
66
|
+
* Generate a fresh history-entry key. `crypto.randomUUID` when available
|
|
67
|
+
* (browser, modern Node); falls back to a monotone counter so tests in a
|
|
68
|
+
* minimal environment still get unique keys. The format isn't load-bearing
|
|
69
|
+
* — the navigate-integration site only uses it as an opaque sessionStorage
|
|
70
|
+
* key.
|
|
71
|
+
*/
|
|
72
|
+
export const generateHistoryKey = () => {
|
|
73
|
+
if (typeof globalThis !== "undefined" &&
|
|
74
|
+
typeof globalThis.crypto?.randomUUID === "function") {
|
|
75
|
+
return globalThis.crypto.randomUUID();
|
|
76
|
+
}
|
|
77
|
+
counter += 1;
|
|
78
|
+
return `key-${counter}`;
|
|
79
|
+
};
|
|
80
|
+
export const createScrollManager = (cfg) => {
|
|
81
|
+
const scrollWindowTo = (x, y) => {
|
|
82
|
+
if (typeof window === "undefined")
|
|
83
|
+
return;
|
|
84
|
+
if (typeof window.scrollTo === "function")
|
|
85
|
+
window.scrollTo(x, y);
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
saveCurrent(historyKey) {
|
|
89
|
+
if (typeof window === "undefined")
|
|
90
|
+
return;
|
|
91
|
+
const map = readMap(cfg.scrollKey);
|
|
92
|
+
map[historyKey] = {
|
|
93
|
+
x: window.scrollX ?? 0,
|
|
94
|
+
y: window.scrollY ?? 0,
|
|
95
|
+
};
|
|
96
|
+
writeMap(cfg.scrollKey, map);
|
|
97
|
+
},
|
|
98
|
+
restoreFor(historyKey) {
|
|
99
|
+
const map = readMap(cfg.scrollKey);
|
|
100
|
+
const saved = map[historyKey];
|
|
101
|
+
if (!saved)
|
|
102
|
+
return false;
|
|
103
|
+
scrollWindowTo(saved.x, saved.y);
|
|
104
|
+
return true;
|
|
105
|
+
},
|
|
106
|
+
scrollToTop() {
|
|
107
|
+
scrollWindowTo(0, 0);
|
|
108
|
+
},
|
|
109
|
+
applyForNavigation(historyKey, navKind, modeOverride) {
|
|
110
|
+
const mode = modeOverride ?? cfg.defaultMode;
|
|
111
|
+
if (mode === "manual")
|
|
112
|
+
return;
|
|
113
|
+
if (mode === "top") {
|
|
114
|
+
this.scrollToTop();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// mode === "auto"
|
|
118
|
+
if (navKind === "pop") {
|
|
119
|
+
const restored = historyKey !== null && this.restoreFor(historyKey);
|
|
120
|
+
if (!restored)
|
|
121
|
+
this.scrollToTop();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// push / replace → top
|
|
125
|
+
this.scrollToTop();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
// ─── History-state key helpers ────────────────────────────────────────────
|
|
130
|
+
const HISTORY_KEY_FIELD = "__reactraScrollKey";
|
|
131
|
+
/**
|
|
132
|
+
* Read the key on the current history entry, or null if absent (e.g.
|
|
133
|
+
* an entry created before scroll restoration was wired in, or one
|
|
134
|
+
* overwritten by a foreign `replaceState`). The caller's mode handler
|
|
135
|
+
* treats `null` as "no saved scroll" — auto mode then falls back to
|
|
136
|
+
* scroll-to-top.
|
|
137
|
+
*/
|
|
138
|
+
export const readHistoryKey = () => {
|
|
139
|
+
if (typeof window === "undefined")
|
|
140
|
+
return null;
|
|
141
|
+
const state = window.history.state;
|
|
142
|
+
const v = state?.[HISTORY_KEY_FIELD];
|
|
143
|
+
return typeof v === "string" ? v : null;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Ensure the current history entry carries a key. If it already does,
|
|
147
|
+
* returns the existing key. Otherwise generates one + `replaceState`s
|
|
148
|
+
* the entry to write it. Returns the key in use.
|
|
149
|
+
*/
|
|
150
|
+
export const ensureHistoryKey = () => {
|
|
151
|
+
if (typeof window === "undefined")
|
|
152
|
+
return generateHistoryKey();
|
|
153
|
+
const existing = readHistoryKey();
|
|
154
|
+
if (existing !== null)
|
|
155
|
+
return existing;
|
|
156
|
+
const key = generateHistoryKey();
|
|
157
|
+
const state = window.history.state ?? {};
|
|
158
|
+
window.history.replaceState({ ...state, [HISTORY_KEY_FIELD]: key }, "", window.location.href);
|
|
159
|
+
return key;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Push a new history entry with a fresh key embedded. Use this from
|
|
163
|
+
* `navigate()` in place of bare `pushState`.
|
|
164
|
+
*/
|
|
165
|
+
export const pushStateWithKey = (extraState, url) => {
|
|
166
|
+
const key = generateHistoryKey();
|
|
167
|
+
if (typeof window === "undefined")
|
|
168
|
+
return key;
|
|
169
|
+
window.history.pushState({ ...extraState, [HISTORY_KEY_FIELD]: key }, "", url);
|
|
170
|
+
return key;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Replace the current history entry with a fresh state object, embedding
|
|
174
|
+
* the SAME key as the entry being replaced. If the entry has no key yet,
|
|
175
|
+
* a fresh one is generated. Returns the key in use.
|
|
176
|
+
*/
|
|
177
|
+
export const replaceStateWithKey = (extraState, url) => {
|
|
178
|
+
if (typeof window === "undefined")
|
|
179
|
+
return generateHistoryKey();
|
|
180
|
+
const existing = readHistoryKey();
|
|
181
|
+
const key = existing ?? generateHistoryKey();
|
|
182
|
+
window.history.replaceState({ ...extraState, [HISTORY_KEY_FIELD]: key }, "", url);
|
|
183
|
+
return key;
|
|
184
|
+
};
|
|
185
|
+
// Test-only re-exports.
|
|
186
|
+
export const __readMap = readMap;
|
|
187
|
+
export const __writeMap = writeMap;
|
|
188
|
+
export const __HISTORY_KEY_FIELD = HISTORY_KEY_FIELD;
|
|
189
|
+
//# sourceMappingURL=scrollManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrollManager.js","sourceRoot":"","sources":["../src/scrollManager.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reactra/router",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"react": "^19.2.0",
|
|
14
|
+
"react-dom": "^19.2.0"
|
|
15
|
+
},
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"tag": "alpha",
|
|
23
|
+
"provenance": false
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/akhilshastri/reactra.git",
|
|
28
|
+
"directory": "packages/router"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://reactra-docs.vercel.app",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.18"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"description": "Reactra router runtime — file-based routing, params/query, navigation, and middleware."
|
|
37
|
+
}
|