@tschk/moonshine-next 0.4.1 → 0.4.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/README.md +14 -2
- package/package.json +13 -4
- package/src/headers.ts +12 -10
- package/src/link.ts +3 -2
- package/src/navigation.ts +20 -15
package/README.md
CHANGED
|
@@ -96,8 +96,15 @@ Behavioural notes where moonshine deliberately differs from Next:
|
|
|
96
96
|
moonshine only scrolls when the pathname or hash actually changes, so
|
|
97
97
|
query-only navigation (filters, pagination, tabs) keeps your scroll position.
|
|
98
98
|
Pass `scroll` explicitly to force either behaviour.
|
|
99
|
-
- **`router.refresh()`
|
|
100
|
-
|
|
99
|
+
- **`router.refresh()` reloads the browser document.** This requests fresh
|
|
100
|
+
server output and resets local component state. State-preserving RSC refresh
|
|
101
|
+
is unsupported. It is safe to call during SSR, where it does nothing.
|
|
102
|
+
- **`router.prefetch()` is a no-op.** Use `Link` for browser prefetch hints.
|
|
103
|
+
- **Browser navigation stays with the browser.** Links preserve external URLs,
|
|
104
|
+
non-self targets, fragments and downloads without route prefetch. External
|
|
105
|
+
`router.push` uses document assignment; external `replace` replaces the history
|
|
106
|
+
entry. Same-origin absolute URLs retain SPA navigation. The existing safe-href
|
|
107
|
+
policy prevents executable schemes from reaching document navigation.
|
|
101
108
|
- **`redirect()` / `notFound()`** throw `RedirectError` / `NotFoundError`,
|
|
102
109
|
exported from `./navigation` so your renderer can catch them.
|
|
103
110
|
- **`useSearchParams()`** reads the live query string. The moonshine router
|
|
@@ -137,3 +144,8 @@ Does **not** export `MoonshineRouter` from the client entry — import it from
|
|
|
137
144
|
`@tschk/moonshine/router` if you want the moonshine router directly.
|
|
138
145
|
|
|
139
146
|
See [Moonshine README](../../README.md).
|
|
147
|
+
|
|
148
|
+
Request helpers require `requestContext: true` in Moonshine config (enabled by
|
|
149
|
+
Next adoption). Bun/Node deployment and preview inject the runner from the same
|
|
150
|
+
compiled bundle as the route modules; a separately imported context cannot scope
|
|
151
|
+
an already-bundled route. Edge request context is currently unsupported.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tschk/moonshine-next",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Next.js API surface reimplemented on moonshine — drop Next, keep the imports",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"type": "module",
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
"./shaders": "./src/shaders.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@tschk/moonshine": "^0.4.
|
|
25
|
-
"@tschk/moonshine-router": "^0.4.
|
|
26
|
-
"@tschk/moonshine-shaders": "^0.4.
|
|
24
|
+
"@tschk/moonshine": "^0.4.4",
|
|
25
|
+
"@tschk/moonshine-router": "^0.4.4",
|
|
26
|
+
"@tschk/moonshine-shaders": "^0.4.4",
|
|
27
|
+
"@tschk/moonshine-framework": "^0.4.4"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
30
|
"react": ">=18",
|
|
@@ -43,5 +44,13 @@
|
|
|
43
44
|
"lint": "bunx oxlint .",
|
|
44
45
|
"typecheck": "bunx tsc --noEmit",
|
|
45
46
|
"test": "bun test"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"react-dom": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
56
|
}
|
package/src/headers.ts
CHANGED
|
@@ -8,17 +8,11 @@
|
|
|
8
8
|
* import { headers, cookies } from "@tschk/moonshine-next/headers";
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
const store = new AsyncLocalStorage<Request>();
|
|
14
|
-
|
|
15
|
-
/** Runs `fn` with `request` visible to `headers()` and `cookies()`. */
|
|
16
|
-
export function runWithRequest<T>(request: Request, fn: () => T): T {
|
|
17
|
-
return store.run(request, fn);
|
|
18
|
-
}
|
|
11
|
+
import { getRequest } from "@tschk/moonshine-framework/request";
|
|
12
|
+
export { runWithRequest } from "@tschk/moonshine-framework/request";
|
|
19
13
|
|
|
20
14
|
function current(): Request {
|
|
21
|
-
const request =
|
|
15
|
+
const request = getRequest();
|
|
22
16
|
if (!request) {
|
|
23
17
|
throw new Error(
|
|
24
18
|
"headers() was called outside a request. Wrap the render in runWithRequest().",
|
|
@@ -40,7 +34,15 @@ function parseCookies(header: string | null): Map<string, string> {
|
|
|
40
34
|
const index = part.indexOf("=");
|
|
41
35
|
if (index === -1) continue;
|
|
42
36
|
const name = part.slice(0, index).trim();
|
|
43
|
-
if (name)
|
|
37
|
+
if (!name) continue;
|
|
38
|
+
const raw = part.slice(index + 1).trim();
|
|
39
|
+
let value = raw;
|
|
40
|
+
try {
|
|
41
|
+
value = decodeURIComponent(raw);
|
|
42
|
+
} catch {
|
|
43
|
+
// Cookie values may contain literal percent signs or invalid URI sequences.
|
|
44
|
+
}
|
|
45
|
+
jar.set(name, value);
|
|
44
46
|
}
|
|
45
47
|
return jar;
|
|
46
48
|
}
|
package/src/link.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type MouseEvent,
|
|
18
18
|
type ReactNode,
|
|
19
19
|
} from "react";
|
|
20
|
-
import { isExternal,
|
|
20
|
+
import { isExternal, useNavigate } from "@tschk/moonshine/router";
|
|
21
21
|
|
|
22
22
|
export type LinkProps = Omit<
|
|
23
23
|
AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
@@ -85,8 +85,9 @@ export default function Link({
|
|
|
85
85
|
onMouseEnter,
|
|
86
86
|
...rest
|
|
87
87
|
}: LinkProps): ReactNode {
|
|
88
|
+
const navigate = useNavigate();
|
|
88
89
|
const url = hrefToString(href);
|
|
89
|
-
const external = isExternal(url, rest.target);
|
|
90
|
+
const external = isExternal(url, rest.target, rest.download);
|
|
90
91
|
const ref = useRef<HTMLAnchorElement>(null);
|
|
91
92
|
// Navigation runs in a transition so a slow subtree cannot block the click.
|
|
92
93
|
const [, startTransition] = useTransition();
|
package/src/navigation.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
10
|
import { useMemo } from "react";
|
|
11
|
+
import { safeHref } from "@tschk/moonshine-router";
|
|
11
12
|
import {
|
|
12
|
-
|
|
13
|
+
useNavigate as useMoonshineNavigate,
|
|
13
14
|
useLocation,
|
|
14
15
|
useParams as useMoonshineParams,
|
|
15
16
|
} from "@tschk/moonshine/router";
|
|
@@ -32,17 +33,9 @@ export function splitLocation(location: string): [string, string] {
|
|
|
32
33
|
: [location.slice(0, index), location.slice(index + 1)];
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/**
|
|
36
|
-
* The location signal carries the query, so the signal is the source of truth
|
|
37
|
-
* and a query-only navigation re-renders. The browser fallback remains for the
|
|
38
|
-
* one case the signal cannot cover: a controlled `path` prop given without a
|
|
39
|
-
* query while the address bar has one.
|
|
40
|
-
*/
|
|
36
|
+
/** The scoped location signal is the source of truth for query state. */
|
|
41
37
|
function readSearch(location: string): string {
|
|
42
|
-
|
|
43
|
-
if (fromSignal) return fromSignal;
|
|
44
|
-
if (typeof window === "undefined") return "";
|
|
45
|
-
return window.location.search.replace(/^\?/, "");
|
|
38
|
+
return splitLocation(location)[1];
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
function shouldScrollByDefault(href: string): boolean {
|
|
@@ -54,27 +47,39 @@ function shouldScrollByDefault(href: string): boolean {
|
|
|
54
47
|
);
|
|
55
48
|
}
|
|
56
49
|
|
|
50
|
+
function navigateExternal(href: string, replace: boolean): boolean {
|
|
51
|
+
if (typeof window === "undefined") return false;
|
|
52
|
+
const url = new URL(safeHref(href), window.location.href);
|
|
53
|
+
if (url.origin === window.location.origin) return false;
|
|
54
|
+
if (replace) window.location.replace(url.href);
|
|
55
|
+
else window.location.assign(url.href);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
57
59
|
export function useRouter(): AppRouterInstance {
|
|
60
|
+
const navigate = useMoonshineNavigate();
|
|
58
61
|
return useMemo(
|
|
59
62
|
() => ({
|
|
60
63
|
push: (href, options) => {
|
|
64
|
+
if (navigateExternal(href, false)) return;
|
|
61
65
|
const jump = options?.scroll ?? shouldScrollByDefault(href);
|
|
62
66
|
navigate(href);
|
|
63
67
|
if (jump) globalThis.scrollTo?.(0, 0);
|
|
64
68
|
},
|
|
65
69
|
replace: (href, options) => {
|
|
70
|
+
if (navigateExternal(href, true)) return;
|
|
66
71
|
const jump = options?.scroll ?? shouldScrollByDefault(href);
|
|
67
72
|
navigate(href, { replace: true });
|
|
68
73
|
if (jump) globalThis.scrollTo?.(0, 0);
|
|
69
74
|
},
|
|
70
75
|
back: () => globalThis.history?.back(),
|
|
71
76
|
forward: () => globalThis.history?.forward(),
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
refresh: () =>
|
|
77
|
+
// Reload server data through a document request; local component state resets.
|
|
78
|
+
// State-preserving RSC refresh is not supported.
|
|
79
|
+
refresh: () => globalThis.location?.reload(),
|
|
75
80
|
prefetch: () => {},
|
|
76
81
|
}),
|
|
77
|
-
[],
|
|
82
|
+
[navigate],
|
|
78
83
|
);
|
|
79
84
|
}
|
|
80
85
|
|