@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.
@@ -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
+ }