@jk2908/solas 0.5.3 → 0.5.4
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 +5 -0
- package/dist/internal/browser-router/history.d.ts +46 -0
- package/dist/internal/browser-router/history.js +131 -0
- package/dist/internal/browser-router/router.d.ts +4 -12
- package/dist/internal/browser-router/router.js +34 -8
- package/dist/internal/browser-router/shared.d.ts +21 -0
- package/dist/internal/browser-router/use-router.d.ts +1 -10
- package/dist/internal/env/rsc.d.ts +3 -2
- package/dist/internal/env/rsc.js +1 -0
- package/package.json +1 -1
- package/dist/runtimes/bun.d.ts +0 -13
- package/dist/runtimes/bun.js +0 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.4 - 2026-06-08
|
|
4
|
+
|
|
5
|
+
- Add a BrowserRouterHistory API to sync app back/forward navigation with browser navigation.
|
|
6
|
+
- Expose BrowserRouterContext.history for gated use in client components
|
|
7
|
+
|
|
3
8
|
## 0.5.3 - 2026-06-06
|
|
4
9
|
|
|
5
10
|
- Ensured `createRuntime(...)` is exported from `@jk2908/solas/env/rsc` for generated RSC entry compatibility.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BrowserRouter } from './shared.js';
|
|
2
|
+
/**
|
|
3
|
+
* A small in-memory mirror of browser history used by BrowserRouter.
|
|
4
|
+
* It keeps local entries/index in sync with push/replace/go and
|
|
5
|
+
* popstate updates
|
|
6
|
+
*/
|
|
7
|
+
export declare class BrowserRouterHistory {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(url: {
|
|
10
|
+
pathname: string;
|
|
11
|
+
search?: string;
|
|
12
|
+
hash?: string;
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Returns a copy of tracked entries
|
|
16
|
+
*/
|
|
17
|
+
get entries(): BrowserRouter.HistoryEntry[];
|
|
18
|
+
/**
|
|
19
|
+
* Returns the tracked history index
|
|
20
|
+
*/
|
|
21
|
+
get index(): number;
|
|
22
|
+
/**
|
|
23
|
+
* Returns the tracked history length
|
|
24
|
+
*/
|
|
25
|
+
get length(): number;
|
|
26
|
+
/**
|
|
27
|
+
* Returns the current tracked entry
|
|
28
|
+
*/
|
|
29
|
+
get current(): BrowserRouter.HistoryEntry;
|
|
30
|
+
/**
|
|
31
|
+
* Pushes a new history entry and advances the index
|
|
32
|
+
*/
|
|
33
|
+
pushState(path: string): BrowserRouter.HistoryEntry;
|
|
34
|
+
/**
|
|
35
|
+
* Replaces the current history entry without changing length
|
|
36
|
+
*/
|
|
37
|
+
replaceState(path: string): BrowserRouter.HistoryEntry;
|
|
38
|
+
/**
|
|
39
|
+
* Applies relative history movement and updates local index
|
|
40
|
+
*/
|
|
41
|
+
go(delta: number): BrowserRouter.HistoryEntry;
|
|
42
|
+
/**
|
|
43
|
+
* Syncs local index and entry from a popstate event
|
|
44
|
+
*/
|
|
45
|
+
onPopState(): BrowserRouter.HistoryEntry;
|
|
46
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small in-memory mirror of browser history used by BrowserRouter.
|
|
3
|
+
* It keeps local entries/index in sync with push/replace/go and
|
|
4
|
+
* popstate updates
|
|
5
|
+
*/
|
|
6
|
+
export class BrowserRouterHistory {
|
|
7
|
+
#entries;
|
|
8
|
+
#index;
|
|
9
|
+
constructor(url) {
|
|
10
|
+
const entry = {
|
|
11
|
+
pathname: url.pathname,
|
|
12
|
+
search: url.search ?? '',
|
|
13
|
+
hash: url.hash ?? '',
|
|
14
|
+
};
|
|
15
|
+
this.#entries = [entry];
|
|
16
|
+
this.#index = 0;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns a copy of tracked entries
|
|
20
|
+
*/
|
|
21
|
+
get entries() {
|
|
22
|
+
return [...this.#entries];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns the tracked history index
|
|
26
|
+
*/
|
|
27
|
+
get index() {
|
|
28
|
+
return this.#index;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns the tracked history length
|
|
32
|
+
*/
|
|
33
|
+
get length() {
|
|
34
|
+
return this.#entries.length;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the current tracked entry
|
|
38
|
+
*/
|
|
39
|
+
get current() {
|
|
40
|
+
return this.#entries[this.#index] ?? null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pushes a new history entry and advances the index
|
|
44
|
+
*/
|
|
45
|
+
pushState(path) {
|
|
46
|
+
if (this.#index < this.#entries.length - 1) {
|
|
47
|
+
// drop forward entries when pushing from the middle
|
|
48
|
+
this.#entries = this.#entries.slice(0, this.#index + 1);
|
|
49
|
+
}
|
|
50
|
+
window.history.pushState(null, '', path);
|
|
51
|
+
const entry = {
|
|
52
|
+
pathname: window.location.pathname,
|
|
53
|
+
search: window.location.search,
|
|
54
|
+
hash: window.location.hash,
|
|
55
|
+
};
|
|
56
|
+
this.#entries.push(entry);
|
|
57
|
+
this.#index++;
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Replaces the current history entry without changing length
|
|
62
|
+
*/
|
|
63
|
+
replaceState(path) {
|
|
64
|
+
window.history.replaceState(null, '', path);
|
|
65
|
+
const entry = {
|
|
66
|
+
pathname: window.location.pathname,
|
|
67
|
+
search: window.location.search,
|
|
68
|
+
hash: window.location.hash,
|
|
69
|
+
};
|
|
70
|
+
if (this.#entries.length === 0) {
|
|
71
|
+
this.#entries = [entry];
|
|
72
|
+
this.#index = 0;
|
|
73
|
+
return entry;
|
|
74
|
+
}
|
|
75
|
+
this.#entries[this.#index] = entry;
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Applies relative history movement and updates local index
|
|
80
|
+
*/
|
|
81
|
+
go(delta) {
|
|
82
|
+
if (!Number.isInteger(delta) || delta === 0) {
|
|
83
|
+
return this.current;
|
|
84
|
+
}
|
|
85
|
+
const nextIndex = Math.max(0, Math.min(this.#index + delta, this.#entries.length - 1));
|
|
86
|
+
if (nextIndex !== this.#index)
|
|
87
|
+
this.#index = nextIndex;
|
|
88
|
+
window.history.go(delta);
|
|
89
|
+
return this.current;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Syncs local index and entry from a popstate event
|
|
93
|
+
*/
|
|
94
|
+
onPopState() {
|
|
95
|
+
const current = {
|
|
96
|
+
pathname: window.location.pathname,
|
|
97
|
+
search: window.location.search,
|
|
98
|
+
hash: window.location.hash,
|
|
99
|
+
};
|
|
100
|
+
const prev = this.#entries[this.#index - 1];
|
|
101
|
+
const next = this.#entries[this.#index + 1];
|
|
102
|
+
if (prev &&
|
|
103
|
+
prev.pathname === current.pathname &&
|
|
104
|
+
prev.search === current.search &&
|
|
105
|
+
prev.hash === current.hash) {
|
|
106
|
+
this.#index--;
|
|
107
|
+
this.#entries[this.#index] = current;
|
|
108
|
+
return current;
|
|
109
|
+
}
|
|
110
|
+
if (next &&
|
|
111
|
+
next.pathname === current.pathname &&
|
|
112
|
+
next.search === current.search &&
|
|
113
|
+
next.hash === current.hash) {
|
|
114
|
+
this.#index++;
|
|
115
|
+
this.#entries[this.#index] = current;
|
|
116
|
+
return current;
|
|
117
|
+
}
|
|
118
|
+
// fallback: locate an existing matching entry
|
|
119
|
+
const index = this.#entries.findIndex(entry => entry.pathname === current.pathname &&
|
|
120
|
+
entry.search === current.search &&
|
|
121
|
+
entry.hash === current.hash);
|
|
122
|
+
if (index >= 0) {
|
|
123
|
+
this.#index = index;
|
|
124
|
+
this.#entries[index] = current;
|
|
125
|
+
return current;
|
|
126
|
+
}
|
|
127
|
+
this.#entries.push(current);
|
|
128
|
+
this.#index = this.#entries.length - 1;
|
|
129
|
+
return current;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import type { RscPayload } from '../env/rsc.js';
|
|
2
2
|
import { BrowserRouter } from './shared.js';
|
|
3
3
|
export { BrowserRouter } from './shared.js';
|
|
4
|
-
export declare const BrowserRouterContext: import("react").Context<
|
|
5
|
-
go: BrowserRouter.Go;
|
|
6
|
-
prefetch: (path: string) => void;
|
|
7
|
-
refresh: BrowserRouter.Refresh;
|
|
8
|
-
isNavigating: boolean;
|
|
9
|
-
url: {
|
|
10
|
-
pathname?: string | undefined;
|
|
11
|
-
search?: string | undefined;
|
|
12
|
-
};
|
|
13
|
-
}>;
|
|
4
|
+
export declare const BrowserRouterContext: import("react").Context<BrowserRouter.Context>;
|
|
14
5
|
export declare function BrowserRouterProvider({ children, setPayload, isNavigating, url }: {
|
|
15
6
|
children: React.ReactNode;
|
|
16
7
|
setPayload?: (payload: RscPayload) => void;
|
|
17
8
|
isNavigating?: boolean;
|
|
18
|
-
url
|
|
19
|
-
pathname
|
|
9
|
+
url: {
|
|
10
|
+
pathname: string;
|
|
20
11
|
search?: string;
|
|
12
|
+
hash?: string;
|
|
21
13
|
};
|
|
22
14
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,6 +4,7 @@ import { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
|
4
4
|
import { createFromFetch } from '@vitejs/plugin-rsc/browser';
|
|
5
5
|
import { Logger } from '../../utils/logger.js';
|
|
6
6
|
import { Solas } from '../../solas.js';
|
|
7
|
+
import { BrowserRouterHistory } from './history.js';
|
|
7
8
|
import { ResponseCache } from './response-cache.js';
|
|
8
9
|
import { BrowserRouter } from './shared.js';
|
|
9
10
|
export { BrowserRouter } from './shared.js';
|
|
@@ -12,7 +13,15 @@ export const BrowserRouterContext = createContext({
|
|
|
12
13
|
prefetch: () => { },
|
|
13
14
|
refresh: async () => '',
|
|
14
15
|
isNavigating: false,
|
|
15
|
-
url: {
|
|
16
|
+
url: {
|
|
17
|
+
pathname: '',
|
|
18
|
+
search: '',
|
|
19
|
+
hash: '',
|
|
20
|
+
},
|
|
21
|
+
history: {
|
|
22
|
+
entries: [],
|
|
23
|
+
index: -1,
|
|
24
|
+
},
|
|
16
25
|
});
|
|
17
26
|
const DEFAULT_GO_CONFIG = {
|
|
18
27
|
replace: false,
|
|
@@ -22,6 +31,7 @@ const responseCache = new ResponseCache();
|
|
|
22
31
|
export function BrowserRouterProvider({ children, setPayload, isNavigating = false, url, }) {
|
|
23
32
|
const id = useRef(0);
|
|
24
33
|
const controller = useRef(null);
|
|
34
|
+
const history = useRef(new BrowserRouterHistory(url));
|
|
25
35
|
/**
|
|
26
36
|
* Navigates to a given path
|
|
27
37
|
*
|
|
@@ -35,6 +45,13 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
|
|
|
35
45
|
const navigationId = id.current;
|
|
36
46
|
const currentPath = window.location.pathname + window.location.search;
|
|
37
47
|
let path = currentPath;
|
|
48
|
+
// support numeric relative navigation like router.go(-1) to go back,
|
|
49
|
+
// or router.go(1) to go forward. Will trigger a popstate event which
|
|
50
|
+
// is intercepted
|
|
51
|
+
if (typeof to === 'number') {
|
|
52
|
+
const entry = history.current.go(to);
|
|
53
|
+
return `${entry.pathname}${entry.search}`;
|
|
54
|
+
}
|
|
38
55
|
const replace = opts?.replace ?? DEFAULT_GO_CONFIG.replace;
|
|
39
56
|
controller.current?.abort();
|
|
40
57
|
controller.current = null;
|
|
@@ -51,10 +68,10 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
|
|
|
51
68
|
path = key;
|
|
52
69
|
if (path !== currentPath) {
|
|
53
70
|
if (replace) {
|
|
54
|
-
|
|
71
|
+
history.current.replaceState(path);
|
|
55
72
|
}
|
|
56
73
|
else {
|
|
57
|
-
|
|
74
|
+
history.current.pushState(path);
|
|
58
75
|
}
|
|
59
76
|
}
|
|
60
77
|
let promise = responseCache.get(path);
|
|
@@ -78,7 +95,7 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
|
|
|
78
95
|
if (navigationId !== id.current)
|
|
79
96
|
return resolvedPath;
|
|
80
97
|
if (resolvedPath !== path) {
|
|
81
|
-
|
|
98
|
+
history.current.replaceState(resolvedPath);
|
|
82
99
|
}
|
|
83
100
|
setPayload?.(payload);
|
|
84
101
|
window.dispatchEvent(new CustomEvent(Solas.Events.names.NAVIGATION, {
|
|
@@ -138,9 +155,13 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
|
|
|
138
155
|
});
|
|
139
156
|
}, [go]);
|
|
140
157
|
useEffect(() => {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
const h = history.current;
|
|
159
|
+
function handler() {
|
|
160
|
+
h.onPopState();
|
|
161
|
+
void go(BrowserRouter.toTarget(window.location.pathname + window.location.search), {
|
|
162
|
+
replace: true,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
144
165
|
window.addEventListener('popstate', handler);
|
|
145
166
|
return () => {
|
|
146
167
|
controller.current?.abort();
|
|
@@ -154,8 +175,13 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
|
|
|
154
175
|
refresh,
|
|
155
176
|
isNavigating,
|
|
156
177
|
url: {
|
|
157
|
-
pathname: url
|
|
178
|
+
pathname: url.pathname,
|
|
158
179
|
search: url?.search,
|
|
180
|
+
hash: url?.hash,
|
|
181
|
+
},
|
|
182
|
+
history: {
|
|
183
|
+
entries: history.current.entries,
|
|
184
|
+
index: history.current.index,
|
|
159
185
|
},
|
|
160
186
|
}), [go, prefetch, refresh, isNavigating, url]);
|
|
161
187
|
return _jsx(BrowserRouterContext, { value: value, children: children });
|
|
@@ -11,6 +11,26 @@ export declare namespace BrowserRouter {
|
|
|
11
11
|
query?: Query;
|
|
12
12
|
params?: Params;
|
|
13
13
|
};
|
|
14
|
+
export type Context = {
|
|
15
|
+
go: BrowserRouter.Go;
|
|
16
|
+
prefetch: (path: string) => void;
|
|
17
|
+
refresh: BrowserRouter.Refresh;
|
|
18
|
+
isNavigating: boolean;
|
|
19
|
+
url: {
|
|
20
|
+
pathname: string;
|
|
21
|
+
search?: string;
|
|
22
|
+
hash?: string;
|
|
23
|
+
};
|
|
24
|
+
history: {
|
|
25
|
+
entries: HistoryEntry[];
|
|
26
|
+
index: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type HistoryEntry = {
|
|
30
|
+
pathname: string;
|
|
31
|
+
search: string;
|
|
32
|
+
hash: string;
|
|
33
|
+
};
|
|
14
34
|
/**
|
|
15
35
|
* These targets are used as-is. They are not matched against the route table,
|
|
16
36
|
* so this covers normal external URLs and hash-only links
|
|
@@ -152,6 +172,7 @@ export declare namespace BrowserRouter {
|
|
|
152
172
|
* ```
|
|
153
173
|
*/
|
|
154
174
|
export type Go = {
|
|
175
|
+
(to: number): Promise<string>;
|
|
155
176
|
<TTo extends Path>(to: TTo, opts?: PatternConfig<TTo> & Replace): Promise<string>;
|
|
156
177
|
<TTo extends Target>(to: TTo, opts?: TargetConfig & Replace): Promise<string>;
|
|
157
178
|
<TTo extends string>(to: string extends TTo ? TTo : never, opts?: GoOptions): Promise<string>;
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export declare function useRouter():
|
|
2
|
-
go: import("./shared.js").BrowserRouter.Go;
|
|
3
|
-
prefetch: (path: string) => void;
|
|
4
|
-
refresh: import("./shared.js").BrowserRouter.Refresh;
|
|
5
|
-
isNavigating: boolean;
|
|
6
|
-
url: {
|
|
7
|
-
pathname?: string | undefined;
|
|
8
|
-
search?: string | undefined;
|
|
9
|
-
};
|
|
10
|
-
};
|
|
1
|
+
export declare function useRouter(): import("./shared.js").BrowserRouter.Context;
|
package/dist/internal/env/rsc.js
CHANGED
package/package.json
CHANGED
package/dist/runtimes/bun.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { RuntimeBase } from '../internal/runtimes/runtime.js';
|
|
2
|
-
declare class BunAdapter extends RuntimeBase {
|
|
3
|
-
readonly name: "bun";
|
|
4
|
-
readonly module: string;
|
|
5
|
-
exists(filePath: string): Promise<boolean>;
|
|
6
|
-
readText(filePath: string): Promise<string>;
|
|
7
|
-
readBuffer(filePath: string): Promise<ArrayBuffer>;
|
|
8
|
-
mimeType(filePath: string): string;
|
|
9
|
-
write(filePath: string, content: string | NodeJS.ArrayBufferView): Promise<void>;
|
|
10
|
-
hash(value: string): string;
|
|
11
|
-
}
|
|
12
|
-
export default function bunAdapter(): BunAdapter;
|
|
13
|
-
export {};
|
package/dist/runtimes/bun.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { RuntimeBase } from '../internal/runtimes/runtime.js';
|
|
2
|
-
import { Solas } from '../solas.js';
|
|
3
|
-
class BunAdapter extends RuntimeBase {
|
|
4
|
-
name = 'bun';
|
|
5
|
-
module = `${Solas.Config.PKG_NAME}/runtimes/bun`;
|
|
6
|
-
async exists(filePath) {
|
|
7
|
-
return Bun.file(filePath).exists();
|
|
8
|
-
}
|
|
9
|
-
readText(filePath) {
|
|
10
|
-
return Bun.file(filePath).text();
|
|
11
|
-
}
|
|
12
|
-
readBuffer(filePath) {
|
|
13
|
-
return Bun.file(filePath).arrayBuffer();
|
|
14
|
-
}
|
|
15
|
-
mimeType(filePath) {
|
|
16
|
-
return Bun.file(filePath).type || 'application/octet-stream';
|
|
17
|
-
}
|
|
18
|
-
async write(filePath, content) {
|
|
19
|
-
// normalise wider arraybuffer views into a shape Bun.write accepts directly
|
|
20
|
-
await Bun.write(filePath, typeof content === 'string'
|
|
21
|
-
? content
|
|
22
|
-
: content instanceof Uint8Array
|
|
23
|
-
? content
|
|
24
|
-
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength));
|
|
25
|
-
}
|
|
26
|
-
hash(value) {
|
|
27
|
-
const hash = Bun.hash(value);
|
|
28
|
-
// Bun.hash returns an integer-like value, so keep it in BigInt space and avoid
|
|
29
|
-
// precision loss. Clamp it to an unsigned 64-bit value before hex
|
|
30
|
-
// formatting. Pad to 16 chars to match the Node adapter
|
|
31
|
-
// output shape
|
|
32
|
-
return BigInt.asUintN(64, typeof hash === 'bigint' ? hash : BigInt(hash))
|
|
33
|
-
.toString(16)
|
|
34
|
-
.padStart(16, '0');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
export default function bunAdapter() {
|
|
38
|
-
return new BunAdapter();
|
|
39
|
-
}
|