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.
@@ -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 };
@@ -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,4 +1,4 @@
1
- import useSWRMutation from 'swr/mutation';
1
+ import { useSWRMutation } from './swr-compat.js';
2
2
  import { useApiContext } from '../provider.js';
3
3
  import { soloFetch } from '../fetcher.js';
4
4
 
@@ -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
- try {
8
- const item = window.localStorage.getItem(key);
9
- if (item !== null) {
10
- setStoredValue(JSON.parse(item));
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) : initialValue);
36
+ setStoredValue(e.newValue !== null ? JSON.parse(e.newValue) : initialValueRef.current);
18
37
  } catch {
19
- setStoredValue(initialValue);
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
- return () => window.removeEventListener('storage', handleStorage);
26
- }, [key, initialValue]);
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
- setStoredValue((prev) => {
31
- const nextValue = value instanceof Function ? value(prev) : value;
32
- try {
33
- window.localStorage.setItem(key, JSON.stringify(nextValue));
34
- } catch {}
35
- return nextValue;
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 (notably `useSearchParams`) re-read the URL.
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`/`useSearchParams` consumers re-render even though pathname and
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 `useSearchParams` had
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
- function Link({ href, children, onClick, scroll, replace, prefetch = 'hover', ...rest }) {
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, href);
332
+ prefetchRoute(manifest, resolvedHref);
317
333
  }
318
- }, [href, prefetch]);
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, href);
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
- }, [href, prefetch]);
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(href, window.location.origin);
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(href, { scroll, replace });
377
+ router.navigate(resolvedHref, { scroll, replace });
362
378
  } else {
363
- window.history.pushState(null, '', href);
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
- { ref: linkRef, href, onClick: handleClick, onMouseEnter: handleMouseEnter, ...rest },
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, useParams, usePathname, useRouter, useSearchParams };
398
+ export { Link, Router, SoloRouter, useRouter };