hydrogen-sanity 5.1.3 → 5.3.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.
@@ -1,6 +1,7 @@
1
1
  import { createClient } from '@sanity/client';
2
2
  import { useLiveMode } from '@sanity/react-loader';
3
- import { useMemo, useEffect } from 'react';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { useState, useEffect, useMemo } from 'react';
4
5
  import { useSanityProviderValue } from './provider.js';
5
6
  import { isServer } from './utils.js';
6
7
  import { useRefresh } from './refresh.js';
@@ -13,6 +14,12 @@ if (isServer()) {
13
14
  function LiveModeClient(props) {
14
15
  const { onConnect, onDisconnect, ...stegaProps } = props;
15
16
  const sanityProvider = useSanityProviderValue();
17
+ const [stableStegaProps, setStableStegaProps] = useState(stegaProps);
18
+ useEffect(() => {
19
+ if (!isEqual(stableStegaProps, stegaProps)) {
20
+ setStableStegaProps(stegaProps);
21
+ }
22
+ }, [stegaProps]);
16
23
  const client = useMemo(() => {
17
24
  const baseClient = createClient({
18
25
  projectId: sanityProvider.projectId,
@@ -21,11 +28,11 @@ function LiveModeClient(props) {
21
28
  apiVersion: sanityProvider.apiVersion,
22
29
  useCdn: false
23
30
  });
24
- if (sanityProvider.stegaEnabled && Object.keys(stegaProps).length > 0) {
31
+ if (sanityProvider.stegaEnabled && Object.keys(stableStegaProps).length > 0) {
25
32
  return baseClient.withConfig({
26
33
  stega: {
27
34
  enabled: true,
28
- ...stegaProps
35
+ ...stableStegaProps
29
36
  }
30
37
  });
31
38
  }
@@ -36,18 +43,14 @@ function LiveModeClient(props) {
36
43
  sanityProvider.perspective,
37
44
  sanityProvider.apiVersion,
38
45
  sanityProvider.stegaEnabled,
39
- // eslint-disable-next-line react-hooks/preserve-manual-memoization
40
- stegaProps
46
+ stableStegaProps
41
47
  ]);
42
48
  useLiveMode({
43
49
  client,
44
50
  onConnect,
45
51
  onDisconnect
46
52
  });
47
- const { handleRevalidatorState } = useRefresh();
48
- useEffect(() => {
49
- handleRevalidatorState();
50
- });
53
+ useRefresh();
51
54
  return null;
52
55
  }
53
56
 
@@ -1 +1 @@
1
- {"version":3,"file":"LiveMode.client.js","sources":["../../src/visual-editing/LiveMode.client.tsx"],"sourcesContent":["import {createClient, type StegaConfig} from '@sanity/client'\nimport {useLiveMode} from '@sanity/react-loader'\nimport {type ReactNode, useEffect, useMemo} from 'react'\n\nimport {useSanityProviderValue} from '../provider'\nimport {isServer} from '../utils'\nimport {useRefresh} from './hooks/refresh'\n\nexport interface LiveModeProps extends Omit<StegaConfig, 'enabled'> {\n /**\n * Fires when a connection is established to the Studio.\n */\n onConnect?: () => void\n /**\n * Fires when a connection to the Studio is lost.\n */\n onDisconnect?: () => void\n}\n\n/**\n * Prevent a consumer from importing into a worker/server bundle.\n */\nif (isServer()) {\n throw new Error(\n 'LiveMode should only run client-side. Please check that this file is not being imported into a worker or server bundle.',\n )\n}\n\n/**\n * Enables live data synchronization with Sanity Studio.\n *\n * This component handles:\n * - Real-time data updates via comlink connection to Studio\n * - Perspective changes (draft/published switching)\n * - Connection status with Studio\n *\n * Only use this component when you have client-side loaders (useQuery hooks)\n * that can receive real-time updates. For server-only setups, use only\n * Overlays component.\n *\n * @see https://www.sanity.io/docs/introduction-to-visual-editing\n */\nfunction LiveModeClient(props: LiveModeProps): ReactNode {\n const {onConnect, onDisconnect, ...stegaProps} = props\n\n const sanityProvider = useSanityProviderValue()\n\n // eslint-disable-next-line react-hooks/preserve-manual-memoization\n const client = useMemo(() => {\n const baseClient = createClient({\n projectId: sanityProvider.projectId,\n dataset: sanityProvider.dataset,\n perspective: sanityProvider.perspective,\n apiVersion: sanityProvider.apiVersion,\n useCdn: false,\n })\n\n // Apply stega configuration if provided\n if (sanityProvider.stegaEnabled && Object.keys(stegaProps).length > 0) {\n return baseClient.withConfig({\n stega: {\n enabled: true,\n ...stegaProps,\n },\n })\n }\n\n return baseClient\n }, [\n sanityProvider.projectId,\n sanityProvider.dataset,\n sanityProvider.perspective,\n sanityProvider.apiVersion,\n sanityProvider.stegaEnabled,\n // eslint-disable-next-line react-hooks/preserve-manual-memoization\n stegaProps,\n ])\n\n // Enable live mode for real-time data updates (client loaders only)\n useLiveMode({\n client,\n onConnect,\n onDisconnect,\n })\n\n // Automatically handle revalidator state changes\n const {handleRevalidatorState} = useRefresh()\n useEffect(() => {\n handleRevalidatorState()\n })\n\n return null\n}\n\nexport default LiveModeClient\n"],"names":[],"mappings":";;;;;;;AAsBA,IAAI,UAAS,EAAG;AACd,EAAA,MAAM,IAAI,KAAA;AAAA,IACR;AAAA,GACF;AACF;AAgBA,SAAS,eAAe,KAAA,EAAiC;AACvD,EAAA,MAAM,EAAC,SAAA,EAAW,YAAA,EAAc,GAAG,YAAU,GAAI,KAAA;AAEjD,EAAA,MAAM,iBAAiB,sBAAA,EAAuB;AAG9C,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAM;AAC3B,IAAA,MAAM,aAAa,YAAA,CAAa;AAAA,MAC9B,WAAW,cAAA,CAAe,SAAA;AAAA,MAC1B,SAAS,cAAA,CAAe,OAAA;AAAA,MACxB,aAAa,cAAA,CAAe,WAAA;AAAA,MAC5B,YAAY,cAAA,CAAe,UAAA;AAAA,MAC3B,MAAA,EAAQ;AAAA,KACT,CAAA;AAGD,IAAA,IAAI,eAAe,YAAA,IAAgB,MAAA,CAAO,KAAK,UAAU,CAAA,CAAE,SAAS,CAAA,EAAG;AACrE,MAAA,OAAO,WAAW,UAAA,CAAW;AAAA,QAC3B,KAAA,EAAO;AAAA,UACL,OAAA,EAAS,IAAA;AAAA,UACT,GAAG;AAAA;AACL,OACD,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,UAAA;AAAA,EACT,CAAA,EAAG;AAAA,IACD,cAAA,CAAe,SAAA;AAAA,IACf,cAAA,CAAe,OAAA;AAAA,IACf,cAAA,CAAe,WAAA;AAAA,IACf,cAAA,CAAe,UAAA;AAAA,IACf,cAAA,CAAe,YAAA;AAAA;AAAA,IAEf;AAAA,GACD,CAAA;AAGD,EAAA,WAAA,CAAY;AAAA,IACV,MAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AAGD,EAAA,MAAM,EAAC,sBAAA,EAAsB,GAAI,UAAA,EAAW;AAC5C,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,sBAAA,EAAuB;AAAA,EACzB,CAAC,CAAA;AAED,EAAA,OAAO,IAAA;AACT;;;;"}
1
+ {"version":3,"file":"LiveMode.client.js","sources":["../../src/visual-editing/LiveMode.client.tsx"],"sourcesContent":["import {createClient, type StegaConfig} from '@sanity/client'\nimport {useLiveMode} from '@sanity/react-loader'\nimport isEqual from 'fast-deep-equal'\nimport {type ReactNode, useEffect, useMemo, useState} from 'react'\n\nimport {useSanityProviderValue} from '../provider'\nimport {isServer} from '../utils'\nimport {useRefresh} from './hooks/refresh'\n\nexport interface LiveModeProps extends Omit<StegaConfig, 'enabled'> {\n /**\n * Fires when a connection is established to the Studio.\n */\n onConnect?: () => void\n /**\n * Fires when a connection to the Studio is lost.\n */\n onDisconnect?: () => void\n}\n\n/**\n * Prevent a consumer from importing into a worker/server bundle.\n */\nif (isServer()) {\n throw new Error(\n 'LiveMode should only run client-side. Please check that this file is not being imported into a worker or server bundle.',\n )\n}\n\n/**\n * Enables live data synchronization with Sanity Studio.\n *\n * This component handles:\n * - Real-time data updates via comlink connection to Studio\n * - Perspective changes (draft/published switching)\n * - Connection status with Studio\n *\n * Only use this component when you have client-side loaders (useQuery hooks)\n * that can receive real-time updates. For server-only setups, use only\n * Overlays component.\n *\n * @see https://www.sanity.io/docs/introduction-to-visual-editing\n */\nfunction LiveModeClient(props: LiveModeProps): ReactNode {\n const {onConnect, onDisconnect, ...stegaProps} = props\n\n const sanityProvider = useSanityProviderValue()\n\n // Maintain reference stability for stegaProps when content is unchanged\n // This prevents unnecessary client recreation when parent component re-renders\n const [stableStegaProps, setStableStegaProps] = useState(stegaProps)\n useEffect(() => {\n if (!isEqual(stableStegaProps, stegaProps)) {\n setStableStegaProps(stegaProps)\n }\n // Intentionally not including stableStegaProps in deps - we only want to\n // update when the incoming stegaProps changes, comparing against the stored value\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [stegaProps])\n\n const client = useMemo(() => {\n const baseClient = createClient({\n projectId: sanityProvider.projectId,\n dataset: sanityProvider.dataset,\n perspective: sanityProvider.perspective,\n apiVersion: sanityProvider.apiVersion,\n useCdn: false,\n })\n\n // Apply stega configuration if provided\n if (sanityProvider.stegaEnabled && Object.keys(stableStegaProps).length > 0) {\n return baseClient.withConfig({\n stega: {\n enabled: true,\n ...stableStegaProps,\n },\n })\n }\n\n return baseClient\n }, [\n sanityProvider.projectId,\n sanityProvider.dataset,\n sanityProvider.perspective,\n sanityProvider.apiVersion,\n sanityProvider.stegaEnabled,\n stableStegaProps,\n ])\n\n // Enable live mode for real-time data updates (client loaders only)\n useLiveMode({\n client,\n onConnect,\n onDisconnect,\n })\n\n // Initialize refresh hook to handle revalidator state transitions\n // The hook internally manages state changes via useEffect\n useRefresh()\n\n return null\n}\n\nexport default LiveModeClient\n"],"names":[],"mappings":";;;;;;;;AAuBA,IAAI,UAAS,EAAG;AACd,EAAA,MAAM,IAAI,KAAA;AAAA,IACR;AAAA,GACF;AACF;AAgBA,SAAS,eAAe,KAAA,EAAiC;AACvD,EAAA,MAAM,EAAC,SAAA,EAAW,YAAA,EAAc,GAAG,YAAU,GAAI,KAAA;AAEjD,EAAA,MAAM,iBAAiB,sBAAA,EAAuB;AAI9C,EAAA,MAAM,CAAC,gBAAA,EAAkB,mBAAmB,CAAA,GAAI,SAAS,UAAU,CAAA;AACnE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAA,CAAQ,gBAAA,EAAkB,UAAU,CAAA,EAAG;AAC1C,MAAA,mBAAA,CAAoB,UAAU,CAAA;AAAA,IAChC;AAAA,EAIF,CAAA,EAAG,CAAC,UAAU,CAAC,CAAA;AAEf,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAM;AAC3B,IAAA,MAAM,aAAa,YAAA,CAAa;AAAA,MAC9B,WAAW,cAAA,CAAe,SAAA;AAAA,MAC1B,SAAS,cAAA,CAAe,OAAA;AAAA,MACxB,aAAa,cAAA,CAAe,WAAA;AAAA,MAC5B,YAAY,cAAA,CAAe,UAAA;AAAA,MAC3B,MAAA,EAAQ;AAAA,KACT,CAAA;AAGD,IAAA,IAAI,eAAe,YAAA,IAAgB,MAAA,CAAO,KAAK,gBAAgB,CAAA,CAAE,SAAS,CAAA,EAAG;AAC3E,MAAA,OAAO,WAAW,UAAA,CAAW;AAAA,QAC3B,KAAA,EAAO;AAAA,UACL,OAAA,EAAS,IAAA;AAAA,UACT,GAAG;AAAA;AACL,OACD,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,UAAA;AAAA,EACT,CAAA,EAAG;AAAA,IACD,cAAA,CAAe,SAAA;AAAA,IACf,cAAA,CAAe,OAAA;AAAA,IACf,cAAA,CAAe,WAAA;AAAA,IACf,cAAA,CAAe,UAAA;AAAA,IACf,cAAA,CAAe,YAAA;AAAA,IACf;AAAA,GACD,CAAA;AAGD,EAAA,WAAA,CAAY;AAAA,IACV,MAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AAID,EAAA,UAAA,EAAW;AAEX,EAAA,OAAO,IAAA;AACT;;;;"}
@@ -3,7 +3,7 @@ import { enableVisualEditing } from '@sanity/visual-editing';
3
3
  import { useRef, useState, useEffect, useMemo } from 'react';
4
4
  import { useNavigate, useLocation, useSubmit, useRevalidator } from 'react-router';
5
5
  import { useEffectEvent } from 'use-effect-event';
6
- import { isServer } from './utils.js';
6
+ import { isServer, sanitizePerspective } from './utils.js';
7
7
  import { useRefresh } from './refresh.js';
8
8
  import { useHasActiveLoaders } from './registry.js';
9
9
 
@@ -12,16 +12,18 @@ function useHistory() {
12
12
  const navigateRemixRef = useRef(navigateRemix);
13
13
  const [navigate, setNavigate] = useState();
14
14
  const location = useLocation();
15
+ const isProgrammaticNavRef = useRef(false);
15
16
  useEffect(() => {
16
17
  navigateRemixRef.current = navigateRemix;
17
18
  }, [navigateRemix]);
18
19
  useEffect(() => {
19
- if (navigate) {
20
+ if (navigate && !isProgrammaticNavRef.current) {
20
21
  navigate({
21
22
  type: "push",
22
23
  url: `${location.pathname}${location.search}${location.hash}`
23
24
  });
24
25
  }
26
+ isProgrammaticNavRef.current = false;
25
27
  }, [location.hash, location.pathname, location.search, navigate]);
26
28
  const historyAdapter = useMemo(
27
29
  () => ({
@@ -30,6 +32,7 @@ function useHistory() {
30
32
  return () => setNavigate(void 0);
31
33
  },
32
34
  update(update) {
35
+ isProgrammaticNavRef.current = true;
33
36
  if (update.type === "push" || update.type === "replace") {
34
37
  navigateRemixRef.current(update.url, {
35
38
  replace: update.type === "replace"
@@ -64,8 +67,12 @@ function OverlaysClient(props) {
64
67
  return isMaybePresentation();
65
68
  });
66
69
  const handlePerspectiveChange = useEffectEvent((perspective) => {
70
+ const cleanPerspective = sanitizePerspective(perspective);
67
71
  const formData = new FormData();
68
- formData.set("perspective", Array.isArray(perspective) ? perspective.join(",") : perspective);
72
+ formData.set(
73
+ "perspective",
74
+ Array.isArray(cleanPerspective) ? cleanPerspective.join(",") : cleanPerspective
75
+ );
69
76
  submit(formData, {
70
77
  method: "PUT",
71
78
  action,
@@ -1 +1 @@
1
- {"version":3,"file":"Overlays.client.js","sources":["../../src/visual-editing/hooks/history.ts","../../src/visual-editing/Overlays.client.tsx"],"sourcesContent":["import type {HistoryAdapter, HistoryAdapterNavigate, HistoryUpdate} from '@sanity/visual-editing'\nimport {useEffect, useMemo, useRef, useState} from 'react'\nimport {useLocation, useNavigate} from 'react-router'\n\n/**\n * Hook that provides history management for visual editing.\n * Integrates with React Router's navigation for Studio-storefront communication.\n */\nexport function useHistory(): HistoryAdapter {\n const navigateRemix = useNavigate()\n const navigateRemixRef = useRef(navigateRemix)\n const [navigate, setNavigate] = useState<HistoryAdapterNavigate | undefined>()\n const location = useLocation()\n\n useEffect(() => {\n navigateRemixRef.current = navigateRemix\n }, [navigateRemix])\n\n useEffect(() => {\n if (navigate) {\n navigate({\n type: 'push',\n url: `${location.pathname}${location.search}${location.hash}`,\n })\n }\n }, [location.hash, location.pathname, location.search, navigate])\n\n const historyAdapter: HistoryAdapter = useMemo(\n () => ({\n subscribe(_navigate: HistoryAdapterNavigate) {\n setNavigate(() => _navigate)\n return () => setNavigate(undefined)\n },\n update(update: HistoryUpdate) {\n if (update.type === 'push' || update.type === 'replace') {\n navigateRemixRef.current(update.url, {\n replace: update.type === 'replace',\n })\n } else if (update.type === 'pop') {\n navigateRemixRef.current(-1)\n }\n },\n }),\n [],\n )\n\n return historyAdapter\n}\n","import {type ClientPerspective} from '@sanity/client'\nimport {isMaybePresentation} from '@sanity/presentation-comlink'\nimport {\n enableVisualEditing,\n type HistoryRefresh,\n type OverlayComponentResolver,\n} from '@sanity/visual-editing'\nimport {type ReactNode, useEffect, useState} from 'react'\nimport {useRevalidator, useSubmit} from 'react-router'\nimport {useEffectEvent} from 'use-effect-event'\n\nimport {isServer} from '../utils'\nimport {useHistory} from './hooks/history'\nimport {useRefresh} from './hooks/refresh'\nimport {useHasActiveLoaders} from './registry'\nimport type {Revalidator} from './types'\n\nexport interface OverlaysProps {\n /**\n * Custom overlay components for visual editing.\n */\n components?: OverlayComponentResolver\n /**\n * The CSS z-index for visual editing overlays.\n */\n zIndex?: string | number\n /**\n * Custom refresh logic. Called when content changes.\n */\n refresh?: (\n payload: HistoryRefresh,\n refreshDefault: () => false | Promise<void>,\n revalidator: Revalidator,\n ) => false | Promise<void>\n /**\n * The action URL path used to submit perspective changes.\n */\n action?: string\n}\n\n/**\n * Prevent a consumer from importing into a worker/server bundle.\n */\nif (isServer()) {\n throw new Error(\n 'Overlays should only run client-side. Please check that this file is not being imported into a worker or server bundle.',\n )\n}\n\n/**\n * Enables visual editing overlays and click-to-edit functionality.\n *\n * This component handles:\n * - Visual overlays for click-to-edit\n * - Element highlighting and interactions\n * - Perspective changes from Studio\n * - Server revalidation for content changes\n *\n * For real-time data synchronization with Studio, also use LiveMode component.\n *\n * @see https://www.sanity.io/docs/introduction-to-visual-editing\n */\nfunction OverlaysClient(props: OverlaysProps): ReactNode {\n const {components, zIndex, refresh, action = '/api/preview'} = props\n\n const submit = useSubmit()\n const revalidator = useRevalidator()\n const {refreshHandler} = useRefresh()\n const refreshFn = refreshHandler(refresh)\n const historyAdapter = useHistory()\n const hasActiveLoaders = useHasActiveLoaders()\n\n // Detect if we're in a Studio presentation context (lazy initialization for SSR safety)\n const [inStudioContext] = useState<boolean | null>(() => {\n // Only run on client-side to avoid SSR mismatch\n if (isServer()) {\n return null\n }\n\n return isMaybePresentation()\n })\n\n // Handle perspective changes from Studio\n const handlePerspectiveChange = useEffectEvent((perspective: ClientPerspective) => {\n const formData = new FormData()\n formData.set('perspective', Array.isArray(perspective) ? perspective.join(',') : perspective)\n submit(formData, {\n method: 'PUT',\n action,\n navigate: false,\n preventScrollReset: true,\n })\n\n // Always trigger refresh for perspective changes (server revalidation needed)\n refreshFn({\n source: 'manual',\n livePreviewEnabled: false, // Force server revalidation for perspective changes\n } as const)\n })\n\n const handleRefresh = useEffectEvent((payload: HistoryRefresh): false | Promise<void> => {\n // Prioritize userland refresh function if provided\n if (refresh) {\n return refresh(payload, () => refreshFn(payload), revalidator)\n }\n\n switch (payload.source) {\n case 'manual':\n // Always handle manual refresh events (like perspective changes)\n return refreshFn(payload)\n\n default:\n // For other refresh events, check if active loaders should handle them\n if (hasActiveLoaders) {\n return false // Let client loaders handle mutations (live mode active)\n }\n return refreshFn(payload) // Server revalidation for server-only setups\n }\n })\n\n // Listen for presentation events from Studio (only perspective changes needed for server revalidation)\n useEffect(() => {\n if (isServer() || !inStudioContext) return undefined\n\n const handleMessage = (event: MessageEvent) => {\n const {type, data} = event.data || {}\n\n // Only handle perspective changes - let enableVisualEditing handle refresh events\n if (type === 'presentation/perspective') {\n handlePerspectiveChange(data.perspective)\n }\n }\n\n window.addEventListener('message', handleMessage)\n\n return () => {\n window.removeEventListener('message', handleMessage)\n }\n }, [inStudioContext])\n\n // Enable visual editing overlays and interactions\n useEffect(() => {\n const disable = enableVisualEditing({\n components,\n zIndex,\n refresh: handleRefresh,\n history: historyAdapter,\n })\n\n return () => disable()\n }, [components, zIndex, historyAdapter])\n\n return null\n}\n\nexport default OverlaysClient\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Overlays.client.js","sources":["../../src/visual-editing/hooks/history.ts","../../src/visual-editing/Overlays.client.tsx"],"sourcesContent":["import type {HistoryAdapter, HistoryAdapterNavigate, HistoryUpdate} from '@sanity/visual-editing'\nimport {useEffect, useMemo, useRef, useState} from 'react'\nimport {useLocation, useNavigate} from 'react-router'\n\n/**\n * Hook that provides history management for visual editing.\n * Integrates with React Router's navigation for Studio-storefront communication.\n */\nexport function useHistory(): HistoryAdapter {\n const navigateRemix = useNavigate()\n const navigateRemixRef = useRef(navigateRemix)\n const [navigate, setNavigate] = useState<HistoryAdapterNavigate | undefined>()\n const location = useLocation()\n // Track programmatic navigations to avoid duplicate notifications to Studio\n const isProgrammaticNavRef = useRef(false)\n\n useEffect(() => {\n navigateRemixRef.current = navigateRemix\n }, [navigateRemix])\n\n useEffect(() => {\n // Skip notification for programmatic navigations (initiated by Studio)\n // to avoid duplicate notifications and potential infinite loops\n if (navigate && !isProgrammaticNavRef.current) {\n navigate({\n type: 'push',\n url: `${location.pathname}${location.search}${location.hash}`,\n })\n }\n isProgrammaticNavRef.current = false\n }, [location.hash, location.pathname, location.search, navigate])\n\n const historyAdapter: HistoryAdapter = useMemo(\n () => ({\n subscribe(_navigate: HistoryAdapterNavigate) {\n setNavigate(() => _navigate)\n return () => setNavigate(undefined)\n },\n update(update: HistoryUpdate) {\n // Mark as programmatic navigation to skip notification in the location effect\n isProgrammaticNavRef.current = true\n if (update.type === 'push' || update.type === 'replace') {\n navigateRemixRef.current(update.url, {\n replace: update.type === 'replace',\n })\n } else if (update.type === 'pop') {\n navigateRemixRef.current(-1)\n }\n },\n }),\n [],\n )\n\n return historyAdapter\n}\n","import {type ClientPerspective} from '@sanity/client'\nimport {isMaybePresentation} from '@sanity/presentation-comlink'\nimport {\n enableVisualEditing,\n type HistoryRefresh,\n type OverlayComponentResolver,\n} from '@sanity/visual-editing'\nimport {type ReactNode, useEffect, useState} from 'react'\nimport {useRevalidator, useSubmit} from 'react-router'\nimport {useEffectEvent} from 'use-effect-event'\n\nimport {isServer, sanitizePerspective} from '../utils'\nimport {useHistory} from './hooks/history'\nimport {useRefresh} from './hooks/refresh'\nimport {useHasActiveLoaders} from './registry'\nimport type {Revalidator} from './types'\n\nexport interface OverlaysProps {\n /**\n * Custom overlay components for visual editing.\n */\n components?: OverlayComponentResolver\n /**\n * The CSS z-index for visual editing overlays.\n */\n zIndex?: string | number\n /**\n * Custom refresh logic. Called when content changes.\n */\n refresh?: (\n payload: HistoryRefresh,\n refreshDefault: () => false | Promise<void>,\n revalidator: Revalidator,\n ) => false | Promise<void>\n /**\n * The action URL path used to submit perspective changes.\n */\n action?: string\n}\n\n/**\n * Prevent a consumer from importing into a worker/server bundle.\n */\nif (isServer()) {\n throw new Error(\n 'Overlays should only run client-side. Please check that this file is not being imported into a worker or server bundle.',\n )\n}\n\n/**\n * Enables visual editing overlays and click-to-edit functionality.\n *\n * This component handles:\n * - Visual overlays for click-to-edit\n * - Element highlighting and interactions\n * - Perspective changes from Studio\n * - Server revalidation for content changes\n *\n * For real-time data synchronization with Studio, also use LiveMode component.\n *\n * @see https://www.sanity.io/docs/introduction-to-visual-editing\n */\nfunction OverlaysClient(props: OverlaysProps): ReactNode {\n const {components, zIndex, refresh, action = '/api/preview'} = props\n\n const submit = useSubmit()\n const revalidator = useRevalidator()\n const {refreshHandler} = useRefresh()\n const refreshFn = refreshHandler(refresh)\n const historyAdapter = useHistory()\n const hasActiveLoaders = useHasActiveLoaders()\n\n // Detect if we're in a Studio presentation context (lazy initialization for SSR safety)\n const [inStudioContext] = useState<boolean | null>(() => {\n // Only run on client-side to avoid SSR mismatch\n if (isServer()) {\n return null\n }\n\n return isMaybePresentation()\n })\n\n // Handle perspective changes from Studio\n const handlePerspectiveChange = useEffectEvent((perspective: ClientPerspective) => {\n // Sanitize perspective (filters out undefined/empty values from upstream bug)\n const cleanPerspective = sanitizePerspective(perspective)\n\n const formData = new FormData()\n formData.set(\n 'perspective',\n Array.isArray(cleanPerspective) ? cleanPerspective.join(',') : cleanPerspective,\n )\n submit(formData, {\n method: 'PUT',\n action,\n navigate: false,\n preventScrollReset: true,\n })\n\n // Always trigger refresh for perspective changes (server revalidation needed)\n refreshFn({\n source: 'manual',\n livePreviewEnabled: false, // Force server revalidation for perspective changes\n } as const)\n })\n\n const handleRefresh = useEffectEvent((payload: HistoryRefresh): false | Promise<void> => {\n // Prioritize userland refresh function if provided\n if (refresh) {\n return refresh(payload, () => refreshFn(payload), revalidator)\n }\n\n switch (payload.source) {\n case 'manual':\n // Always handle manual refresh events (like perspective changes)\n return refreshFn(payload)\n\n default:\n // For other refresh events, check if active loaders should handle them\n if (hasActiveLoaders) {\n return false // Let client loaders handle mutations (live mode active)\n }\n return refreshFn(payload) // Server revalidation for server-only setups\n }\n })\n\n // Listen for presentation events from Studio (only perspective changes needed for server revalidation)\n useEffect(() => {\n if (isServer() || !inStudioContext) return undefined\n\n const handleMessage = (event: MessageEvent) => {\n const {type, data} = event.data || {}\n\n // Only handle perspective changes - let enableVisualEditing handle refresh events\n if (type === 'presentation/perspective') {\n handlePerspectiveChange(data.perspective)\n }\n }\n\n window.addEventListener('message', handleMessage)\n\n return () => {\n window.removeEventListener('message', handleMessage)\n }\n }, [inStudioContext])\n\n // Enable visual editing overlays and interactions\n useEffect(() => {\n const disable = enableVisualEditing({\n components,\n zIndex,\n refresh: handleRefresh,\n history: historyAdapter,\n })\n\n return () => disable()\n }, [components, zIndex, historyAdapter])\n\n return null\n}\n\nexport default OverlaysClient\n"],"names":[],"mappings":"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"}
@@ -1,19 +1,25 @@
1
- import { useState, useCallback } from 'react';
1
+ import { useState, startTransition, useEffect, useCallback } from 'react';
2
2
  import { useRevalidator } from 'react-router';
3
+ import { useEffectEvent } from 'use-effect-event';
3
4
 
4
5
  function useRefresh() {
5
6
  const revalidator = useRevalidator();
6
7
  const [revalidatorPromise, setRevalidatorPromise] = useState(null);
7
8
  const [revalidatorLoading, setRevalidatorLoading] = useState(false);
8
- const handleRevalidatorState = useCallback(() => {
9
+ const handleRevalidatorState = useEffectEvent(() => {
9
10
  if (revalidatorPromise && revalidator.state === "loading") {
10
- setRevalidatorLoading(true);
11
+ startTransition(() => setRevalidatorLoading(true));
11
12
  } else if (revalidatorPromise && revalidatorLoading && revalidator.state === "idle") {
12
13
  revalidatorPromise();
13
- setRevalidatorPromise(null);
14
- setRevalidatorLoading(false);
14
+ startTransition(() => {
15
+ setRevalidatorPromise(null);
16
+ setRevalidatorLoading(false);
17
+ });
15
18
  }
16
- }, [revalidatorLoading, revalidator.state, revalidatorPromise]);
19
+ });
20
+ useEffect(() => {
21
+ handleRevalidatorState();
22
+ }, [revalidator.state]);
17
23
  const createRefreshHandler = useCallback(
18
24
  (customRefresh) => {
19
25
  return (payload) => {
@@ -32,9 +38,7 @@ function useRefresh() {
32
38
  [revalidator]
33
39
  );
34
40
  return {
35
- refreshHandler: createRefreshHandler,
36
- handleRevalidatorState,
37
- revalidatorState: revalidator.state
41
+ refreshHandler: createRefreshHandler
38
42
  };
39
43
  }
40
44
 
@@ -1 +1 @@
1
- {"version":3,"file":"refresh.js","sources":["../../src/visual-editing/hooks/refresh.ts"],"sourcesContent":["import type {HistoryRefresh} from '@sanity/visual-editing'\nimport {useCallback, useState} from 'react'\nimport {useRevalidator} from 'react-router'\n\nimport type {Revalidator} from '../types'\n\n/**\n * Hook that provides refresh logic for visual editing.\n * Integrates with React Router's revalidator for optimal data refetching.\n */\nexport function useRefresh(): {\n refreshHandler: (\n customRefresh?: (\n payload: HistoryRefresh,\n refreshDefault: () => false | Promise<void>,\n revalidator: Revalidator,\n ) => false | Promise<void>,\n ) => (payload: HistoryRefresh) => false | Promise<void>\n handleRevalidatorState: () => void\n revalidatorState: Revalidator['state']\n} {\n const revalidator = useRevalidator()\n const [revalidatorPromise, setRevalidatorPromise] = useState<(() => void) | null>(null)\n const [revalidatorLoading, setRevalidatorLoading] = useState(false)\n\n const handleRevalidatorState = useCallback(() => {\n if (revalidatorPromise && revalidator.state === 'loading') {\n setRevalidatorLoading(true)\n } else if (revalidatorPromise && revalidatorLoading && revalidator.state === 'idle') {\n revalidatorPromise()\n setRevalidatorPromise(null)\n setRevalidatorLoading(false)\n }\n }, [revalidatorLoading, revalidator.state, revalidatorPromise])\n\n const createRefreshHandler = useCallback(\n (\n customRefresh?: (\n payload: HistoryRefresh,\n refreshDefault: () => false | Promise<void>,\n revalidator: Revalidator,\n ) => false | Promise<void>,\n ) => {\n return (payload: HistoryRefresh) => {\n function refreshDefault() {\n // Only skip revalidation for mutations when client loaders are active\n if (payload.source === 'mutation' && payload.livePreviewEnabled) {\n // Client loaders (useQuery) will handle the update via comlink\n return false\n }\n\n // All other cases: revalidate\n // - Manual refreshes (user explicitly requested refresh)\n // - Mutations without client loaders (server-only setup needs revalidation)\n // - Unknown source types (fallback to revalidate for safety)\n return new Promise<void>((resolve) => {\n revalidator.revalidate()\n setRevalidatorPromise(() => resolve)\n })\n }\n\n return customRefresh\n ? customRefresh(payload, refreshDefault, revalidator)\n : refreshDefault()\n }\n },\n [revalidator],\n )\n\n return {\n refreshHandler: createRefreshHandler,\n handleRevalidatorState,\n revalidatorState: revalidator.state,\n }\n}\n"],"names":[],"mappings":";;;AAUO,SAAS,UAAA,GAUd;AACA,EAAA,MAAM,cAAc,cAAA,EAAe;AACnC,EAAA,MAAM,CAAC,kBAAA,EAAoB,qBAAqB,CAAA,GAAI,SAA8B,IAAI,CAAA;AACtF,EAAA,MAAM,CAAC,kBAAA,EAAoB,qBAAqB,CAAA,GAAI,SAAS,KAAK,CAAA;AAElE,EAAA,MAAM,sBAAA,GAAyB,YAAY,MAAM;AAC/C,IAAA,IAAI,kBAAA,IAAsB,WAAA,CAAY,KAAA,KAAU,SAAA,EAAW;AACzD,MAAA,qBAAA,CAAsB,IAAI,CAAA;AAAA,IAC5B,CAAA,MAAA,IAAW,kBAAA,IAAsB,kBAAA,IAAsB,WAAA,CAAY,UAAU,MAAA,EAAQ;AACnF,MAAA,kBAAA,EAAmB;AACnB,MAAA,qBAAA,CAAsB,IAAI,CAAA;AAC1B,MAAA,qBAAA,CAAsB,KAAK,CAAA;AAAA,IAC7B;AAAA,EACF,GAAG,CAAC,kBAAA,EAAoB,WAAA,CAAY,KAAA,EAAO,kBAAkB,CAAC,CAAA;AAE9D,EAAA,MAAM,oBAAA,GAAuB,WAAA;AAAA,IAC3B,CACE,aAAA,KAKG;AACH,MAAA,OAAO,CAAC,OAAA,KAA4B;AAClC,QAAA,SAAS,cAAA,GAAiB;AAExB,UAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,UAAA,IAAc,OAAA,CAAQ,kBAAA,EAAoB;AAE/D,YAAA,OAAO,KAAA;AAAA,UACT;AAMA,UAAA,OAAO,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACpC,YAAA,WAAA,CAAY,UAAA,EAAW;AACvB,YAAA,qBAAA,CAAsB,MAAM,OAAO,CAAA;AAAA,UACrC,CAAC,CAAA;AAAA,QACH;AAEA,QAAA,OAAO,gBACH,aAAA,CAAc,OAAA,EAAS,cAAA,EAAgB,WAAW,IAClD,cAAA,EAAe;AAAA,MACrB,CAAA;AAAA,IACF,CAAA;AAAA,IACA,CAAC,WAAW;AAAA,GACd;AAEA,EAAA,OAAO;AAAA,IACL,cAAA,EAAgB,oBAAA;AAAA,IAChB,sBAAA;AAAA,IACA,kBAAkB,WAAA,CAAY;AAAA,GAChC;AACF;;;;"}
1
+ {"version":3,"file":"refresh.js","sources":["../../src/visual-editing/hooks/refresh.ts"],"sourcesContent":["import type {HistoryRefresh} from '@sanity/visual-editing'\nimport {startTransition, useCallback, useEffect, useState} from 'react'\nimport {useRevalidator} from 'react-router'\nimport {useEffectEvent} from 'use-effect-event'\n\nimport type {Revalidator} from '../types'\n\n/**\n * Hook that provides refresh logic for visual editing.\n * Integrates with React Router's revalidator for optimal data refetching.\n */\nexport function useRefresh(): {\n refreshHandler: (\n customRefresh?: (\n payload: HistoryRefresh,\n refreshDefault: () => false | Promise<void>,\n revalidator: Revalidator,\n ) => false | Promise<void>,\n ) => (payload: HistoryRefresh) => false | Promise<void>\n} {\n const revalidator = useRevalidator()\n const [revalidatorPromise, setRevalidatorPromise] = useState<(() => void) | null>(null)\n const [revalidatorLoading, setRevalidatorLoading] = useState(false)\n\n // Handle revalidator state transitions internally\n const handleRevalidatorState = useEffectEvent(() => {\n if (revalidatorPromise && revalidator.state === 'loading') {\n startTransition(() => setRevalidatorLoading(true))\n } else if (revalidatorPromise && revalidatorLoading && revalidator.state === 'idle') {\n revalidatorPromise()\n startTransition(() => {\n setRevalidatorPromise(null)\n setRevalidatorLoading(false)\n })\n }\n })\n\n // Automatically handle revalidator state changes\n useEffect(() => {\n handleRevalidatorState()\n }, [revalidator.state])\n\n const createRefreshHandler = useCallback(\n (\n customRefresh?: (\n payload: HistoryRefresh,\n refreshDefault: () => false | Promise<void>,\n revalidator: Revalidator,\n ) => false | Promise<void>,\n ) => {\n return (payload: HistoryRefresh) => {\n function refreshDefault() {\n // Only skip revalidation for mutations when client loaders are active\n if (payload.source === 'mutation' && payload.livePreviewEnabled) {\n // Client loaders (useQuery) will handle the update via comlink\n return false\n }\n\n // All other cases: revalidate\n // - Manual refreshes (user explicitly requested refresh)\n // - Mutations without client loaders (server-only setup needs revalidation)\n // - Unknown source types (fallback to revalidate for safety)\n return new Promise<void>((resolve) => {\n revalidator.revalidate()\n setRevalidatorPromise(() => resolve)\n })\n }\n\n return customRefresh\n ? customRefresh(payload, refreshDefault, revalidator)\n : refreshDefault()\n }\n },\n [revalidator],\n )\n\n return {\n refreshHandler: createRefreshHandler,\n }\n}\n"],"names":[],"mappings":";;;;AAWO,SAAS,UAAA,GAQd;AACA,EAAA,MAAM,cAAc,cAAA,EAAe;AACnC,EAAA,MAAM,CAAC,kBAAA,EAAoB,qBAAqB,CAAA,GAAI,SAA8B,IAAI,CAAA;AACtF,EAAA,MAAM,CAAC,kBAAA,EAAoB,qBAAqB,CAAA,GAAI,SAAS,KAAK,CAAA;AAGlE,EAAA,MAAM,sBAAA,GAAyB,eAAe,MAAM;AAClD,IAAA,IAAI,kBAAA,IAAsB,WAAA,CAAY,KAAA,KAAU,SAAA,EAAW;AACzD,MAAA,eAAA,CAAgB,MAAM,qBAAA,CAAsB,IAAI,CAAC,CAAA;AAAA,IACnD,CAAA,MAAA,IAAW,kBAAA,IAAsB,kBAAA,IAAsB,WAAA,CAAY,UAAU,MAAA,EAAQ;AACnF,MAAA,kBAAA,EAAmB;AACnB,MAAA,eAAA,CAAgB,MAAM;AACpB,QAAA,qBAAA,CAAsB,IAAI,CAAA;AAC1B,QAAA,qBAAA,CAAsB,KAAK,CAAA;AAAA,MAC7B,CAAC,CAAA;AAAA,IACH;AAAA,EACF,CAAC,CAAA;AAGD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,sBAAA,EAAuB;AAAA,EACzB,CAAA,EAAG,CAAC,WAAA,CAAY,KAAK,CAAC,CAAA;AAEtB,EAAA,MAAM,oBAAA,GAAuB,WAAA;AAAA,IAC3B,CACE,aAAA,KAKG;AACH,MAAA,OAAO,CAAC,OAAA,KAA4B;AAClC,QAAA,SAAS,cAAA,GAAiB;AAExB,UAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,UAAA,IAAc,OAAA,CAAQ,kBAAA,EAAoB;AAE/D,YAAA,OAAO,KAAA;AAAA,UACT;AAMA,UAAA,OAAO,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACpC,YAAA,WAAA,CAAY,UAAA,EAAW;AACvB,YAAA,qBAAA,CAAsB,MAAM,OAAO,CAAA;AAAA,UACrC,CAAC,CAAA;AAAA,QACH;AAEA,QAAA,OAAO,gBACH,aAAA,CAAc,OAAA,EAAS,cAAA,EAAgB,WAAW,IAClD,cAAA,EAAe;AAAA,MACrB,CAAA;AAAA,IACF,CAAA;AAAA,IACA,CAAC,WAAW;AAAA,GACd;AAEA,EAAA,OAAO;AAAA,IACL,cAAA,EAAgB;AAAA,GAClB;AACF;;;;"}
@@ -1,4 +1,5 @@
1
1
  import { validateApiPerspective } from '@sanity/client';
2
+ import { urlSearchParamPreviewPerspective } from '@sanity/preview-url-secret/constants';
2
3
 
3
4
  async function sha256(message) {
4
5
  const messageBuffer = await new TextEncoder().encode(message);
@@ -13,7 +14,12 @@ function hashQuery(query, params) {
13
14
  return sha256(hash);
14
15
  }
15
16
  function sanitizePerspective(perspective) {
16
- const sanitizedPerspective = typeof perspective === "string" && perspective.includes(",") ? perspective.split(",") : perspective;
17
+ let sanitizedPerspective = typeof perspective === "string" && perspective.includes(",") ? perspective.split(",") : perspective;
18
+ if (Array.isArray(sanitizedPerspective)) {
19
+ sanitizedPerspective = sanitizedPerspective.filter(
20
+ (p) => typeof p === "string" && p.length > 0
21
+ );
22
+ }
17
23
  validateApiPerspective(sanitizedPerspective);
18
24
  return sanitizedPerspective === "raw" ? "drafts" : sanitizedPerspective;
19
25
  }
@@ -27,10 +33,20 @@ function supportsPerspectiveStack(apiVersion) {
27
33
  return versionDate >= cutoffDate;
28
34
  }
29
35
  function getPerspective(session) {
30
- const perspective = session.get("perspective")?.split(",");
36
+ const perspective = session.get("perspective")?.split(",").filter((p) => p.length > 0);
31
37
  validateApiPerspective(perspective);
32
38
  return perspective;
33
39
  }
40
+ function getPerspectiveFromUrl(url) {
41
+ try {
42
+ const parsed = typeof url === "string" ? new URL(url) : url;
43
+ const param = parsed.searchParams.get(urlSearchParamPreviewPerspective);
44
+ if (!param) return void 0;
45
+ return sanitizePerspective(param);
46
+ } catch {
47
+ return void 0;
48
+ }
49
+ }
34
50
  function isSanityPreviewSession(session) {
35
51
  return isHydrogenSession(session) && "has" in session && typeof session.has === "function" && "destroy" in session && typeof session.destroy === "function";
36
52
  }
@@ -41,5 +57,5 @@ function isServer() {
41
57
  return typeof document === "undefined";
42
58
  }
43
59
 
44
- export { getPerspective, hashQuery, isHydrogenSession, isSanityPreviewSession, isServer, sanitizePerspective, supportsPerspectiveStack };
60
+ export { getPerspective, getPerspectiveFromUrl, hashQuery, isHydrogenSession, isSanityPreviewSession, isServer, sanitizePerspective, supportsPerspectiveStack };
45
61
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import {\n type ClientPerspective,\n type QueryParams,\n type QueryWithoutParams,\n validateApiPerspective,\n} from '@sanity/client'\nimport type {HydrogenSession} from '@shopify/hydrogen'\n\nimport type {SanityPreviewSession} from './preview/session'\n\n/**\n * Create an SHA-256 hash as a hex string\n * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string\n */\nexport async function sha256(message: string): Promise<string> {\n // encode as UTF-8\n const messageBuffer = await new TextEncoder().encode(message)\n // hash the message\n const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer)\n // convert bytes to hex string\n return Array.from(new Uint8Array(hashBuffer))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Hash query and its parameters for use as cache key.\n * NOTE: Oxygen deployment will break if the cache key is long or contains `\\n`\n */\nexport function hashQuery(\n query: string,\n params: QueryParams | QueryWithoutParams,\n): Promise<string> {\n let hash = query\n\n if (params) {\n hash += JSON.stringify(params)\n }\n\n return sha256(hash)\n}\n\n/**\n * Sanitizes and validates a perspective value.\n * Handles both string (comma-separated) and array formats.\n */\nexport function sanitizePerspective(perspective: unknown): Exclude<ClientPerspective, 'raw'> {\n const sanitizedPerspective =\n typeof perspective === 'string' && perspective.includes(',')\n ? perspective.split(',')\n : perspective\n\n validateApiPerspective(sanitizedPerspective)\n\n return sanitizedPerspective === 'raw' ? 'drafts' : sanitizedPerspective\n}\n\n/**\n * Check if API version supports perspective stack (v2025-02-19 or later)\n * Special versions: '1' doesn't support perspectives, 'X' does support perspectives\n */\nexport function supportsPerspectiveStack(apiVersion: string): boolean {\n // Special cases\n if (apiVersion === '1') return false\n if (apiVersion === 'X') return true\n\n // Normalize version by removing 'v' prefix if present\n const normalizedVersion = `${apiVersion}`.replace(/^v/, '')\n\n // Parse date format: 2025-02-19\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(normalizedVersion)) return false\n\n const versionDate = new Date(normalizedVersion)\n const cutoffDate = new Date('2025-02-19')\n\n return versionDate >= cutoffDate\n}\n\n/**\n * Extracts and validates the perspective from a session.\n */\nexport function getPerspective(session: SanityPreviewSession | HydrogenSession): ClientPerspective {\n const perspective = session.get('perspective')?.split(',')\n validateApiPerspective(perspective)\n return perspective\n}\n\n/**\n * Type guard that checks if a session object is a SanityPreviewSession.\n * Validates presence of required methods: has, destroy (in addition to Hydrogen session methods).\n */\nexport function isSanityPreviewSession(session: unknown): session is SanityPreviewSession {\n return (\n isHydrogenSession(session) &&\n 'has' in session &&\n typeof session.has === 'function' &&\n 'destroy' in session &&\n typeof session.destroy === 'function'\n )\n}\n\n/**\n * Type guard that checks if a session object is a valid Hydrogen session.\n * Validates presence of required methods: get, set, unset, commit.\n */\nexport function isHydrogenSession(session: unknown): session is HydrogenSession {\n return (\n !!session &&\n typeof session === 'object' &&\n 'get' in session &&\n typeof session.get === 'function' &&\n 'set' in session &&\n typeof session.set === 'function' &&\n 'unset' in session &&\n typeof session.unset === 'function' &&\n 'commit' in session &&\n typeof session.commit === 'function'\n )\n}\n\n/**\n * Utility function that detects if code is running on the server.\n * Used for SSR safety and preventing client-only code from running on server.\n */\nexport function isServer(): boolean {\n return typeof document === 'undefined'\n}\n"],"names":[],"mappings":";;AAcA,eAAsB,OAAO,OAAA,EAAkC;AAE7D,EAAA,MAAM,gBAAgB,MAAM,IAAI,WAAA,EAAY,CAAE,OAAO,OAAO,CAAA;AAE5D,EAAA,MAAM,aAAa,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,WAAW,aAAa,CAAA;AAEtE,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,UAAA,CAAW,UAAU,CAAC,CAAA,CACzC,IAAI,CAAC,CAAA,KAAM,EAAE,QAAA,CAAS,EAAE,EAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA,CAC1C,KAAK,EAAE,CAAA;AACZ;AAMO,SAAS,SAAA,CACd,OACA,MAAA,EACiB;AACjB,EAAA,IAAI,IAAA,GAAO,KAAA;AAEX,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,IAAA,IAAQ,IAAA,CAAK,UAAU,MAAM,CAAA;AAAA,EAC/B;AAEA,EAAA,OAAO,OAAO,IAAI,CAAA;AACpB;AAMO,SAAS,oBAAoB,WAAA,EAAyD;AAC3F,EAAA,MAAM,oBAAA,GACJ,OAAO,WAAA,KAAgB,QAAA,IAAY,WAAA,CAAY,QAAA,CAAS,GAAG,CAAA,GACvD,WAAA,CAAY,KAAA,CAAM,GAAG,CAAA,GACrB,WAAA;AAEN,EAAA,sBAAA,CAAuB,oBAAoB,CAAA;AAE3C,EAAA,OAAO,oBAAA,KAAyB,QAAQ,QAAA,GAAW,oBAAA;AACrD;AAMO,SAAS,yBAAyB,UAAA,EAA6B;AAEpE,EAAA,IAAI,UAAA,KAAe,KAAK,OAAO,KAAA;AAC/B,EAAA,IAAI,UAAA,KAAe,KAAK,OAAO,IAAA;AAG/B,EAAA,MAAM,oBAAoB,CAAA,EAAG,UAAU,CAAA,CAAA,CAAG,OAAA,CAAQ,MAAM,EAAE,CAAA;AAG1D,EAAA,IAAI,CAAC,qBAAA,CAAsB,IAAA,CAAK,iBAAiB,GAAG,OAAO,KAAA;AAE3D,EAAA,MAAM,WAAA,GAAc,IAAI,IAAA,CAAK,iBAAiB,CAAA;AAC9C,EAAA,MAAM,UAAA,mBAAa,IAAI,IAAA,CAAK,YAAY,CAAA;AAExC,EAAA,OAAO,WAAA,IAAe,UAAA;AACxB;AAKO,SAAS,eAAe,OAAA,EAAoE;AACjG,EAAA,MAAM,cAAc,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAA,EAAG,MAAM,GAAG,CAAA;AACzD,EAAA,sBAAA,CAAuB,WAAW,CAAA;AAClC,EAAA,OAAO,WAAA;AACT;AAMO,SAAS,uBAAuB,OAAA,EAAmD;AACxF,EAAA,OACE,iBAAA,CAAkB,OAAO,CAAA,IACzB,KAAA,IAAS,OAAA,IACT,OAAO,OAAA,CAAQ,GAAA,KAAQ,UAAA,IACvB,SAAA,IAAa,OAAA,IACb,OAAO,QAAQ,OAAA,KAAY,UAAA;AAE/B;AAMO,SAAS,kBAAkB,OAAA,EAA8C;AAC9E,EAAA,OACE,CAAC,CAAC,OAAA,IACF,OAAO,OAAA,KAAY,QAAA,IACnB,KAAA,IAAS,OAAA,IACT,OAAO,OAAA,CAAQ,GAAA,KAAQ,UAAA,IACvB,KAAA,IAAS,OAAA,IACT,OAAO,OAAA,CAAQ,GAAA,KAAQ,UAAA,IACvB,OAAA,IAAW,OAAA,IACX,OAAO,OAAA,CAAQ,KAAA,KAAU,UAAA,IACzB,QAAA,IAAY,OAAA,IACZ,OAAO,OAAA,CAAQ,MAAA,KAAW,UAAA;AAE9B;AAMO,SAAS,QAAA,GAAoB;AAClC,EAAA,OAAO,OAAO,QAAA,KAAa,WAAA;AAC7B;;;;"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import {\n type ClientPerspective,\n type QueryParams,\n type QueryWithoutParams,\n validateApiPerspective,\n} from '@sanity/client'\nimport {urlSearchParamPreviewPerspective} from '@sanity/preview-url-secret/constants'\nimport type {HydrogenSession} from '@shopify/hydrogen'\n\nimport type {SanityPreviewSession} from './preview/session'\n\n/**\n * Create an SHA-256 hash as a hex string\n * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string\n */\nexport async function sha256(message: string): Promise<string> {\n // encode as UTF-8\n const messageBuffer = await new TextEncoder().encode(message)\n // hash the message\n const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer)\n // convert bytes to hex string\n return Array.from(new Uint8Array(hashBuffer))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Hash query and its parameters for use as cache key.\n * NOTE: Oxygen deployment will break if the cache key is long or contains `\\n`\n */\nexport function hashQuery(\n query: string,\n params: QueryParams | QueryWithoutParams,\n): Promise<string> {\n let hash = query\n\n if (params) {\n hash += JSON.stringify(params)\n }\n\n return sha256(hash)\n}\n\n/**\n * Sanitizes and validates a perspective value.\n * Handles both string (comma-separated) and array formats.\n */\nexport function sanitizePerspective(perspective: unknown): Exclude<ClientPerspective, 'raw'> {\n let sanitizedPerspective =\n typeof perspective === 'string' && perspective.includes(',')\n ? perspective.split(',')\n : perspective\n\n // Filter out empty strings and undefined values from perspective array\n if (Array.isArray(sanitizedPerspective)) {\n sanitizedPerspective = sanitizedPerspective.filter(\n (p): p is string => typeof p === 'string' && p.length > 0,\n )\n }\n\n validateApiPerspective(sanitizedPerspective)\n\n return sanitizedPerspective === 'raw' ? 'drafts' : sanitizedPerspective\n}\n\n/**\n * Check if API version supports perspective stack (v2025-02-19 or later)\n * Special versions: '1' doesn't support perspectives, 'X' does support perspectives\n */\nexport function supportsPerspectiveStack(apiVersion: string): boolean {\n // Special cases\n if (apiVersion === '1') return false\n if (apiVersion === 'X') return true\n\n // Normalize version by removing 'v' prefix if present\n const normalizedVersion = `${apiVersion}`.replace(/^v/, '')\n\n // Parse date format: 2025-02-19\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(normalizedVersion)) return false\n\n const versionDate = new Date(normalizedVersion)\n const cutoffDate = new Date('2025-02-19')\n\n return versionDate >= cutoffDate\n}\n\n/**\n * Extracts and validates the perspective from a session.\n */\nexport function getPerspective(session: SanityPreviewSession | HydrogenSession): ClientPerspective {\n const perspective = session\n .get('perspective')\n ?.split(',')\n .filter((p: string) => p.length > 0)\n validateApiPerspective(perspective)\n return perspective\n}\n\n/**\n * Reads the `sanity-preview-perspective` URL search param and validates it.\n * Returns `undefined` if absent or invalid, so callers can fall back to the session.\n */\nexport function getPerspectiveFromUrl(url: URL | string): ClientPerspective | undefined {\n try {\n const parsed = typeof url === 'string' ? new URL(url) : url\n const param = parsed.searchParams.get(urlSearchParamPreviewPerspective)\n if (!param) return undefined\n return sanitizePerspective(param)\n } catch {\n return undefined\n }\n}\n\n/**\n * Type guard that checks if a session object is a SanityPreviewSession.\n * Validates presence of required methods: has, destroy (in addition to Hydrogen session methods).\n */\nexport function isSanityPreviewSession(session: unknown): session is SanityPreviewSession {\n return (\n isHydrogenSession(session) &&\n 'has' in session &&\n typeof session.has === 'function' &&\n 'destroy' in session &&\n typeof session.destroy === 'function'\n )\n}\n\n/**\n * Type guard that checks if a session object is a valid Hydrogen session.\n * Validates presence of required methods: get, set, unset, commit.\n */\nexport function isHydrogenSession(session: unknown): session is HydrogenSession {\n return (\n !!session &&\n typeof session === 'object' &&\n 'get' in session &&\n typeof session.get === 'function' &&\n 'set' in session &&\n typeof session.set === 'function' &&\n 'unset' in session &&\n typeof session.unset === 'function' &&\n 'commit' in session &&\n typeof session.commit === 'function'\n )\n}\n\n/**\n * Utility function that detects if code is running on the server.\n * Used for SSR safety and preventing client-only code from running on server.\n */\nexport function isServer(): boolean {\n return typeof document === 'undefined'\n}\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {Any} from '@sanity/client'
2
2
  import {CachingStrategy} from '@shopify/hydrogen'
3
3
  import {ClientConfig} from '@sanity/client'
4
+ import {ClientPerspective} from '@sanity/client'
4
5
  import {ClientReturn} from '@sanity/client'
5
6
  import {createDataAttribute} from '@sanity/react-loader'
6
7
  import {EncodeDataAttributeFunction} from '@sanity/core-loader/encode-data-attribute'
@@ -86,6 +87,12 @@ declare type FetchOptions<T> = HydrogenResponseQueryOptions & {
86
87
  }
87
88
  }
88
89
 
90
+ /**
91
+ * Reads the `sanity-preview-perspective` URL search param and validates it.
92
+ * Returns `undefined` if absent or invalid, so callers can fall back to the session.
93
+ */
94
+ export declare function getPerspectiveFromUrl(url: URL | string): ClientPerspective | undefined
95
+
89
96
  declare type HydrogenResponseQueryOptions = Omit<ResponseQueryOptions, 'next' | 'cache'> & {
90
97
  hydrogen?: 'hydrogen' extends keyof RequestInit_2 ? RequestInit_2['hydrogen'] : never
91
98
  }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { createElement, useMemo, lazy, Suspense, useSyncExternalStore, useId, us
4
4
  import { isPreviewEnabled, usePreviewMode } from './preview/index.js';
5
5
  import { SanityProvider, useSanityProviderValue } from './_chunks-es/provider.js';
6
6
  export { Sanity } from './_chunks-es/provider.js';
7
- import { supportsPerspectiveStack, getPerspective, hashQuery, isServer } from './_chunks-es/utils.js';
7
+ import { getPerspectiveFromUrl, supportsPerspectiveStack, getPerspective, hashQuery, isServer } from './_chunks-es/utils.js';
8
8
  import { createImageUrlBuilder } from '@sanity/image-url';
9
9
  import { jsx } from 'react/jsx-runtime';
10
10
  import { useQuery as useQuery$1 } from '@sanity/react-loader';
@@ -17,7 +17,6 @@ const DEFAULT_CACHE_STRATEGY = CacheLong();
17
17
  let didWarnAboutNoApiVersion = false;
18
18
  let didWarnAboutNoPerspectiveSupport = false;
19
19
  let didWarnAboutLoadQuery = false;
20
- let didInitializeLoader = false;
21
20
  async function createSanityContext(options) {
22
21
  const { cache, waitUntil = () => Promise.resolve(), request, preview, defaultStrategy } = options;
23
22
  const withCache = cache ? createWithCache({ cache, waitUntil, request }) : null;
@@ -43,7 +42,10 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
43
42
  if (previewEnabled) {
44
43
  const apiVersion2 = client.config().apiVersion;
45
44
  let perspective;
46
- if (supportsPerspectiveStack(apiVersion2)) {
45
+ const urlPerspective = getPerspectiveFromUrl(request.url);
46
+ if (urlPerspective !== void 0 && !(Array.isArray(urlPerspective) && !supportsPerspectiveStack(apiVersion2))) {
47
+ perspective = urlPerspective;
48
+ } else if (supportsPerspectiveStack(apiVersion2)) {
47
49
  perspective = getPerspective(preview.session);
48
50
  } else {
49
51
  if (process.env.NODE_ENV === "development" && !didWarnAboutNoPerspectiveSupport) {
@@ -77,11 +79,8 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
77
79
  * Bypasses Hydrogen cache in preview mode.
78
80
  */
79
81
  async loadQuery(query, params, loaderOptions) {
80
- if (!didInitializeLoader) {
81
- const { setServerClient } = await import('@sanity/react-loader');
82
- setServerClient(client);
83
- didInitializeLoader = true;
84
- }
82
+ const { setServerClient } = await import('@sanity/react-loader');
83
+ setServerClient(client);
85
84
  if (!previewEnabled && process.env.NODE_ENV === "development" && !didWarnAboutLoadQuery) {
86
85
  console.warn(
87
86
  `\`loadQuery\` is being called outside of preview mode. Consider using \`query\` instead, which automatically handles both preview and production modes efficiently, or use \`fetch\`. \`loadQuery\` is intended to be called conditionally in preview and visual editing contexts.`
@@ -90,7 +89,8 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
90
89
  }
91
90
  if (!withCache || previewEnabled) {
92
91
  const { loadQuery } = await import('@sanity/react-loader');
93
- return await loadQuery(query, params, loaderOptions);
92
+ const resolvedOptions = previewEnabled && !loaderOptions?.perspective ? { ...loaderOptions, perspective: client.config().perspective } : loaderOptions;
93
+ return await loadQuery(query, params, resolvedOptions);
94
94
  }
95
95
  const cacheStrategy = loaderOptions?.hydrogen?.cache || defaultStrategy || DEFAULT_CACHE_STRATEGY;
96
96
  const queryHash = await hashQuery(query, params);
@@ -226,5 +226,5 @@ function useQuery(query, params, options) {
226
226
  return useQuery$1(query, params, options);
227
227
  }
228
228
 
229
- export { DEFAULT_API_VERSION, DEFAULT_CACHE_STRATEGY, Query, createSanityContext, useImageUrl, useImageUrlBuilder, useQuery, useSanityProviderValue };
229
+ export { DEFAULT_API_VERSION, DEFAULT_CACHE_STRATEGY, Query, createSanityContext, getPerspectiveFromUrl, useImageUrl, useImageUrlBuilder, useQuery, useSanityProviderValue };
230
230
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/constants.ts","../src/context.ts","../src/image.ts","../src/Query.tsx","../src/visual-editing/useQuery.tsx"],"sourcesContent":["import {CacheLong, type CachingStrategy} from '@shopify/hydrogen'\n\n/** Default Sanity API version with perspective stack support */\nexport const DEFAULT_API_VERSION = 'v2025-02-19'\n\n/** Default Hydrogen caching strategy for Sanity queries */\nexport const DEFAULT_CACHE_STRATEGY: CachingStrategy = CacheLong()\n","import {\n type Any,\n type ClientConfig,\n type ClientPerspective,\n type ClientReturn,\n createClient,\n type QueryParams,\n type QueryWithoutParams,\n type ResponseQueryOptions,\n SanityClient,\n} from '@sanity/client'\nimport type {QueryResponseInitial} from '@sanity/react-loader'\nimport {type CachingStrategy, createWithCache, type HydrogenSession} from '@shopify/hydrogen'\nimport {createElement, type PropsWithChildren, type ReactNode} from 'react'\n\nimport {DEFAULT_API_VERSION, DEFAULT_CACHE_STRATEGY} from './constants'\nimport type {SanityPreviewSession} from './preview/session'\nimport {isPreviewEnabled} from './preview/utils'\nimport {SanityProvider, type SanityProviderValue} from './provider'\nimport type {CacheActionFunctionParam, WaitUntil} from './types'\nimport {getPerspective} from './utils'\nimport {hashQuery, supportsPerspectiveStack} from './utils'\n\nlet didWarnAboutNoApiVersion = false\nlet didWarnAboutNoPerspectiveSupport = false\nlet didWarnAboutLoadQuery = false\nlet didInitializeLoader = false\n\nexport type CreateSanityContextOptions = {\n request: Request\n\n cache?: Cache | undefined\n waitUntil?: WaitUntil | undefined\n\n /**\n * Sanity client or configuration to use.\n */\n client: SanityClient | ClientConfig\n\n /**\n * The default caching strategy to use for `loadQuery` subrequests.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies\n *\n * Defaults to `CacheLong`\n */\n defaultStrategy?: CachingStrategy | null\n\n /**\n * Configuration for enabling preview mode.\n */\n preview?: {\n token: string\n session: SanityPreviewSession | HydrogenSession\n }\n}\n\ninterface RequestInit {\n hydrogen?: {\n /**\n * The caching strategy to use for the subrequest.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies\n */\n cache?: CachingStrategy\n\n /**\n * Optional debugging information to be displayed in the subrequest profiler.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request\n */\n debug?: {\n displayName: string\n }\n }\n}\n\ntype HydrogenResponseQueryOptions = Omit<ResponseQueryOptions, 'next' | 'cache'> & {\n hydrogen?: 'hydrogen' extends keyof RequestInit ? RequestInit['hydrogen'] : never\n}\n\nexport type LoadQueryOptions<T> = Pick<\n HydrogenResponseQueryOptions,\n 'perspective' | 'hydrogen' | 'useCdn' | 'stega' | 'headers' | 'tag'\n> & {\n hydrogen?: {\n /**\n * The caching strategy to use for the subrequest.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies\n */\n cache?: CachingStrategy\n\n /**\n * Optional debugging information to be displayed in the subrequest profiler.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request\n */\n debug?: {\n displayName: string\n }\n\n /**\n * Whether to cache the result of the query or not.\n * @defaultValue () => true\n */\n shouldCacheResult?: (value: QueryResponseInitial<T>) => boolean\n }\n}\n\nexport type FetchOptions<T> = HydrogenResponseQueryOptions & {\n hydrogen?: {\n /**\n * The caching strategy to use for the subrequest.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies\n */\n cache?: CachingStrategy\n\n /**\n * Optional debugging information to be displayed in the subrequest profiler.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request\n */\n debug?: {\n displayName: string\n }\n\n /**\n * Whether to cache the result of the query or not.\n * @defaultValue () => true\n */\n shouldCacheResult?: (value: QueryResponseInitial<T>) => boolean\n }\n}\n\nexport interface SanityContext {\n /**\n * Query Sanity using the loader.\n * @see https://www.sanity.io/docs/loaders\n */\n loadQuery<Result = Any, Query extends string = string>(\n query: Query,\n params?: QueryParams | QueryWithoutParams,\n options?: LoadQueryOptions<ClientReturn<Query, Result>>,\n ): Promise<QueryResponseInitial<ClientReturn<Query, Result>>>\n\n /**\n * Query Sanity using direct client fetch with Hydrogen caching.\n * Use this when you need direct client results without react-loader integration.\n * Automatically disables caching in preview mode for real-time updates.\n */\n fetch<Result = Any, Query extends string = string>(\n query: Query,\n params?: QueryParams | QueryWithoutParams,\n options?: FetchOptions<Result>,\n ): Promise<ClientReturn<Query, Result>>\n\n /**\n * Conditionally query Sanity using either loadQuery (for preview mode) or fetch (for static mode).\n * This optimizes bundle size by only loading @sanity/react-loader dependencies when in preview mode.\n */\n query<Result = Any, Query extends string = string>(\n query: Query,\n params?: QueryParams | QueryWithoutParams,\n options?: LoadQueryOptions<ClientReturn<Query, Result>> & FetchOptions<Result>,\n ): Promise<QueryResponseInitial<ClientReturn<Query, Result>> | ClientReturn<Query, Result>>\n\n /**\n * The Sanity client, automatically configured for preview mode when enabled.\n * Uses preview token, perspective, and CDN settings based on session state.\n */\n client: SanityClient\n\n preview?: CreateSanityContextOptions['preview'] & {\n /**\n * Whether preview mode is currently enabled based on session detection\n */\n enabled: boolean\n }\n\n SanityProvider: (props: PropsWithChildren<object>) => ReactNode\n}\n\n/**\n * @public\n */\nexport async function createSanityContext(\n options: CreateSanityContextOptions,\n): Promise<SanityContext> {\n const {cache, waitUntil = () => Promise.resolve(), request, preview, defaultStrategy} = options\n const withCache = cache ? createWithCache({cache, waitUntil, request}) : null\n let client =\n options.client instanceof SanityClient ? options.client : createClient(options.client)\n\n if (client.config().apiVersion === '1') {\n if (process.env.NODE_ENV === 'development' && !didWarnAboutNoApiVersion) {\n console.warn(\n `\nNo API version specified, defaulting to \\`${DEFAULT_API_VERSION}\\` which supports perspectives and Content Releases.\nYou can find the latest version in the Sanity changelog: https://www.sanity.io/changelog.\n `.trim(),\n )\n\n didWarnAboutNoApiVersion = true\n }\n\n client = client.withConfig({apiVersion: DEFAULT_API_VERSION})\n }\n\n // Determine if preview is enabled and configure the client accordingly\n let previewEnabled = false\n if (preview) {\n if (!preview.token) {\n throw new Error('Enabling preview mode requires a token.')\n }\n\n previewEnabled = isPreviewEnabled(client.config().projectId!, preview.session)\n\n if (previewEnabled) {\n const apiVersion = client.config().apiVersion\n let perspective: ClientPerspective\n if (supportsPerspectiveStack(apiVersion)) {\n perspective = getPerspective(preview.session)\n } else {\n if (process.env.NODE_ENV === 'development' && !didWarnAboutNoPerspectiveSupport) {\n console.warn(\n `API version \\`${apiVersion}\\` does not support perspective stacks. Using \\`previewDrafts\\` perspective. Consider upgrading to \\`v2025-02-19\\` or later for full perspective support.`,\n )\n\n didWarnAboutNoPerspectiveSupport = true\n }\n perspective = 'previewDrafts'\n }\n\n client = client.withConfig({\n useCdn: false,\n token: preview.token,\n perspective,\n })\n }\n }\n\n // Server client will be initialized lazily on first loadQuery call\n const {apiHost, projectId, dataset, apiVersion} = client.config()\n const providerValue: SanityProviderValue = {\n projectId: projectId!,\n dataset: dataset!,\n apiHost,\n apiVersion: apiVersion!,\n previewEnabled,\n perspective: client.config().perspective || 'published',\n stegaEnabled: client.config().stega?.enabled ?? false,\n }\n\n return {\n /**\n * Loads a Sanity query with client-side loader support and Hydrogen cache integration.\n * Bypasses Hydrogen cache in preview mode.\n */\n async loadQuery<Result = Any, Query extends string = string>(\n query: Query,\n params: QueryParams | QueryWithoutParams,\n loaderOptions?: LoadQueryOptions<ClientReturn<Query, Result>>,\n ): Promise<QueryResponseInitial<ClientReturn<Query, Result>>> {\n // Lazy initialize the loader on first call with the configured client\n if (!didInitializeLoader) {\n const {setServerClient} = await import('@sanity/react-loader')\n setServerClient(client)\n didInitializeLoader = true\n }\n\n // Warn users to migrate to `query` method when using loadQuery outside preview mode\n if (!previewEnabled && process.env.NODE_ENV === 'development' && !didWarnAboutLoadQuery) {\n console.warn(\n `\\`loadQuery\\` is being called outside of preview mode. Consider using \\`query\\` instead, which automatically handles both preview and production modes efficiently, or use \\`fetch\\`. \\`loadQuery\\` is intended to be called conditionally in preview and visual editing contexts.`,\n )\n didWarnAboutLoadQuery = true\n }\n\n if (!withCache || previewEnabled) {\n const {loadQuery} = await import('@sanity/react-loader')\n return await loadQuery<ClientReturn<Query, Result>>(query, params, loaderOptions)\n }\n\n const cacheStrategy =\n loaderOptions?.hydrogen?.cache || defaultStrategy || DEFAULT_CACHE_STRATEGY\n const queryHash = await hashQuery(query, params)\n const shouldCacheResult = loaderOptions?.hydrogen?.shouldCacheResult ?? (() => true)\n\n return await withCache.run(\n {cacheKey: queryHash, cacheStrategy, shouldCacheResult},\n async ({\n addDebugData,\n }: CacheActionFunctionParam): Promise<\n QueryResponseInitial<ClientReturn<Query, Result>>\n > => {\n // Name displayed in the subrequest profiler\n const displayName = loaderOptions?.hydrogen?.debug?.displayName || 'query Sanity'\n\n addDebugData({\n displayName,\n })\n\n const {loadQuery} = await import('@sanity/react-loader')\n return await loadQuery<ClientReturn<Query, Result>>(query, params, loaderOptions)\n },\n )\n },\n\n /**\n * Executes a Sanity query with Hydrogen cache integration.\n * Direct client fetch without loader integration. Bypasses cache in preview mode.\n */\n async fetch<Result = Any, Query extends string = string>(\n query: Query,\n params: QueryParams | QueryWithoutParams = {},\n fetchOptions?: Pick<\n LoadQueryOptions<Result>,\n 'perspective' | 'hydrogen' | 'useCdn' | 'headers' | 'tag'\n >,\n ): Promise<ClientReturn<Query, Result>> {\n if (!withCache || previewEnabled) {\n return await client.fetch<ClientReturn<Query, Result>>(query, params, fetchOptions)\n }\n\n const cacheStrategy =\n fetchOptions?.hydrogen?.cache || defaultStrategy || DEFAULT_CACHE_STRATEGY\n const queryHash = await hashQuery(query, params)\n\n return await withCache.run(\n {cacheKey: queryHash, cacheStrategy, shouldCacheResult: () => true},\n async ({addDebugData}: CacheActionFunctionParam): Promise<ClientReturn<Query, Result>> => {\n // Name displayed in the subrequest profiler\n const displayName = fetchOptions?.hydrogen?.debug?.displayName || 'fetch Sanity'\n\n addDebugData({\n displayName,\n })\n\n return await client.fetch<ClientReturn<Query, Result>>(query, params, fetchOptions)\n },\n )\n },\n\n /**\n * Automatic query method that automatically adapts based on preview mode state.\n * Uses `loadQuery` (with client-side loader integration) when preview is enabled, `fetch` otherwise.\n * Bypasses cache in preview mode.\n */\n async query<Result = Any, Query extends string = string>(\n query: Query,\n params?: QueryParams | QueryWithoutParams,\n queryOptions?: LoadQueryOptions<ClientReturn<Query, Result>> & FetchOptions<Result>,\n ): Promise<QueryResponseInitial<ClientReturn<Query, Result>> | ClientReturn<Query, Result>> {\n return await (previewEnabled ? this.loadQuery : this.fetch)(query, params, queryOptions)\n },\n\n /** The configured Sanity client instance */\n client,\n\n /** Preview configuration with session-based state, undefined when preview is not configured */\n preview: preview ? {...preview, enabled: previewEnabled} : undefined,\n\n /**\n * React Provider component that serializes Sanity configuration across server-client boundary.\n */\n SanityProvider({children}: PropsWithChildren<object>) {\n return createElement(\n SanityProvider,\n {\n value: Object.freeze(providerValue),\n },\n children,\n )\n },\n } satisfies SanityContext\n}\n","import type {ImageUrlBuilder, SanityImageSource, SanityModernClientLike} from '@sanity/image-url'\nimport {createImageUrlBuilder} from '@sanity/image-url'\nimport {useMemo} from 'react'\n\nimport {useSanityProviderValue} from './provider'\n\n/**\n * Hook that returns a Sanity image URL builder configured with current provider settings.\n * Use this to create custom image transformations beyond `useImageUrl`.\n */\nexport function useImageUrlBuilder(): ImageUrlBuilder {\n const {projectId, dataset, apiHost} = useSanityProviderValue()\n return useMemo(() => {\n return createImageUrlBuilder({\n config: () => ({projectId, dataset, apiHost}),\n } as SanityModernClientLike)\n }, [apiHost, dataset, projectId])\n}\n\n/**\n * Hook that generates image URLs from Sanity image assets.\n * Returns a configured image URL builder for the given source.\n */\nexport function useImageUrl(source: SanityImageSource): ImageUrlBuilder {\n const builder = useImageUrlBuilder()\n return builder.image(source)\n}\n\nexport type * from '@sanity/image-url'\n","import type {Any, ClientReturn, QueryParams, QueryWithoutParams} from '@sanity/client'\nimport type {EncodeDataAttributeFunction} from '@sanity/core-loader/encode-data-attribute'\nimport type {QueryResponseInitial} from '@sanity/react-loader'\nimport {lazy, type ReactNode, Suspense, type SuspenseProps, useSyncExternalStore} from 'react'\n\nimport type {LoadQueryOptions} from './context'\nimport {usePreviewMode} from './preview/hooks'\nimport type {QueryClientProps} from './Query.client'\nimport {isServer} from './utils'\n\n/**\n * Fallback component that renders nothing, preventing hydration mismatches.\n */\nfunction SanityQueryFallback(): ReactNode {\n return null\n}\n\n/**\n * Simple hydration store to avoid hydration mismatches.\n * Returns false on server, true on client after hydration.\n */\nfunction useIsHydrated(): boolean {\n return useSyncExternalStore(\n // eslint-disable-next-line no-empty-function\n () => () => {},\n () => true,\n () => false,\n )\n}\n\nconst QueryClient = isServer()\n ? SanityQueryFallback\n : (lazy(\n () =>\n /**\n * `lazy` expects the component as the default export\n * @see https://react.dev/reference/react/lazy\n */\n import('./Query.client'),\n ) as <Result = Any, Query extends string = string>(\n props: QueryClientProps<Result, Query>,\n ) => ReactNode)\n\nconst noopEncodeDataAttribute: EncodeDataAttributeFunction = Object.assign(() => undefined, {\n scope: () => noopEncodeDataAttribute,\n})\n\nexport interface QueryProps<Result = Any, Query extends string = string> extends Omit<\n QueryClientProps<Result, Query>,\n 'options'\n> {\n query: Query\n params?: QueryParams | QueryWithoutParams\n options: {\n initial: ClientReturn<Query, Result> | QueryResponseInitial<ClientReturn<Query, Result>>\n } & LoadQueryOptions<ClientReturn<Query, Result>>\n children: (\n data: ClientReturn<Query, Result>,\n encodeDataAttribute: EncodeDataAttributeFunction,\n ) => ReactNode\n}\n\n/**\n * Query component that provides live updates in preview mode and static data otherwise.\n *\n * @public\n */\nexport function Query<Result = Any, Query extends string = string>({\n query,\n params,\n options,\n children,\n ...suspenseProps\n}: QueryProps<Result, Query> & Omit<SuspenseProps, 'children'>): ReactNode {\n const isPreviewMode = usePreviewMode()\n const isHydrated = useIsHydrated()\n\n // If in preview mode and hydrated, render the client component\n if (isPreviewMode && isHydrated) {\n return (\n <Suspense {...suspenseProps} fallback={suspenseProps.fallback ?? <SanityQueryFallback />}>\n <QueryClient<Result, Query>\n query={query}\n params={params}\n options={options as QueryClientProps<Result, Query>['options']}\n >\n {children}\n </QueryClient>\n </Suspense>\n )\n }\n\n // Render static data in non-preview mode or during hydration\n return children(options.initial as ClientReturn<Query, Result>, noopEncodeDataAttribute)\n}\n","import {useQuery as _useQuery, type UseQueryOptionsDefinedInitial} from '@sanity/react-loader'\nimport {useEffect, useId} from 'react'\n\nimport {registerQuery} from './registry'\n\n/**\n * Automatically registers with the query detection system.\n * This enables automatic live mode detection in `VisualEditing` components.\n */\nexport function useQuery<QueryResponseResult = unknown>(\n query: string,\n params?: Record<string, unknown>,\n options?: UseQueryOptionsDefinedInitial<QueryResponseResult>,\n): ReturnType<typeof _useQuery<QueryResponseResult>> {\n // Generate stable ID for this `useQuery` instance\n const id = useId()\n\n // Register this `useQuery` instance with the detection system\n useEffect(() => {\n const unregister = registerQuery(id)\n return unregister\n }, [id])\n\n // Call the original `useQuery` with all the same arguments\n return _useQuery<QueryResponseResult>(query, params, options)\n}\n"],"names":["apiVersion","_useQuery"],"mappings":";;;;;;;;;;;;;AAGO,MAAM,mBAAA,GAAsB;AAG5B,MAAM,yBAA0C,SAAA;;ACiBvD,IAAI,wBAAA,GAA2B,KAAA;AAC/B,IAAI,gCAAA,GAAmC,KAAA;AACvC,IAAI,qBAAA,GAAwB,KAAA;AAC5B,IAAI,mBAAA,GAAsB,KAAA;AA0J1B,eAAsB,oBACpB,OAAA,EACwB;AACxB,EAAA,MAAM,EAAC,KAAA,EAAO,SAAA,GAAY,MAAM,OAAA,CAAQ,SAAQ,EAAG,OAAA,EAAS,OAAA,EAAS,eAAA,EAAe,GAAI,OAAA;AACxF,EAAA,MAAM,SAAA,GAAY,QAAQ,eAAA,CAAgB,EAAC,OAAO,SAAA,EAAW,OAAA,EAAQ,CAAA,GAAI,IAAA;AACzE,EAAA,IAAI,MAAA,GACF,QAAQ,MAAA,YAAkB,YAAA,GAAe,QAAQ,MAAA,GAAS,YAAA,CAAa,QAAQ,MAAM,CAAA;AAEvF,EAAA,IAAI,MAAA,CAAO,MAAA,EAAO,CAAE,UAAA,KAAe,GAAA,EAAK;AACtC,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa,aAAA,IAAiB,CAAC,wBAAA,EAA0B;AACvE,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN;AAAA,0CAAA,EACoC,mBAAmB,CAAA;AAAA;AAAA,IAAA,CAAA,CAEzD,IAAA;AAAK,OACL;AAEA,MAAA,wBAAA,GAA2B,IAAA;AAAA,IAC7B;AAEA,IAAA,MAAA,GAAS,MAAA,CAAO,UAAA,CAAW,EAAC,UAAA,EAAY,qBAAoB,CAAA;AAAA,EAC9D;AAGA,EAAA,IAAI,cAAA,GAAiB,KAAA;AACrB,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,IAAI,CAAC,QAAQ,KAAA,EAAO;AAClB,MAAA,MAAM,IAAI,MAAM,yCAAyC,CAAA;AAAA,IAC3D;AAEA,IAAA,cAAA,GAAiB,iBAAiB,MAAA,CAAO,MAAA,EAAO,CAAE,SAAA,EAAY,QAAQ,OAAO,CAAA;AAE7E,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,MAAMA,WAAAA,GAAa,MAAA,CAAO,MAAA,EAAO,CAAE,UAAA;AACnC,MAAA,IAAI,WAAA;AACJ,MAAA,IAAI,wBAAA,CAAyBA,WAAU,CAAA,EAAG;AACxC,QAAA,WAAA,GAAc,cAAA,CAAe,QAAQ,OAAO,CAAA;AAAA,MAC9C,CAAA,MAAO;AACL,QAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa,aAAA,IAAiB,CAAC,gCAAA,EAAkC;AAC/E,UAAA,OAAA,CAAQ,IAAA;AAAA,YACN,iBAAiBA,WAAU,CAAA,yJAAA;AAAA,WAC7B;AAEA,UAAA,gCAAA,GAAmC,IAAA;AAAA,QACrC;AACA,QAAA,WAAA,GAAc,eAAA;AAAA,MAChB;AAEA,MAAA,MAAA,GAAS,OAAO,UAAA,CAAW;AAAA,QACzB,MAAA,EAAQ,KAAA;AAAA,QACR,OAAO,OAAA,CAAQ,KAAA;AAAA,QACf;AAAA,OACD,CAAA;AAAA,IACH;AAAA,EACF;AAGA,EAAA,MAAM,EAAC,OAAA,EAAS,SAAA,EAAW,SAAS,UAAA,EAAU,GAAI,OAAO,MAAA,EAAO;AAChE,EAAA,MAAM,aAAA,GAAqC;AAAA,IACzC,SAAA;AAAA,IACA,OAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,cAAA;AAAA,IACA,WAAA,EAAa,MAAA,CAAO,MAAA,EAAO,CAAE,WAAA,IAAe,WAAA;AAAA,IAC5C,YAAA,EAAc,MAAA,CAAO,MAAA,EAAO,CAAE,OAAO,OAAA,IAAW;AAAA,GAClD;AAEA,EAAA,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKL,MAAM,SAAA,CACJ,KAAA,EACA,MAAA,EACA,aAAA,EAC4D;AAE5D,MAAA,IAAI,CAAC,mBAAA,EAAqB;AACxB,QAAA,MAAM,EAAC,eAAA,EAAe,GAAI,MAAM,OAAO,sBAAsB,CAAA;AAC7D,QAAA,eAAA,CAAgB,MAAM,CAAA;AACtB,QAAA,mBAAA,GAAsB,IAAA;AAAA,MACxB;AAGA,MAAA,IAAI,CAAC,cAAA,IAAkB,OAAA,CAAQ,IAAI,QAAA,KAAa,aAAA,IAAiB,CAAC,qBAAA,EAAuB;AACvF,QAAA,OAAA,CAAQ,IAAA;AAAA,UACN,CAAA,kRAAA;AAAA,SACF;AACA,QAAA,qBAAA,GAAwB,IAAA;AAAA,MAC1B;AAEA,MAAA,IAAI,CAAC,aAAa,cAAA,EAAgB;AAChC,QAAA,MAAM,EAAC,SAAA,EAAS,GAAI,MAAM,OAAO,sBAAsB,CAAA;AACvD,QAAA,OAAO,MAAM,SAAA,CAAuC,KAAA,EAAO,MAAA,EAAQ,aAAa,CAAA;AAAA,MAClF;AAEA,MAAA,MAAM,aAAA,GACJ,aAAA,EAAe,QAAA,EAAU,KAAA,IAAS,eAAA,IAAmB,sBAAA;AACvD,MAAA,MAAM,SAAA,GAAY,MAAM,SAAA,CAAU,KAAA,EAAO,MAAM,CAAA;AAC/C,MAAA,MAAM,iBAAA,GAAoB,aAAA,EAAe,QAAA,EAAU,iBAAA,KAAsB,MAAM,IAAA,CAAA;AAE/E,MAAA,OAAO,MAAM,SAAA,CAAU,GAAA;AAAA,QACrB,EAAC,QAAA,EAAU,SAAA,EAAW,aAAA,EAAe,iBAAA,EAAiB;AAAA,QACtD,OAAO;AAAA,UACL;AAAA,SACF,KAEK;AAEH,UAAA,MAAM,WAAA,GAAc,aAAA,EAAe,QAAA,EAAU,KAAA,EAAO,WAAA,IAAe,cAAA;AAEnE,UAAA,YAAA,CAAa;AAAA,YACX;AAAA,WACD,CAAA;AAED,UAAA,MAAM,EAAC,SAAA,EAAS,GAAI,MAAM,OAAO,sBAAsB,CAAA;AACvD,UAAA,OAAO,MAAM,SAAA,CAAuC,KAAA,EAAO,MAAA,EAAQ,aAAa,CAAA;AAAA,QAClF;AAAA,OACF;AAAA,IACF,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,KAAA,CACJ,KAAA,EACA,MAAA,GAA2C,IAC3C,YAAA,EAIsC;AACtC,MAAA,IAAI,CAAC,aAAa,cAAA,EAAgB;AAChC,QAAA,OAAO,MAAM,MAAA,CAAO,KAAA,CAAmC,KAAA,EAAO,QAAQ,YAAY,CAAA;AAAA,MACpF;AAEA,MAAA,MAAM,aAAA,GACJ,YAAA,EAAc,QAAA,EAAU,KAAA,IAAS,eAAA,IAAmB,sBAAA;AACtD,MAAA,MAAM,SAAA,GAAY,MAAM,SAAA,CAAU,KAAA,EAAO,MAAM,CAAA;AAE/C,MAAA,OAAO,MAAM,SAAA,CAAU,GAAA;AAAA,QACrB,EAAC,QAAA,EAAU,SAAA,EAAW,aAAA,EAAe,iBAAA,EAAmB,MAAM,IAAA,EAAI;AAAA,QAClE,OAAO,EAAC,YAAA,EAAY,KAAsE;AAExF,UAAA,MAAM,WAAA,GAAc,YAAA,EAAc,QAAA,EAAU,KAAA,EAAO,WAAA,IAAe,cAAA;AAElE,UAAA,YAAA,CAAa;AAAA,YACX;AAAA,WACD,CAAA;AAED,UAAA,OAAO,MAAM,MAAA,CAAO,KAAA,CAAmC,KAAA,EAAO,QAAQ,YAAY,CAAA;AAAA,QACpF;AAAA,OACF;AAAA,IACF,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAM,KAAA,CACJ,KAAA,EACA,MAAA,EACA,YAAA,EAC0F;AAC1F,MAAA,OAAO,MAAA,CAAO,iBAAiB,IAAA,CAAK,SAAA,GAAY,KAAK,KAAA,EAAO,KAAA,EAAO,QAAQ,YAAY,CAAA;AAAA,IACzF,CAAA;AAAA;AAAA,IAGA,MAAA;AAAA;AAAA,IAGA,SAAS,OAAA,GAAU,EAAC,GAAG,OAAA,EAAS,OAAA,EAAS,gBAAc,GAAI,MAAA;AAAA;AAAA;AAAA;AAAA,IAK3D,cAAA,CAAe,EAAC,QAAA,EAAQ,EAA8B;AACpD,MAAA,OAAO,aAAA;AAAA,QACL,cAAA;AAAA,QACA;AAAA,UACE,KAAA,EAAO,MAAA,CAAO,MAAA,CAAO,aAAa;AAAA,SACpC;AAAA,QACA;AAAA,OACF;AAAA,IACF;AAAA,GACF;AACF;;ACxWO,SAAS,kBAAA,GAAsC;AACpD,EAAA,MAAM,EAAC,SAAA,EAAW,OAAA,EAAS,OAAA,KAAW,sBAAA,EAAuB;AAC7D,EAAA,OAAO,QAAQ,MAAM;AACnB,IAAA,OAAO,qBAAA,CAAsB;AAAA,MAC3B,MAAA,EAAQ,OAAO,EAAC,SAAA,EAAW,SAAS,OAAA,EAAO;AAAA,KAClB,CAAA;AAAA,EAC7B,CAAA,EAAG,CAAC,OAAA,EAAS,OAAA,EAAS,SAAS,CAAC,CAAA;AAClC;AAMO,SAAS,YAAY,MAAA,EAA4C;AACtE,EAAA,MAAM,UAAU,kBAAA,EAAmB;AACnC,EAAA,OAAO,OAAA,CAAQ,MAAM,MAAM,CAAA;AAC7B;;ACbA,SAAS,mBAAA,GAAiC;AACxC,EAAA,OAAO,IAAA;AACT;AAMA,SAAS,aAAA,GAAyB;AAChC,EAAA,OAAO,oBAAA;AAAA;AAAA,IAEL,MAAM,MAAM;AAAA,IAAC,CAAA;AAAA,IACb,MAAM,IAAA;AAAA,IACN,MAAM;AAAA,GACR;AACF;AAEA,MAAM,WAAA,GAAc,QAAA,EAAS,GACzB,mBAAA,GACC,IAAA;AAAA,EACC;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE,OAAO,8BAAgB;AAAA;AAC3B,CAAA;AAIJ,MAAM,uBAAA,GAAuD,MAAA,CAAO,MAAA,CAAO,MAAM,MAAA,EAAW;AAAA,EAC1F,OAAO,MAAM;AACf,CAAC,CAAA;AAsBM,SAAS,KAAA,CAAmD;AAAA,EACjE,KAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,GAAG;AACL,CAAA,EAA2E;AACzE,EAAA,MAAM,gBAAgB,cAAA,EAAe;AACrC,EAAA,MAAM,aAAa,aAAA,EAAc;AAGjC,EAAA,IAAI,iBAAiB,UAAA,EAAY;AAC/B,IAAA,uBACE,GAAA,CAAC,YAAU,GAAG,aAAA,EAAe,UAAU,aAAA,CAAc,QAAA,oBAAY,GAAA,CAAC,mBAAA,EAAA,EAAoB,CAAA,EACpF,QAAA,kBAAA,GAAA;AAAA,MAAC,WAAA;AAAA,MAAA;AAAA,QACC,KAAA;AAAA,QACA,MAAA;AAAA,QACA,OAAA;AAAA,QAEC;AAAA;AAAA,KACH,EACF,CAAA;AAAA,EAEJ;AAGA,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,OAAA,EAAwC,uBAAuB,CAAA;AACzF;;ACrFO,SAAS,QAAA,CACd,KAAA,EACA,MAAA,EACA,OAAA,EACmD;AAEnD,EAAA,MAAM,KAAK,KAAA,EAAM;AAGjB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,UAAA,GAAa,cAAc,EAAE,CAAA;AACnC,IAAA,OAAO,UAAA;AAAA,EACT,CAAA,EAAG,CAAC,EAAE,CAAC,CAAA;AAGP,EAAA,OAAOC,UAAA,CAA+B,KAAA,EAAO,MAAA,EAAQ,OAAO,CAAA;AAC9D;;;;"}
1
+ {"version":3,"file":"index.js","sources":["../src/constants.ts","../src/context.ts","../src/image.ts","../src/Query.tsx","../src/visual-editing/useQuery.tsx"],"sourcesContent":["import {CacheLong, type CachingStrategy} from '@shopify/hydrogen'\n\n/** Default Sanity API version with perspective stack support */\nexport const DEFAULT_API_VERSION = 'v2025-02-19'\n\n/** Default Hydrogen caching strategy for Sanity queries */\nexport const DEFAULT_CACHE_STRATEGY: CachingStrategy = CacheLong()\n","import {\n type Any,\n type ClientConfig,\n type ClientPerspective,\n type ClientReturn,\n createClient,\n type QueryParams,\n type QueryWithoutParams,\n type ResponseQueryOptions,\n SanityClient,\n} from '@sanity/client'\nimport type {QueryResponseInitial} from '@sanity/react-loader'\nimport {type CachingStrategy, createWithCache, type HydrogenSession} from '@shopify/hydrogen'\nimport {createElement, type PropsWithChildren, type ReactNode} from 'react'\n\nimport {DEFAULT_API_VERSION, DEFAULT_CACHE_STRATEGY} from './constants'\nimport type {SanityPreviewSession} from './preview/session'\nimport {isPreviewEnabled} from './preview/utils'\nimport {SanityProvider, type SanityProviderValue} from './provider'\nimport type {CacheActionFunctionParam, WaitUntil} from './types'\nimport {getPerspective, getPerspectiveFromUrl} from './utils'\nimport {hashQuery, supportsPerspectiveStack} from './utils'\n\nlet didWarnAboutNoApiVersion = false\nlet didWarnAboutNoPerspectiveSupport = false\nlet didWarnAboutLoadQuery = false\n\nexport type CreateSanityContextOptions = {\n request: Request\n\n cache?: Cache | undefined\n waitUntil?: WaitUntil | undefined\n\n /**\n * Sanity client or configuration to use.\n */\n client: SanityClient | ClientConfig\n\n /**\n * The default caching strategy to use for `loadQuery` subrequests.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies\n *\n * Defaults to `CacheLong`\n */\n defaultStrategy?: CachingStrategy | null\n\n /**\n * Configuration for enabling preview mode.\n */\n preview?: {\n token: string\n session: SanityPreviewSession | HydrogenSession\n }\n}\n\ninterface RequestInit {\n hydrogen?: {\n /**\n * The caching strategy to use for the subrequest.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies\n */\n cache?: CachingStrategy\n\n /**\n * Optional debugging information to be displayed in the subrequest profiler.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request\n */\n debug?: {\n displayName: string\n }\n }\n}\n\ntype HydrogenResponseQueryOptions = Omit<ResponseQueryOptions, 'next' | 'cache'> & {\n hydrogen?: 'hydrogen' extends keyof RequestInit ? RequestInit['hydrogen'] : never\n}\n\nexport type LoadQueryOptions<T> = Pick<\n HydrogenResponseQueryOptions,\n 'perspective' | 'hydrogen' | 'useCdn' | 'stega' | 'headers' | 'tag'\n> & {\n hydrogen?: {\n /**\n * The caching strategy to use for the subrequest.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies\n */\n cache?: CachingStrategy\n\n /**\n * Optional debugging information to be displayed in the subrequest profiler.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request\n */\n debug?: {\n displayName: string\n }\n\n /**\n * Whether to cache the result of the query or not.\n * @defaultValue () => true\n */\n shouldCacheResult?: (value: QueryResponseInitial<T>) => boolean\n }\n}\n\nexport type FetchOptions<T> = HydrogenResponseQueryOptions & {\n hydrogen?: {\n /**\n * The caching strategy to use for the subrequest.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies\n */\n cache?: CachingStrategy\n\n /**\n * Optional debugging information to be displayed in the subrequest profiler.\n * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request\n */\n debug?: {\n displayName: string\n }\n\n /**\n * Whether to cache the result of the query or not.\n * @defaultValue () => true\n */\n shouldCacheResult?: (value: QueryResponseInitial<T>) => boolean\n }\n}\n\nexport interface SanityContext {\n /**\n * Query Sanity using the loader.\n * @see https://www.sanity.io/docs/loaders\n */\n loadQuery<Result = Any, Query extends string = string>(\n query: Query,\n params?: QueryParams | QueryWithoutParams,\n options?: LoadQueryOptions<ClientReturn<Query, Result>>,\n ): Promise<QueryResponseInitial<ClientReturn<Query, Result>>>\n\n /**\n * Query Sanity using direct client fetch with Hydrogen caching.\n * Use this when you need direct client results without react-loader integration.\n * Automatically disables caching in preview mode for real-time updates.\n */\n fetch<Result = Any, Query extends string = string>(\n query: Query,\n params?: QueryParams | QueryWithoutParams,\n options?: FetchOptions<Result>,\n ): Promise<ClientReturn<Query, Result>>\n\n /**\n * Conditionally query Sanity using either loadQuery (for preview mode) or fetch (for static mode).\n * This optimizes bundle size by only loading @sanity/react-loader dependencies when in preview mode.\n */\n query<Result = Any, Query extends string = string>(\n query: Query,\n params?: QueryParams | QueryWithoutParams,\n options?: LoadQueryOptions<ClientReturn<Query, Result>> & FetchOptions<Result>,\n ): Promise<QueryResponseInitial<ClientReturn<Query, Result>> | ClientReturn<Query, Result>>\n\n /**\n * The Sanity client, automatically configured for preview mode when enabled.\n * Uses preview token, perspective, and CDN settings based on session state.\n */\n client: SanityClient\n\n preview?: CreateSanityContextOptions['preview'] & {\n /**\n * Whether preview mode is currently enabled based on session detection\n */\n enabled: boolean\n }\n\n SanityProvider: (props: PropsWithChildren<object>) => ReactNode\n}\n\n/**\n * @public\n */\nexport async function createSanityContext(\n options: CreateSanityContextOptions,\n): Promise<SanityContext> {\n const {cache, waitUntil = () => Promise.resolve(), request, preview, defaultStrategy} = options\n const withCache = cache ? createWithCache({cache, waitUntil, request}) : null\n let client =\n options.client instanceof SanityClient ? options.client : createClient(options.client)\n\n if (client.config().apiVersion === '1') {\n if (process.env.NODE_ENV === 'development' && !didWarnAboutNoApiVersion) {\n console.warn(\n `\nNo API version specified, defaulting to \\`${DEFAULT_API_VERSION}\\` which supports perspectives and Content Releases.\nYou can find the latest version in the Sanity changelog: https://www.sanity.io/changelog.\n `.trim(),\n )\n\n didWarnAboutNoApiVersion = true\n }\n\n client = client.withConfig({apiVersion: DEFAULT_API_VERSION})\n }\n\n // Determine if preview is enabled and configure the client accordingly\n let previewEnabled = false\n if (preview) {\n if (!preview.token) {\n throw new Error('Enabling preview mode requires a token.')\n }\n\n previewEnabled = isPreviewEnabled(client.config().projectId!, preview.session)\n\n if (previewEnabled) {\n const apiVersion = client.config().apiVersion\n let perspective: ClientPerspective\n\n // Prefer URL param over session — the cookie may lag behind the iframe reload.\n const urlPerspective = getPerspectiveFromUrl(request.url)\n\n if (\n urlPerspective !== undefined &&\n !(Array.isArray(urlPerspective) && !supportsPerspectiveStack(apiVersion))\n ) {\n perspective = urlPerspective\n } else if (supportsPerspectiveStack(apiVersion)) {\n perspective = getPerspective(preview.session)\n } else {\n if (process.env.NODE_ENV === 'development' && !didWarnAboutNoPerspectiveSupport) {\n console.warn(\n `API version \\`${apiVersion}\\` does not support perspective stacks. Using \\`previewDrafts\\` perspective. Consider upgrading to \\`v2025-02-19\\` or later for full perspective support.`,\n )\n\n didWarnAboutNoPerspectiveSupport = true\n }\n perspective = 'previewDrafts'\n }\n\n client = client.withConfig({\n useCdn: false,\n token: preview.token,\n perspective,\n })\n }\n }\n\n // Server client will be initialized lazily on first loadQuery call\n const {apiHost, projectId, dataset, apiVersion} = client.config()\n const providerValue: SanityProviderValue = {\n projectId: projectId!,\n dataset: dataset!,\n apiHost,\n apiVersion: apiVersion!,\n previewEnabled,\n perspective: client.config().perspective || 'published',\n stegaEnabled: client.config().stega?.enabled ?? false,\n }\n\n return {\n /**\n * Loads a Sanity query with client-side loader support and Hydrogen cache integration.\n * Bypasses Hydrogen cache in preview mode.\n */\n async loadQuery<Result = Any, Query extends string = string>(\n query: Query,\n params: QueryParams | QueryWithoutParams,\n loaderOptions?: LoadQueryOptions<ClientReturn<Query, Result>>,\n ): Promise<QueryResponseInitial<ClientReturn<Query, Result>>> {\n const {setServerClient} = await import('@sanity/react-loader')\n setServerClient(client)\n\n // Warn users to migrate to `query` method when using loadQuery outside preview mode\n if (!previewEnabled && process.env.NODE_ENV === 'development' && !didWarnAboutLoadQuery) {\n console.warn(\n `\\`loadQuery\\` is being called outside of preview mode. Consider using \\`query\\` instead, which automatically handles both preview and production modes efficiently, or use \\`fetch\\`. \\`loadQuery\\` is intended to be called conditionally in preview and visual editing contexts.`,\n )\n didWarnAboutLoadQuery = true\n }\n\n if (!withCache || previewEnabled) {\n const {loadQuery} = await import('@sanity/react-loader')\n // Override the singleton's possibly-stale perspective with the per-request value.\n const resolvedOptions =\n previewEnabled && !loaderOptions?.perspective\n ? {...loaderOptions, perspective: client.config().perspective as ClientPerspective}\n : loaderOptions\n return await loadQuery<ClientReturn<Query, Result>>(query, params, resolvedOptions)\n }\n\n const cacheStrategy =\n loaderOptions?.hydrogen?.cache || defaultStrategy || DEFAULT_CACHE_STRATEGY\n const queryHash = await hashQuery(query, params)\n const shouldCacheResult = loaderOptions?.hydrogen?.shouldCacheResult ?? (() => true)\n\n return await withCache.run(\n {cacheKey: queryHash, cacheStrategy, shouldCacheResult},\n async ({\n addDebugData,\n }: CacheActionFunctionParam): Promise<\n QueryResponseInitial<ClientReturn<Query, Result>>\n > => {\n // Name displayed in the subrequest profiler\n const displayName = loaderOptions?.hydrogen?.debug?.displayName || 'query Sanity'\n\n addDebugData({\n displayName,\n })\n\n const {loadQuery} = await import('@sanity/react-loader')\n return await loadQuery<ClientReturn<Query, Result>>(query, params, loaderOptions)\n },\n )\n },\n\n /**\n * Executes a Sanity query with Hydrogen cache integration.\n * Direct client fetch without loader integration. Bypasses cache in preview mode.\n */\n async fetch<Result = Any, Query extends string = string>(\n query: Query,\n params: QueryParams | QueryWithoutParams = {},\n fetchOptions?: Pick<\n LoadQueryOptions<Result>,\n 'perspective' | 'hydrogen' | 'useCdn' | 'headers' | 'tag'\n >,\n ): Promise<ClientReturn<Query, Result>> {\n if (!withCache || previewEnabled) {\n return await client.fetch<ClientReturn<Query, Result>>(query, params, fetchOptions)\n }\n\n const cacheStrategy =\n fetchOptions?.hydrogen?.cache || defaultStrategy || DEFAULT_CACHE_STRATEGY\n const queryHash = await hashQuery(query, params)\n\n return await withCache.run(\n {cacheKey: queryHash, cacheStrategy, shouldCacheResult: () => true},\n async ({addDebugData}: CacheActionFunctionParam): Promise<ClientReturn<Query, Result>> => {\n // Name displayed in the subrequest profiler\n const displayName = fetchOptions?.hydrogen?.debug?.displayName || 'fetch Sanity'\n\n addDebugData({\n displayName,\n })\n\n return await client.fetch<ClientReturn<Query, Result>>(query, params, fetchOptions)\n },\n )\n },\n\n /**\n * Automatic query method that automatically adapts based on preview mode state.\n * Uses `loadQuery` (with client-side loader integration) when preview is enabled, `fetch` otherwise.\n * Bypasses cache in preview mode.\n */\n async query<Result = Any, Query extends string = string>(\n query: Query,\n params?: QueryParams | QueryWithoutParams,\n queryOptions?: LoadQueryOptions<ClientReturn<Query, Result>> & FetchOptions<Result>,\n ): Promise<QueryResponseInitial<ClientReturn<Query, Result>> | ClientReturn<Query, Result>> {\n return await (previewEnabled ? this.loadQuery : this.fetch)(query, params, queryOptions)\n },\n\n /** The configured Sanity client instance */\n client,\n\n /** Preview configuration with session-based state, undefined when preview is not configured */\n preview: preview ? {...preview, enabled: previewEnabled} : undefined,\n\n /**\n * React Provider component that serializes Sanity configuration across server-client boundary.\n */\n SanityProvider({children}: PropsWithChildren<object>) {\n return createElement(\n SanityProvider,\n {\n value: Object.freeze(providerValue),\n },\n children,\n )\n },\n } satisfies SanityContext\n}\n","import type {ImageUrlBuilder, SanityImageSource, SanityModernClientLike} from '@sanity/image-url'\nimport {createImageUrlBuilder} from '@sanity/image-url'\nimport {useMemo} from 'react'\n\nimport {useSanityProviderValue} from './provider'\n\n/**\n * Hook that returns a Sanity image URL builder configured with current provider settings.\n * Use this to create custom image transformations beyond `useImageUrl`.\n */\nexport function useImageUrlBuilder(): ImageUrlBuilder {\n const {projectId, dataset, apiHost} = useSanityProviderValue()\n return useMemo(() => {\n return createImageUrlBuilder({\n config: () => ({projectId, dataset, apiHost}),\n } as SanityModernClientLike)\n }, [apiHost, dataset, projectId])\n}\n\n/**\n * Hook that generates image URLs from Sanity image assets.\n * Returns a configured image URL builder for the given source.\n */\nexport function useImageUrl(source: SanityImageSource): ImageUrlBuilder {\n const builder = useImageUrlBuilder()\n return builder.image(source)\n}\n\nexport type * from '@sanity/image-url'\n","import type {Any, ClientReturn, QueryParams, QueryWithoutParams} from '@sanity/client'\nimport type {EncodeDataAttributeFunction} from '@sanity/core-loader/encode-data-attribute'\nimport type {QueryResponseInitial} from '@sanity/react-loader'\nimport {lazy, type ReactNode, Suspense, type SuspenseProps, useSyncExternalStore} from 'react'\n\nimport type {LoadQueryOptions} from './context'\nimport {usePreviewMode} from './preview/hooks'\nimport type {QueryClientProps} from './Query.client'\nimport {isServer} from './utils'\n\n/**\n * Fallback component that renders nothing, preventing hydration mismatches.\n */\nfunction SanityQueryFallback(): ReactNode {\n return null\n}\n\n/**\n * Simple hydration store to avoid hydration mismatches.\n * Returns false on server, true on client after hydration.\n */\nfunction useIsHydrated(): boolean {\n return useSyncExternalStore(\n // eslint-disable-next-line no-empty-function\n () => () => {},\n () => true,\n () => false,\n )\n}\n\nconst QueryClient = isServer()\n ? SanityQueryFallback\n : (lazy(\n () =>\n /**\n * `lazy` expects the component as the default export\n * @see https://react.dev/reference/react/lazy\n */\n import('./Query.client'),\n ) as <Result = Any, Query extends string = string>(\n props: QueryClientProps<Result, Query>,\n ) => ReactNode)\n\nconst noopEncodeDataAttribute: EncodeDataAttributeFunction = Object.assign(() => undefined, {\n scope: () => noopEncodeDataAttribute,\n})\n\nexport interface QueryProps<Result = Any, Query extends string = string> extends Omit<\n QueryClientProps<Result, Query>,\n 'options'\n> {\n query: Query\n params?: QueryParams | QueryWithoutParams\n options: {\n initial: ClientReturn<Query, Result> | QueryResponseInitial<ClientReturn<Query, Result>>\n } & LoadQueryOptions<ClientReturn<Query, Result>>\n children: (\n data: ClientReturn<Query, Result>,\n encodeDataAttribute: EncodeDataAttributeFunction,\n ) => ReactNode\n}\n\n/**\n * Query component that provides live updates in preview mode and static data otherwise.\n *\n * @public\n */\nexport function Query<Result = Any, Query extends string = string>({\n query,\n params,\n options,\n children,\n ...suspenseProps\n}: QueryProps<Result, Query> & Omit<SuspenseProps, 'children'>): ReactNode {\n const isPreviewMode = usePreviewMode()\n const isHydrated = useIsHydrated()\n\n // If in preview mode and hydrated, render the client component\n if (isPreviewMode && isHydrated) {\n return (\n <Suspense {...suspenseProps} fallback={suspenseProps.fallback ?? <SanityQueryFallback />}>\n <QueryClient<Result, Query>\n query={query}\n params={params}\n options={options as QueryClientProps<Result, Query>['options']}\n >\n {children}\n </QueryClient>\n </Suspense>\n )\n }\n\n // Render static data in non-preview mode or during hydration\n return children(options.initial as ClientReturn<Query, Result>, noopEncodeDataAttribute)\n}\n","import {useQuery as _useQuery, type UseQueryOptionsDefinedInitial} from '@sanity/react-loader'\nimport {useEffect, useId} from 'react'\n\nimport {registerQuery} from './registry'\n\n/**\n * Automatically registers with the query detection system.\n * This enables automatic live mode detection in `VisualEditing` components.\n */\nexport function useQuery<QueryResponseResult = unknown>(\n query: string,\n params?: Record<string, unknown>,\n options?: UseQueryOptionsDefinedInitial<QueryResponseResult>,\n): ReturnType<typeof _useQuery<QueryResponseResult>> {\n // Generate stable ID for this `useQuery` instance\n const id = useId()\n\n // Register this `useQuery` instance with the detection system\n useEffect(() => {\n const unregister = registerQuery(id)\n return unregister\n }, [id])\n\n // Call the original `useQuery` with all the same arguments\n return _useQuery<QueryResponseResult>(query, params, options)\n}\n"],"names":["apiVersion","_useQuery"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hydrogen-sanity",
3
- "version": "5.1.3",
3
+ "version": "5.3.0",
4
4
  "description": "Sanity.io toolkit for Hydrogen",
5
5
  "keywords": [
6
6
  "sanity",
@@ -26,8 +26,36 @@
26
26
  "publishConfig": {
27
27
  "access": "public",
28
28
  "registry": "https://registry.npmjs.org/",
29
- "provenance": true
29
+ "provenance": true,
30
+ "exports": {
31
+ ".": {
32
+ "import": "./dist/index.js",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./preview": {
36
+ "import": "./dist/preview/index.js",
37
+ "default": "./dist/preview/index.js"
38
+ },
39
+ "./preview/route": {
40
+ "import": "./dist/preview/route.js",
41
+ "default": "./dist/preview/route.js"
42
+ },
43
+ "./preview/session": {
44
+ "import": "./dist/preview/session.js",
45
+ "default": "./dist/preview/session.js"
46
+ },
47
+ "./visual-editing": {
48
+ "import": "./dist/visual-editing/index.js",
49
+ "default": "./dist/visual-editing/index.js"
50
+ },
51
+ "./vite": {
52
+ "import": "./dist/vite/index.js",
53
+ "default": "./dist/vite/index.js"
54
+ },
55
+ "./package.json": "./package.json"
56
+ }
30
57
  },
58
+ "browserslist": "extends @sanity/browserslist-config",
31
59
  "type": "module",
32
60
  "exports": {
33
61
  ".": {
@@ -106,6 +134,7 @@
106
134
  "@sanity/preview-url-secret": "^2.1.14",
107
135
  "@sanity/react-loader": "^1.11.18",
108
136
  "@sanity/visual-editing": "^3.0.3",
137
+ "fast-deep-equal": "^3.1.3",
109
138
  "use-effect-event": "^2.0.3"
110
139
  },
111
140
  "devDependencies": {
@@ -139,7 +168,7 @@
139
168
  "peerDependencies": {
140
169
  "@sanity/client": "^7",
141
170
  "@shopify/hydrogen": "~2025.5.0 || ~2025.7.0",
142
- "react": "^18.2.0",
171
+ "react": "^18.2.0 || ^19.0.0",
143
172
  "react-router": "^7.6.0",
144
173
  "vite": "^5.1.0 || ^6.2.1"
145
174
  },
@@ -439,6 +439,131 @@ describe('stegaEnabled serialization', () => {
439
439
  })
440
440
  })
441
441
 
442
+ describe('perspective resolution priority', () => {
443
+ beforeEach(() => {
444
+ vi.clearAllMocks()
445
+ })
446
+
447
+ it('should use URL param perspective over session value', async () => {
448
+ const previewSession = new PreviewSession()
449
+ previewSession.set('projectId', projectId)
450
+ previewSession.set('perspective', 'drafts')
451
+
452
+ const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts')
453
+
454
+ const context = await createSanityContext({
455
+ request,
456
+ cache,
457
+ client,
458
+ preview: {
459
+ token: 'my-token',
460
+ session: previewSession,
461
+ },
462
+ })
463
+
464
+ expect(context.client.config().perspective).toEqual(['releaseId', 'drafts'])
465
+ })
466
+
467
+ it('should fall back to session perspective when URL param is absent', async () => {
468
+ const previewSession = new PreviewSession()
469
+ previewSession.set('projectId', projectId)
470
+ previewSession.set('perspective', 'drafts')
471
+
472
+ const request = new Request('https://example.com/')
473
+
474
+ const context = await createSanityContext({
475
+ request,
476
+ cache,
477
+ client,
478
+ preview: {
479
+ token: 'my-token',
480
+ session: previewSession,
481
+ },
482
+ })
483
+
484
+ expect(context.client.config().perspective).toEqual(['drafts'])
485
+ })
486
+
487
+ it('should pass perspective explicitly to loadQuery in preview mode', async () => {
488
+ const previewSession = new PreviewSession()
489
+ previewSession.set('projectId', projectId)
490
+ previewSession.set('perspective', 'drafts')
491
+
492
+ const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts')
493
+
494
+ const context = await createSanityContext({
495
+ request,
496
+ cache,
497
+ client,
498
+ preview: {
499
+ token: 'my-token',
500
+ session: previewSession,
501
+ },
502
+ })
503
+
504
+ await context.loadQuery<boolean>(query, params)
505
+
506
+ expect(loadQuery).toHaveBeenCalledWith(
507
+ query,
508
+ params,
509
+ expect.objectContaining({
510
+ perspective: ['releaseId', 'drafts'],
511
+ }),
512
+ )
513
+ })
514
+
515
+ it('should ignore URL param perspective stack when API version is too old', async () => {
516
+ const previewSession = new PreviewSession()
517
+ previewSession.set('projectId', projectId)
518
+
519
+ const oldClient = createClient({
520
+ projectId,
521
+ dataset: 'my-dataset',
522
+ apiVersion: '2024-01-01',
523
+ })
524
+
525
+ const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts')
526
+
527
+ const context = await createSanityContext({
528
+ request,
529
+ cache,
530
+ client: oldClient,
531
+ preview: {
532
+ token: 'my-token',
533
+ session: previewSession,
534
+ },
535
+ })
536
+
537
+ // Should fall back to previewDrafts since API version doesn't support stacks
538
+ expect(context.client.config().perspective).toBe('previewDrafts')
539
+ })
540
+
541
+ it('should accept single URL param perspective even with old API version', async () => {
542
+ const previewSession = new PreviewSession()
543
+ previewSession.set('projectId', projectId)
544
+
545
+ const oldClient = createClient({
546
+ projectId,
547
+ dataset: 'my-dataset',
548
+ apiVersion: '2024-01-01',
549
+ })
550
+
551
+ const request = new Request('https://example.com/?sanity-preview-perspective=drafts')
552
+
553
+ const context = await createSanityContext({
554
+ request,
555
+ cache,
556
+ client: oldClient,
557
+ preview: {
558
+ token: 'my-token',
559
+ session: previewSession,
560
+ },
561
+ })
562
+
563
+ expect(context.client.config().perspective).toBe('drafts')
564
+ })
565
+ })
566
+
442
567
  describe('lazy-initialize loaders', () => {
443
568
  const request = new Request('https://example.com')
444
569
 
@@ -456,22 +581,21 @@ describe('lazy-initialize loaders', () => {
456
581
  expect(setServerClient).not.toHaveBeenCalled()
457
582
  })
458
583
 
459
- it('should allow `loadQuery` to be called not in preview mode', async () => {
584
+ it('should call `setServerClient` on every `loadQuery` invocation', async () => {
460
585
  const context = await createSanityContext({
461
586
  request,
462
587
  cache,
463
588
  client,
464
589
  })
465
590
 
466
- // loadQuery should work in non-preview mode (backwards compatibility)
467
- // The actual loadQuery function will be called from the mocked module
468
591
  await context.loadQuery<boolean>(query, params)
592
+ expect(setServerClient).toHaveBeenCalledTimes(1)
469
593
 
470
- // Verify the mock was called (actual behavior testing is done in other tests)
471
- expect(loadQuery).toHaveBeenCalled()
594
+ await context.loadQuery<boolean>(query, params)
595
+ expect(setServerClient).toHaveBeenCalledTimes(2)
472
596
  })
473
597
 
474
- it('should call `setServerClient` on first `loadQuery` invocation in preview mode', async () => {
598
+ it('should call `setServerClient` with the preview-configured client', async () => {
475
599
  const previewSession = new PreviewSession()
476
600
  previewSession.set('projectId', projectId)
477
601
 
@@ -485,17 +609,11 @@ describe('lazy-initialize loaders', () => {
485
609
  },
486
610
  })
487
611
 
488
- // First call to `loadQuery`
489
612
  await context.loadQuery<boolean>(query, params)
490
613
 
491
- // Should be called with the preview-configured client
492
- // Check the most recent call
493
- const latestCall = setServerClient.mock.calls[setServerClient.mock.calls.length - 1]
494
- if (latestCall) {
495
- const calledWithClient = latestCall[0]
496
- expect(calledWithClient.config().useCdn).toBe(false)
497
- expect(calledWithClient.config().token).toBe('my-token')
498
- }
614
+ const calledWithClient = setServerClient.mock.calls[0][0]
615
+ expect(calledWithClient.config().useCdn).toBe(false)
616
+ expect(calledWithClient.config().token).toBe('my-token')
499
617
  })
500
618
 
501
619
  it('should display warning when `loadQuery` called outside preview mode in development', async () => {
package/src/context.ts CHANGED
@@ -18,13 +18,12 @@ import type {SanityPreviewSession} from './preview/session'
18
18
  import {isPreviewEnabled} from './preview/utils'
19
19
  import {SanityProvider, type SanityProviderValue} from './provider'
20
20
  import type {CacheActionFunctionParam, WaitUntil} from './types'
21
- import {getPerspective} from './utils'
21
+ import {getPerspective, getPerspectiveFromUrl} from './utils'
22
22
  import {hashQuery, supportsPerspectiveStack} from './utils'
23
23
 
24
24
  let didWarnAboutNoApiVersion = false
25
25
  let didWarnAboutNoPerspectiveSupport = false
26
26
  let didWarnAboutLoadQuery = false
27
- let didInitializeLoader = false
28
27
 
29
28
  export type CreateSanityContextOptions = {
30
29
  request: Request
@@ -213,7 +212,16 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
213
212
  if (previewEnabled) {
214
213
  const apiVersion = client.config().apiVersion
215
214
  let perspective: ClientPerspective
216
- if (supportsPerspectiveStack(apiVersion)) {
215
+
216
+ // Prefer URL param over session — the cookie may lag behind the iframe reload.
217
+ const urlPerspective = getPerspectiveFromUrl(request.url)
218
+
219
+ if (
220
+ urlPerspective !== undefined &&
221
+ !(Array.isArray(urlPerspective) && !supportsPerspectiveStack(apiVersion))
222
+ ) {
223
+ perspective = urlPerspective
224
+ } else if (supportsPerspectiveStack(apiVersion)) {
217
225
  perspective = getPerspective(preview.session)
218
226
  } else {
219
227
  if (process.env.NODE_ENV === 'development' && !didWarnAboutNoPerspectiveSupport) {
@@ -256,12 +264,8 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
256
264
  params: QueryParams | QueryWithoutParams,
257
265
  loaderOptions?: LoadQueryOptions<ClientReturn<Query, Result>>,
258
266
  ): Promise<QueryResponseInitial<ClientReturn<Query, Result>>> {
259
- // Lazy initialize the loader on first call with the configured client
260
- if (!didInitializeLoader) {
261
- const {setServerClient} = await import('@sanity/react-loader')
262
- setServerClient(client)
263
- didInitializeLoader = true
264
- }
267
+ const {setServerClient} = await import('@sanity/react-loader')
268
+ setServerClient(client)
265
269
 
266
270
  // Warn users to migrate to `query` method when using loadQuery outside preview mode
267
271
  if (!previewEnabled && process.env.NODE_ENV === 'development' && !didWarnAboutLoadQuery) {
@@ -273,7 +277,12 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
273
277
 
274
278
  if (!withCache || previewEnabled) {
275
279
  const {loadQuery} = await import('@sanity/react-loader')
276
- return await loadQuery<ClientReturn<Query, Result>>(query, params, loaderOptions)
280
+ // Override the singleton's possibly-stale perspective with the per-request value.
281
+ const resolvedOptions =
282
+ previewEnabled && !loaderOptions?.perspective
283
+ ? {...loaderOptions, perspective: client.config().perspective as ClientPerspective}
284
+ : loaderOptions
285
+ return await loadQuery<ClientReturn<Query, Result>>(query, params, resolvedOptions)
277
286
  }
278
287
 
279
288
  const cacheStrategy =
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export {createSanityContext, type SanityContext} from './context'
3
3
  export {useImageUrl, useImageUrlBuilder} from './image'
4
4
  export {Sanity, useSanityProviderValue} from './provider'
5
5
  export {Query, type QueryProps} from './Query'
6
+ export {getPerspectiveFromUrl} from './utils'
6
7
  export {useQuery} from './visual-editing/useQuery'
7
8
  export type {EncodeDataAttributeFunction} from '@sanity/core-loader/encode-data-attribute'
8
9
  export type * from '@sanity/react-loader'
package/src/utils.test.ts CHANGED
@@ -2,7 +2,7 @@ import {beforeEach, describe, expect, it, vi} from 'vitest'
2
2
 
3
3
  import {PreviewSession} from './fixtures'
4
4
  import {isPreviewEnabled} from './preview/utils'
5
- import {getPerspective} from './utils'
5
+ import {getPerspective, getPerspectiveFromUrl} from './utils'
6
6
  import {sanitizePerspective, supportsPerspectiveStack} from './utils'
7
7
 
8
8
  describe('sanitizePerspective', () => {
@@ -17,6 +17,20 @@ describe('sanitizePerspective', () => {
17
17
 
18
18
  expect(result).toBe('drafts')
19
19
  })
20
+
21
+ it('should filter out empty strings from perspective array', () => {
22
+ // This happens when upstream sends [undefined, "releaseId", "drafts"]
23
+ // which gets serialized as ",releaseId,drafts" and split back
24
+ const result = sanitizePerspective(',releaseId,drafts')
25
+
26
+ expect(result).toEqual(['releaseId', 'drafts'])
27
+ })
28
+
29
+ it('should filter out multiple empty strings', () => {
30
+ const result = sanitizePerspective('drafts,,published,')
31
+
32
+ expect(result).toEqual(['drafts', 'published'])
33
+ })
20
34
  })
21
35
 
22
36
  describe('supportsPerspectiveStack', () => {
@@ -79,6 +93,57 @@ describe('getPerspective', () => {
79
93
  expect(result).toEqual(['drafts', 'published'])
80
94
  expect(mockSession.get).toHaveBeenCalledWith('perspective')
81
95
  })
96
+
97
+ it('should filter out empty strings from perspective array', () => {
98
+ // This happens when upstream sends [undefined, "releaseId", "drafts"]
99
+ // which gets serialized as ",releaseId,drafts"
100
+ mockSession.get.mockReturnValue(',releaseId,drafts')
101
+
102
+ const result = getPerspective(mockSession)
103
+
104
+ expect(result).toEqual(['releaseId', 'drafts'])
105
+ })
106
+ })
107
+
108
+ describe('getPerspectiveFromUrl', () => {
109
+ it('should return parsed perspective when URL param is present and valid', () => {
110
+ expect(getPerspectiveFromUrl('https://example.com/?sanity-preview-perspective=drafts')).toBe(
111
+ 'drafts',
112
+ )
113
+ })
114
+
115
+ it('should return undefined when param is absent', () => {
116
+ expect(getPerspectiveFromUrl('https://example.com/')).toBeUndefined()
117
+ })
118
+
119
+ it('should return undefined when param is empty', () => {
120
+ expect(
121
+ getPerspectiveFromUrl('https://example.com/?sanity-preview-perspective='),
122
+ ).toBeUndefined()
123
+ })
124
+
125
+ it('should handle comma-separated perspective stacks', () => {
126
+ expect(
127
+ getPerspectiveFromUrl('https://example.com/?sanity-preview-perspective=releaseId,drafts'),
128
+ ).toEqual(['releaseId', 'drafts'])
129
+ })
130
+
131
+ it('should filter empty segments from perspective stacks', () => {
132
+ expect(
133
+ getPerspectiveFromUrl('https://example.com/?sanity-preview-perspective=,releaseId,drafts'),
134
+ ).toEqual(['releaseId', 'drafts'])
135
+ })
136
+
137
+ it('should convert raw to drafts', () => {
138
+ expect(getPerspectiveFromUrl('https://example.com/?sanity-preview-perspective=raw')).toBe(
139
+ 'drafts',
140
+ )
141
+ })
142
+
143
+ it('should accept a URL object', () => {
144
+ const url = new URL('https://example.com/?sanity-preview-perspective=drafts')
145
+ expect(getPerspectiveFromUrl(url)).toBe('drafts')
146
+ })
82
147
  })
83
148
 
84
149
  describe('isPreviewEnabled', () => {
package/src/utils.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  type QueryWithoutParams,
5
5
  validateApiPerspective,
6
6
  } from '@sanity/client'
7
+ import {urlSearchParamPreviewPerspective} from '@sanity/preview-url-secret/constants'
7
8
  import type {HydrogenSession} from '@shopify/hydrogen'
8
9
 
9
10
  import type {SanityPreviewSession} from './preview/session'
@@ -45,11 +46,18 @@ export function hashQuery(
45
46
  * Handles both string (comma-separated) and array formats.
46
47
  */
47
48
  export function sanitizePerspective(perspective: unknown): Exclude<ClientPerspective, 'raw'> {
48
- const sanitizedPerspective =
49
+ let sanitizedPerspective =
49
50
  typeof perspective === 'string' && perspective.includes(',')
50
51
  ? perspective.split(',')
51
52
  : perspective
52
53
 
54
+ // Filter out empty strings and undefined values from perspective array
55
+ if (Array.isArray(sanitizedPerspective)) {
56
+ sanitizedPerspective = sanitizedPerspective.filter(
57
+ (p): p is string => typeof p === 'string' && p.length > 0,
58
+ )
59
+ }
60
+
53
61
  validateApiPerspective(sanitizedPerspective)
54
62
 
55
63
  return sanitizedPerspective === 'raw' ? 'drafts' : sanitizedPerspective
@@ -80,11 +88,29 @@ export function supportsPerspectiveStack(apiVersion: string): boolean {
80
88
  * Extracts and validates the perspective from a session.
81
89
  */
82
90
  export function getPerspective(session: SanityPreviewSession | HydrogenSession): ClientPerspective {
83
- const perspective = session.get('perspective')?.split(',')
91
+ const perspective = session
92
+ .get('perspective')
93
+ ?.split(',')
94
+ .filter((p: string) => p.length > 0)
84
95
  validateApiPerspective(perspective)
85
96
  return perspective
86
97
  }
87
98
 
99
+ /**
100
+ * Reads the `sanity-preview-perspective` URL search param and validates it.
101
+ * Returns `undefined` if absent or invalid, so callers can fall back to the session.
102
+ */
103
+ export function getPerspectiveFromUrl(url: URL | string): ClientPerspective | undefined {
104
+ try {
105
+ const parsed = typeof url === 'string' ? new URL(url) : url
106
+ const param = parsed.searchParams.get(urlSearchParamPreviewPerspective)
107
+ if (!param) return undefined
108
+ return sanitizePerspective(param)
109
+ } catch {
110
+ return undefined
111
+ }
112
+ }
113
+
88
114
  /**
89
115
  * Type guard that checks if a session object is a SanityPreviewSession.
90
116
  * Validates presence of required methods: has, destroy (in addition to Hydrogen session methods).
@@ -1,6 +1,7 @@
1
1
  import {createClient, type StegaConfig} from '@sanity/client'
2
2
  import {useLiveMode} from '@sanity/react-loader'
3
- import {type ReactNode, useEffect, useMemo} from 'react'
3
+ import isEqual from 'fast-deep-equal'
4
+ import {type ReactNode, useEffect, useMemo, useState} from 'react'
4
5
 
5
6
  import {useSanityProviderValue} from '../provider'
6
7
  import {isServer} from '../utils'
@@ -45,7 +46,18 @@ function LiveModeClient(props: LiveModeProps): ReactNode {
45
46
 
46
47
  const sanityProvider = useSanityProviderValue()
47
48
 
48
- // eslint-disable-next-line react-hooks/preserve-manual-memoization
49
+ // Maintain reference stability for stegaProps when content is unchanged
50
+ // This prevents unnecessary client recreation when parent component re-renders
51
+ const [stableStegaProps, setStableStegaProps] = useState(stegaProps)
52
+ useEffect(() => {
53
+ if (!isEqual(stableStegaProps, stegaProps)) {
54
+ setStableStegaProps(stegaProps)
55
+ }
56
+ // Intentionally not including stableStegaProps in deps - we only want to
57
+ // update when the incoming stegaProps changes, comparing against the stored value
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, [stegaProps])
60
+
49
61
  const client = useMemo(() => {
50
62
  const baseClient = createClient({
51
63
  projectId: sanityProvider.projectId,
@@ -56,11 +68,11 @@ function LiveModeClient(props: LiveModeProps): ReactNode {
56
68
  })
57
69
 
58
70
  // Apply stega configuration if provided
59
- if (sanityProvider.stegaEnabled && Object.keys(stegaProps).length > 0) {
71
+ if (sanityProvider.stegaEnabled && Object.keys(stableStegaProps).length > 0) {
60
72
  return baseClient.withConfig({
61
73
  stega: {
62
74
  enabled: true,
63
- ...stegaProps,
75
+ ...stableStegaProps,
64
76
  },
65
77
  })
66
78
  }
@@ -72,8 +84,7 @@ function LiveModeClient(props: LiveModeProps): ReactNode {
72
84
  sanityProvider.perspective,
73
85
  sanityProvider.apiVersion,
74
86
  sanityProvider.stegaEnabled,
75
- // eslint-disable-next-line react-hooks/preserve-manual-memoization
76
- stegaProps,
87
+ stableStegaProps,
77
88
  ])
78
89
 
79
90
  // Enable live mode for real-time data updates (client loaders only)
@@ -83,11 +94,9 @@ function LiveModeClient(props: LiveModeProps): ReactNode {
83
94
  onDisconnect,
84
95
  })
85
96
 
86
- // Automatically handle revalidator state changes
87
- const {handleRevalidatorState} = useRefresh()
88
- useEffect(() => {
89
- handleRevalidatorState()
90
- })
97
+ // Initialize refresh hook to handle revalidator state transitions
98
+ // The hook internally manages state changes via useEffect
99
+ useRefresh()
91
100
 
92
101
  return null
93
102
  }
@@ -9,7 +9,7 @@ import {type ReactNode, useEffect, useState} from 'react'
9
9
  import {useRevalidator, useSubmit} from 'react-router'
10
10
  import {useEffectEvent} from 'use-effect-event'
11
11
 
12
- import {isServer} from '../utils'
12
+ import {isServer, sanitizePerspective} from '../utils'
13
13
  import {useHistory} from './hooks/history'
14
14
  import {useRefresh} from './hooks/refresh'
15
15
  import {useHasActiveLoaders} from './registry'
@@ -82,8 +82,14 @@ function OverlaysClient(props: OverlaysProps): ReactNode {
82
82
 
83
83
  // Handle perspective changes from Studio
84
84
  const handlePerspectiveChange = useEffectEvent((perspective: ClientPerspective) => {
85
+ // Sanitize perspective (filters out undefined/empty values from upstream bug)
86
+ const cleanPerspective = sanitizePerspective(perspective)
87
+
85
88
  const formData = new FormData()
86
- formData.set('perspective', Array.isArray(perspective) ? perspective.join(',') : perspective)
89
+ formData.set(
90
+ 'perspective',
91
+ Array.isArray(cleanPerspective) ? cleanPerspective.join(',') : cleanPerspective,
92
+ )
87
93
  submit(formData, {
88
94
  method: 'PUT',
89
95
  action,
@@ -11,18 +11,23 @@ export function useHistory(): HistoryAdapter {
11
11
  const navigateRemixRef = useRef(navigateRemix)
12
12
  const [navigate, setNavigate] = useState<HistoryAdapterNavigate | undefined>()
13
13
  const location = useLocation()
14
+ // Track programmatic navigations to avoid duplicate notifications to Studio
15
+ const isProgrammaticNavRef = useRef(false)
14
16
 
15
17
  useEffect(() => {
16
18
  navigateRemixRef.current = navigateRemix
17
19
  }, [navigateRemix])
18
20
 
19
21
  useEffect(() => {
20
- if (navigate) {
22
+ // Skip notification for programmatic navigations (initiated by Studio)
23
+ // to avoid duplicate notifications and potential infinite loops
24
+ if (navigate && !isProgrammaticNavRef.current) {
21
25
  navigate({
22
26
  type: 'push',
23
27
  url: `${location.pathname}${location.search}${location.hash}`,
24
28
  })
25
29
  }
30
+ isProgrammaticNavRef.current = false
26
31
  }, [location.hash, location.pathname, location.search, navigate])
27
32
 
28
33
  const historyAdapter: HistoryAdapter = useMemo(
@@ -32,6 +37,8 @@ export function useHistory(): HistoryAdapter {
32
37
  return () => setNavigate(undefined)
33
38
  },
34
39
  update(update: HistoryUpdate) {
40
+ // Mark as programmatic navigation to skip notification in the location effect
41
+ isProgrammaticNavRef.current = true
35
42
  if (update.type === 'push' || update.type === 'replace') {
36
43
  navigateRemixRef.current(update.url, {
37
44
  replace: update.type === 'replace',
@@ -1,6 +1,7 @@
1
1
  import type {HistoryRefresh} from '@sanity/visual-editing'
2
- import {useCallback, useState} from 'react'
2
+ import {startTransition, useCallback, useEffect, useState} from 'react'
3
3
  import {useRevalidator} from 'react-router'
4
+ import {useEffectEvent} from 'use-effect-event'
4
5
 
5
6
  import type {Revalidator} from '../types'
6
7
 
@@ -16,22 +17,28 @@ export function useRefresh(): {
16
17
  revalidator: Revalidator,
17
18
  ) => false | Promise<void>,
18
19
  ) => (payload: HistoryRefresh) => false | Promise<void>
19
- handleRevalidatorState: () => void
20
- revalidatorState: Revalidator['state']
21
20
  } {
22
21
  const revalidator = useRevalidator()
23
22
  const [revalidatorPromise, setRevalidatorPromise] = useState<(() => void) | null>(null)
24
23
  const [revalidatorLoading, setRevalidatorLoading] = useState(false)
25
24
 
26
- const handleRevalidatorState = useCallback(() => {
25
+ // Handle revalidator state transitions internally
26
+ const handleRevalidatorState = useEffectEvent(() => {
27
27
  if (revalidatorPromise && revalidator.state === 'loading') {
28
- setRevalidatorLoading(true)
28
+ startTransition(() => setRevalidatorLoading(true))
29
29
  } else if (revalidatorPromise && revalidatorLoading && revalidator.state === 'idle') {
30
30
  revalidatorPromise()
31
- setRevalidatorPromise(null)
32
- setRevalidatorLoading(false)
31
+ startTransition(() => {
32
+ setRevalidatorPromise(null)
33
+ setRevalidatorLoading(false)
34
+ })
33
35
  }
34
- }, [revalidatorLoading, revalidator.state, revalidatorPromise])
36
+ })
37
+
38
+ // Automatically handle revalidator state changes
39
+ useEffect(() => {
40
+ handleRevalidatorState()
41
+ }, [revalidator.state])
35
42
 
36
43
  const createRefreshHandler = useCallback(
37
44
  (
@@ -69,7 +76,5 @@ export function useRefresh(): {
69
76
 
70
77
  return {
71
78
  refreshHandler: createRefreshHandler,
72
- handleRevalidatorState,
73
- revalidatorState: revalidator.state,
74
79
  }
75
80
  }