arcway 0.1.25 → 0.1.26
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/client/hooks/swr-compat.js +25 -0
- package/client/hooks/use-api.js +1 -4
- package/client/hooks/use-graphql.js +1 -2
- package/client/hooks/use-mutation.js +1 -1
- package/client/hooks/web/use-local-storage.js +54 -16
- package/client/index.js +15 -55
- package/client/router.js +51 -29
- package/client/ui/index.js +62 -380
- package/package.json +1 -1
- package/server/bin/commands/build.js +3 -0
- package/server/build.js +1 -1
- package/server/config/modules/pages.js +2 -2
- package/server/index.js +8 -33
- package/server/pages/build-client.js +1 -1
- package/server/pages/chunk-graph.js +1 -0
- package/server/pages/fonts.js +14 -1
- package/server/pages/handler.js +2 -2
- package/server/pages/lazy-context.js +2 -2
- package/server/pages/out-dir.js +1 -1
- package/server/pages/vite-dev.js +38 -3
- package/server/router/api-router.js +71 -2
- package/server/router/ratelimit.js +50 -0
- package/server/router/routes.js +10 -0
- package/client/hooks/use-form.js +0 -86
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as SWR from 'swr';
|
|
2
|
+
import * as SWRMutation from 'swr/mutation';
|
|
3
|
+
|
|
4
|
+
function resolveHook(mod) {
|
|
5
|
+
if (typeof mod === 'function') return mod;
|
|
6
|
+
if (typeof mod?.default === 'function') return mod.default;
|
|
7
|
+
if (typeof mod?.default?.default === 'function') return mod.default.default;
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function resolveNamed(mod, key) {
|
|
12
|
+
if (typeof mod?.[key] === 'function') return mod[key];
|
|
13
|
+
if (typeof mod?.default?.[key] === 'function') return mod.default[key];
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const useSWR = resolveHook(SWR);
|
|
18
|
+
const useSWRConfig = resolveNamed(SWR, 'useSWRConfig');
|
|
19
|
+
const useSWRMutation = resolveHook(SWRMutation);
|
|
20
|
+
|
|
21
|
+
if (!useSWR || !useSWRConfig || !useSWRMutation) {
|
|
22
|
+
throw new Error('Arcway failed to resolve SWR hooks');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { useSWR, useSWRConfig, useSWRMutation };
|
package/client/hooks/use-api.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
import * as SWR from 'swr';
|
|
3
2
|
import { useApiContext, useWsManager } from '../provider.js';
|
|
4
3
|
import { soloFetch, ApiError } from '../fetcher.js';
|
|
5
4
|
import { stringifyQuery } from '../query.js';
|
|
6
|
-
|
|
7
|
-
const useSWR = typeof SWR.default === 'function' ? SWR.default : SWR;
|
|
8
|
-
const { useSWRConfig } = SWR;
|
|
5
|
+
import { useSWR, useSWRConfig } from './swr-compat.js';
|
|
9
6
|
|
|
10
7
|
function buildUrl(base, query) {
|
|
11
8
|
if (!query) return base;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import useSWR from 'swr';
|
|
2
|
-
import useSWRMutation from 'swr/mutation';
|
|
3
1
|
import { useApiContext } from '../provider.js';
|
|
4
2
|
import { graphqlFetch } from '../graphql.js';
|
|
3
|
+
import { useSWR, useSWRMutation } from './swr-compat.js';
|
|
5
4
|
|
|
6
5
|
function useGraphQL(query, variables, options) {
|
|
7
6
|
const { pathPrefix, headers } = useApiContext();
|
|
@@ -1,39 +1,77 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const LOCAL_STORAGE_EVENT = 'arcway-local-storage';
|
|
2
4
|
|
|
3
5
|
export default function useLocalStorage(key, initialValue) {
|
|
4
6
|
const [storedValue, setStoredValue] = useState(initialValue);
|
|
7
|
+
const storedValueRef = useRef(initialValue);
|
|
8
|
+
const initialValueRef = useRef(initialValue);
|
|
9
|
+
const instanceIdRef = useRef(`ls:${Math.random().toString(36).slice(2)}`);
|
|
5
10
|
|
|
6
11
|
useEffect(() => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
storedValueRef.current = storedValue;
|
|
13
|
+
}, [storedValue]);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
initialValueRef.current = initialValue;
|
|
17
|
+
}, [key, initialValue]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const readValue = () => {
|
|
21
|
+
try {
|
|
22
|
+
const item = window.localStorage.getItem(key);
|
|
23
|
+
return item !== null ? JSON.parse(item) : initialValueRef.current;
|
|
24
|
+
} catch {
|
|
25
|
+
return initialValueRef.current;
|
|
11
26
|
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
setStoredValue(readValue());
|
|
12
31
|
} catch {}
|
|
13
32
|
|
|
14
33
|
const handleStorage = (e) => {
|
|
15
34
|
if (e.key === key) {
|
|
16
35
|
try {
|
|
17
|
-
setStoredValue(e.newValue !== null ? JSON.parse(e.newValue) :
|
|
36
|
+
setStoredValue(e.newValue !== null ? JSON.parse(e.newValue) : initialValueRef.current);
|
|
18
37
|
} catch {
|
|
19
|
-
setStoredValue(
|
|
38
|
+
setStoredValue(initialValueRef.current);
|
|
20
39
|
}
|
|
21
40
|
}
|
|
22
41
|
};
|
|
23
42
|
|
|
43
|
+
const handleLocalUpdate = (e) => {
|
|
44
|
+
if (e.detail?.key !== key) return;
|
|
45
|
+
if (e.detail?.source === instanceIdRef.current) return;
|
|
46
|
+
if ('value' in (e.detail ?? {})) {
|
|
47
|
+
setStoredValue(e.detail.value);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setStoredValue(readValue());
|
|
51
|
+
};
|
|
52
|
+
|
|
24
53
|
window.addEventListener('storage', handleStorage);
|
|
25
|
-
|
|
26
|
-
|
|
54
|
+
window.addEventListener(LOCAL_STORAGE_EVENT, handleLocalUpdate);
|
|
55
|
+
return () => {
|
|
56
|
+
window.removeEventListener('storage', handleStorage);
|
|
57
|
+
window.removeEventListener(LOCAL_STORAGE_EVENT, handleLocalUpdate);
|
|
58
|
+
};
|
|
59
|
+
}, [key]);
|
|
27
60
|
|
|
28
61
|
const setValue = useCallback(
|
|
29
62
|
(value) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
63
|
+
const prev = storedValueRef.current;
|
|
64
|
+
const nextValue = value instanceof Function ? value(prev) : value;
|
|
65
|
+
storedValueRef.current = nextValue;
|
|
66
|
+
setStoredValue(nextValue);
|
|
67
|
+
try {
|
|
68
|
+
window.localStorage.setItem(key, JSON.stringify(nextValue));
|
|
69
|
+
window.dispatchEvent(
|
|
70
|
+
new CustomEvent(LOCAL_STORAGE_EVENT, {
|
|
71
|
+
detail: { key, value: nextValue, source: instanceIdRef.current },
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
} catch {}
|
|
37
75
|
},
|
|
38
76
|
[key],
|
|
39
77
|
);
|
package/client/index.js
CHANGED
|
@@ -1,62 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ApiProvider,
|
|
3
|
-
Provider,
|
|
4
|
-
SoloProvider,
|
|
5
|
-
useApiContext,
|
|
6
|
-
useSoloContext,
|
|
7
|
-
useWsManager,
|
|
8
|
-
} from './provider.js';
|
|
9
|
-
import useApi from './hooks/use-api.js';
|
|
10
|
-
import useQuery from './hooks/use-query.js';
|
|
11
|
-
import useMutation from './hooks/use-mutation.js';
|
|
12
|
-
import useDebounce from './hooks/use-debounce.js';
|
|
13
|
-
import useForm from './hooks/use-form.js';
|
|
14
|
-
import useInterval from './hooks/use-interval.js';
|
|
15
|
-
import { ApiError, soloFetch } from './fetcher.js';
|
|
16
|
-
import { GraphQLError, graphqlFetch } from './graphql.js';
|
|
17
|
-
import { useGraphQL, useGraphQLMutation } from './hooks/use-graphql.js';
|
|
18
|
-
import { WsManager } from './ws.js';
|
|
19
|
-
import useLocalStorage from './hooks/web/use-local-storage.js';
|
|
20
|
-
import useClickOutside from './hooks/web/use-click-outside.js';
|
|
21
|
-
import { Link, Router, SoloRouter, useRouter, usePathname, useParams, useSearchParams } from './router.js';
|
|
22
|
-
import { Head, setSSRHeadData, clearSSRHeadData, renderHeadToString } from './head.js';
|
|
23
|
-
import { useEnv, env, collectPublicEnv, buildEnvScriptTag } from './env.js';
|
|
24
|
-
|
|
25
1
|
export {
|
|
26
|
-
ApiError,
|
|
27
2
|
ApiProvider,
|
|
28
|
-
GraphQLError,
|
|
29
|
-
Head,
|
|
30
|
-
Link,
|
|
31
3
|
Provider,
|
|
32
|
-
Router,
|
|
33
4
|
SoloProvider,
|
|
34
|
-
SoloRouter,
|
|
35
|
-
WsManager,
|
|
36
|
-
buildEnvScriptTag,
|
|
37
|
-
clearSSRHeadData,
|
|
38
|
-
collectPublicEnv,
|
|
39
|
-
env,
|
|
40
|
-
graphqlFetch,
|
|
41
|
-
renderHeadToString,
|
|
42
|
-
setSSRHeadData,
|
|
43
|
-
soloFetch,
|
|
44
|
-
useApi,
|
|
45
5
|
useApiContext,
|
|
46
|
-
useClickOutside,
|
|
47
|
-
useDebounce,
|
|
48
|
-
useEnv,
|
|
49
|
-
useForm,
|
|
50
|
-
useGraphQL,
|
|
51
|
-
useGraphQLMutation,
|
|
52
|
-
useInterval,
|
|
53
|
-
useLocalStorage,
|
|
54
|
-
useMutation,
|
|
55
|
-
useParams,
|
|
56
|
-
usePathname,
|
|
57
|
-
useQuery,
|
|
58
|
-
useRouter,
|
|
59
|
-
useSearchParams,
|
|
60
6
|
useSoloContext,
|
|
61
7
|
useWsManager,
|
|
62
|
-
};
|
|
8
|
+
} from './provider.js';
|
|
9
|
+
export { default as useApi } from './hooks/use-api.js';
|
|
10
|
+
export { default as useQuery } from './hooks/use-query.js';
|
|
11
|
+
export { default as useMutation } from './hooks/use-mutation.js';
|
|
12
|
+
export { default as useDebounce } from './hooks/use-debounce.js';
|
|
13
|
+
export { default as useInterval } from './hooks/use-interval.js';
|
|
14
|
+
export { ApiError, soloFetch } from './fetcher.js';
|
|
15
|
+
export { GraphQLError, graphqlFetch } from './graphql.js';
|
|
16
|
+
export { useGraphQL, useGraphQLMutation } from './hooks/use-graphql.js';
|
|
17
|
+
export { WsManager } from './ws.js';
|
|
18
|
+
export { default as useLocalStorage } from './hooks/web/use-local-storage.js';
|
|
19
|
+
export { default as useClickOutside } from './hooks/web/use-click-outside.js';
|
|
20
|
+
export { Link, Router, SoloRouter, useRouter } from './router.js';
|
|
21
|
+
export { Head, setSSRHeadData, clearSSRHeadData, renderHeadToString } from './head.js';
|
|
22
|
+
export { useEnv, env, collectPublicEnv, buildEnvScriptTag } from './env.js';
|
package/client/router.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
useTransition,
|
|
10
10
|
createElement,
|
|
11
|
+
forwardRef,
|
|
11
12
|
} from 'react';
|
|
12
13
|
import {
|
|
13
14
|
readClientManifest,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
prefetchRoute,
|
|
18
19
|
syncStylesForPath,
|
|
19
20
|
} from './page-loader.js';
|
|
20
|
-
import { parseQuery } from './query.js';
|
|
21
|
+
import { parseQuery, stringifyQuery } from './query.js';
|
|
21
22
|
|
|
22
23
|
const ROUTER_CTX_KEY = '__router_context__';
|
|
23
24
|
const RouterContext = (globalThis[ROUTER_CTX_KEY] ??= createContext(null));
|
|
@@ -29,6 +30,21 @@ function wrapInLayouts(element, layouts) {
|
|
|
29
30
|
return element;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function normalizeNavigateTarget(to) {
|
|
34
|
+
if (typeof to === 'string') return to;
|
|
35
|
+
if (!to || typeof to !== 'object') {
|
|
36
|
+
throw new Error('router.push/replace expects a string URL or { pathname, query }');
|
|
37
|
+
}
|
|
38
|
+
const pathname =
|
|
39
|
+
typeof to.pathname === 'string'
|
|
40
|
+
? to.pathname
|
|
41
|
+
: typeof window !== 'undefined'
|
|
42
|
+
? window.location.pathname
|
|
43
|
+
: '/';
|
|
44
|
+
const qs = stringifyQuery(to.query || {});
|
|
45
|
+
return qs ? `${pathname}?${qs}` : pathname;
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
function useRouter() {
|
|
33
49
|
const ctx = useContext(RouterContext);
|
|
34
50
|
if (!ctx) {
|
|
@@ -40,8 +56,8 @@ function useRouter() {
|
|
|
40
56
|
pathname: ctx.pathname,
|
|
41
57
|
params: ctx.params,
|
|
42
58
|
query: typeof window !== 'undefined' ? parseQuery(window.location.search) : {},
|
|
43
|
-
push: (to, options) => ctx.navigate(to, { ...options, replace: false }),
|
|
44
|
-
replace: (to, options) => ctx.navigate(to, { ...options, replace: true }),
|
|
59
|
+
push: (to, options) => ctx.navigate(normalizeNavigateTarget(to), { ...options, replace: false }),
|
|
60
|
+
replace: (to, options) => ctx.navigate(normalizeNavigateTarget(to), { ...options, replace: true }),
|
|
45
61
|
back: () => {
|
|
46
62
|
if (typeof window !== 'undefined') window.history.back();
|
|
47
63
|
},
|
|
@@ -53,23 +69,11 @@ function useRouter() {
|
|
|
53
69
|
},
|
|
54
70
|
}),
|
|
55
71
|
// queryVersion bumps on every query-only navigation or same-path popstate,
|
|
56
|
-
// so memoised consumers
|
|
72
|
+
// so memoised consumers re-read the URL.
|
|
57
73
|
[ctx.pathname, ctx.params, ctx.navigate, ctx.queryVersion],
|
|
58
74
|
);
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
function usePathname() {
|
|
62
|
-
return useRouter().pathname;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function useParams() {
|
|
66
|
-
return useRouter().params;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function useSearchParams() {
|
|
70
|
-
return useRouter().query;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
77
|
function Router({
|
|
74
78
|
initialPath,
|
|
75
79
|
initialParams,
|
|
@@ -92,7 +96,7 @@ function Router({
|
|
|
92
96
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
93
97
|
const [isPending, startTransition] = useTransition();
|
|
94
98
|
// Bumped on query-only navigations (both push/replace and popstate) so that
|
|
95
|
-
// `useRouter
|
|
99
|
+
// `useRouter` consumers re-render even though pathname and
|
|
96
100
|
// params are unchanged.
|
|
97
101
|
const [queryVersion, setQueryVersion] = useState(0);
|
|
98
102
|
const manifestRef = useRef(null);
|
|
@@ -123,7 +127,7 @@ function Router({
|
|
|
123
127
|
// so React commits the new `pathname` and the re-read of
|
|
124
128
|
// `window.location.search` (triggered by `queryVersion`) in one go. Keeping
|
|
125
129
|
// the bump outside the transition raced at default priority and produced
|
|
126
|
-
// an interim render where `pathname` was stale but `
|
|
130
|
+
// an interim render where `pathname` was stale but `useRouter().query` had
|
|
127
131
|
// already moved to the new query string — see the
|
|
128
132
|
// `query-version-outside-navigation-transition` bug report for the
|
|
129
133
|
// downstream data-corruption pattern that unlocked.
|
|
@@ -305,17 +309,29 @@ function Router({
|
|
|
305
309
|
);
|
|
306
310
|
}
|
|
307
311
|
|
|
308
|
-
|
|
312
|
+
const Link = forwardRef(function Link(
|
|
313
|
+
{ href, children, onClick, scroll, replace, prefetch = 'hover', ...rest },
|
|
314
|
+
forwardedRef,
|
|
315
|
+
) {
|
|
309
316
|
const router = useContext(RouterContext);
|
|
310
317
|
const linkRef = useRef(null);
|
|
318
|
+
const resolvedHref = normalizeNavigateTarget(href);
|
|
319
|
+
const setLinkRef = useCallback(
|
|
320
|
+
(node) => {
|
|
321
|
+
linkRef.current = node;
|
|
322
|
+
if (typeof forwardedRef === 'function') forwardedRef(node);
|
|
323
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
324
|
+
},
|
|
325
|
+
[forwardedRef],
|
|
326
|
+
);
|
|
311
327
|
|
|
312
328
|
const handleMouseEnter = useCallback(() => {
|
|
313
329
|
if (prefetch !== 'hover') return;
|
|
314
330
|
const manifest = readClientManifest();
|
|
315
331
|
if (manifest) {
|
|
316
|
-
prefetchRoute(manifest,
|
|
332
|
+
prefetchRoute(manifest, resolvedHref);
|
|
317
333
|
}
|
|
318
|
-
}, [
|
|
334
|
+
}, [resolvedHref, prefetch]);
|
|
319
335
|
|
|
320
336
|
useEffect(() => {
|
|
321
337
|
if (prefetch !== 'viewport') return;
|
|
@@ -328,7 +344,7 @@ function Link({ href, children, onClick, scroll, replace, prefetch = 'hover', ..
|
|
|
328
344
|
if (entry.isIntersecting) {
|
|
329
345
|
const manifest = readClientManifest();
|
|
330
346
|
if (manifest) {
|
|
331
|
-
prefetchRoute(manifest,
|
|
347
|
+
prefetchRoute(manifest, resolvedHref);
|
|
332
348
|
}
|
|
333
349
|
observer.unobserve(el);
|
|
334
350
|
break;
|
|
@@ -340,7 +356,7 @@ function Link({ href, children, onClick, scroll, replace, prefetch = 'hover', ..
|
|
|
340
356
|
|
|
341
357
|
observer.observe(el);
|
|
342
358
|
return () => observer.disconnect();
|
|
343
|
-
}, [
|
|
359
|
+
}, [resolvedHref, prefetch]);
|
|
344
360
|
|
|
345
361
|
function handleClick(e) {
|
|
346
362
|
if (onClick) onClick(e);
|
|
@@ -350,7 +366,7 @@ function Link({ href, children, onClick, scroll, replace, prefetch = 'hover', ..
|
|
|
350
366
|
if (rest.target === '_blank' || rest.download !== undefined) return;
|
|
351
367
|
|
|
352
368
|
try {
|
|
353
|
-
const url = new URL(
|
|
369
|
+
const url = new URL(resolvedHref, window.location.origin);
|
|
354
370
|
if (url.origin !== window.location.origin) return;
|
|
355
371
|
} catch {
|
|
356
372
|
return;
|
|
@@ -358,19 +374,25 @@ function Link({ href, children, onClick, scroll, replace, prefetch = 'hover', ..
|
|
|
358
374
|
|
|
359
375
|
e.preventDefault();
|
|
360
376
|
if (router) {
|
|
361
|
-
router.navigate(
|
|
377
|
+
router.navigate(resolvedHref, { scroll, replace });
|
|
362
378
|
} else {
|
|
363
|
-
window.history.pushState(null, '',
|
|
379
|
+
window.history.pushState(null, '', resolvedHref);
|
|
364
380
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
365
381
|
}
|
|
366
382
|
}
|
|
367
383
|
|
|
368
384
|
return createElement(
|
|
369
385
|
'a',
|
|
370
|
-
{
|
|
386
|
+
{
|
|
387
|
+
ref: setLinkRef,
|
|
388
|
+
href: resolvedHref,
|
|
389
|
+
onClick: handleClick,
|
|
390
|
+
onMouseEnter: handleMouseEnter,
|
|
391
|
+
...rest,
|
|
392
|
+
},
|
|
371
393
|
children,
|
|
372
394
|
);
|
|
373
|
-
}
|
|
395
|
+
});
|
|
374
396
|
|
|
375
397
|
const SoloRouter = Router;
|
|
376
|
-
export { Link, Router, SoloRouter,
|
|
398
|
+
export { Link, Router, SoloRouter, useRouter };
|