hydrogen-sanity 6.1.0 → 6.2.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.
- package/dist/_chunks-es/LiveMode.client.js +8 -6
- package/dist/_chunks-es/LiveMode.client.js.map +1 -1
- package/dist/_chunks-es/Overlays.client.js +10 -3
- package/dist/_chunks-es/Overlays.client.js.map +1 -1
- package/dist/_chunks-es/refresh.js +6 -4
- package/dist/_chunks-es/refresh.js.map +1 -1
- package/dist/_chunks-es/utils.js +19 -3
- package/dist/_chunks-es/utils.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/context.test.ts +133 -15
- package/src/context.ts +19 -10
- package/src/index.ts +1 -0
- package/src/utils.test.ts +66 -1
- package/src/utils.ts +28 -2
- package/src/visual-editing/LiveMode.client.tsx +11 -6
- package/src/visual-editing/Overlays.client.tsx +8 -2
- package/src/visual-editing/hooks/history.ts +8 -1
- package/src/visual-editing/hooks/refresh.ts +6 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createClient } from '@sanity/client';
|
|
2
2
|
import { useLiveMode } from '@sanity/react-loader';
|
|
3
|
-
import
|
|
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,11 +14,12 @@ if (isServer()) {
|
|
|
13
14
|
function LiveModeClient(props) {
|
|
14
15
|
const { onConnect, onDisconnect, ...stegaProps } = props;
|
|
15
16
|
const sanityProvider = useSanityProviderValue();
|
|
16
|
-
const stableStegaProps =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const [stableStegaProps, setStableStegaProps] = useState(stegaProps);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!isEqual(stableStegaProps, stegaProps)) {
|
|
20
|
+
setStableStegaProps(stegaProps);
|
|
21
|
+
}
|
|
22
|
+
}, [stegaProps]);
|
|
21
23
|
const client = useMemo(() => {
|
|
22
24
|
const baseClient = createClient({
|
|
23
25
|
projectId: sanityProvider.projectId,
|
|
@@ -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, 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 //
|
|
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(
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
1
|
+
import { useState, startTransition, useEffect, useCallback } from 'react';
|
|
2
2
|
import { useRevalidator } from 'react-router';
|
|
3
3
|
import { useEffectEvent } from 'use-effect-event';
|
|
4
4
|
|
|
@@ -8,11 +8,13 @@ function useRefresh() {
|
|
|
8
8
|
const [revalidatorLoading, setRevalidatorLoading] = useState(false);
|
|
9
9
|
const handleRevalidatorState = useEffectEvent(() => {
|
|
10
10
|
if (revalidatorPromise && revalidator.state === "loading") {
|
|
11
|
-
setRevalidatorLoading(true);
|
|
11
|
+
startTransition(() => setRevalidatorLoading(true));
|
|
12
12
|
} else if (revalidatorPromise && revalidatorLoading && revalidator.state === "idle") {
|
|
13
13
|
revalidatorPromise();
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
startTransition(() => {
|
|
15
|
+
setRevalidatorPromise(null);
|
|
16
|
+
setRevalidatorLoading(false);
|
|
17
|
+
});
|
|
16
18
|
}
|
|
17
19
|
});
|
|
18
20
|
useEffect(() => {
|
|
@@ -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, 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
|
|
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;;;;"}
|
package/dist/_chunks-es/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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/core-loader/create-data-attribute'
|
|
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';
|
|
@@ -18,7 +18,6 @@ const DEFAULT_CACHE_STRATEGY = CacheLong();
|
|
|
18
18
|
let didWarnAboutNoApiVersion = false;
|
|
19
19
|
let didWarnAboutNoPerspectiveSupport = false;
|
|
20
20
|
let didWarnAboutLoadQuery = false;
|
|
21
|
-
let didInitializeLoader = false;
|
|
22
21
|
async function createSanityContext(options) {
|
|
23
22
|
const { cache, waitUntil = () => Promise.resolve(), request, preview, defaultStrategy } = options;
|
|
24
23
|
const withCache = cache ? createWithCache({ cache, waitUntil, request }) : null;
|
|
@@ -44,7 +43,10 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
|
|
|
44
43
|
if (previewEnabled) {
|
|
45
44
|
const apiVersion2 = client.config().apiVersion;
|
|
46
45
|
let perspective;
|
|
47
|
-
|
|
46
|
+
const urlPerspective = getPerspectiveFromUrl(request.url);
|
|
47
|
+
if (urlPerspective !== void 0 && !(Array.isArray(urlPerspective) && !supportsPerspectiveStack(apiVersion2))) {
|
|
48
|
+
perspective = urlPerspective;
|
|
49
|
+
} else if (supportsPerspectiveStack(apiVersion2)) {
|
|
48
50
|
perspective = getPerspective(preview.session);
|
|
49
51
|
} else {
|
|
50
52
|
if (process.env.NODE_ENV === "development" && !didWarnAboutNoPerspectiveSupport) {
|
|
@@ -78,11 +80,8 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
|
|
|
78
80
|
* Bypasses Hydrogen cache in preview mode.
|
|
79
81
|
*/
|
|
80
82
|
async loadQuery(query, params, loaderOptions) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
setServerClient(client);
|
|
84
|
-
didInitializeLoader = true;
|
|
85
|
-
}
|
|
83
|
+
const { setServerClient } = await import('@sanity/react-loader');
|
|
84
|
+
setServerClient(client);
|
|
86
85
|
if (!previewEnabled && process.env.NODE_ENV === "development" && !didWarnAboutLoadQuery) {
|
|
87
86
|
console.warn(
|
|
88
87
|
`\`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.`
|
|
@@ -91,7 +90,8 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
|
|
|
91
90
|
}
|
|
92
91
|
if (!withCache || previewEnabled) {
|
|
93
92
|
const { loadQuery } = await import('@sanity/react-loader');
|
|
94
|
-
|
|
93
|
+
const resolvedOptions = previewEnabled && !loaderOptions?.perspective ? { ...loaderOptions, perspective: client.config().perspective } : loaderOptions;
|
|
94
|
+
return await loadQuery(query, params, resolvedOptions);
|
|
95
95
|
}
|
|
96
96
|
const cacheStrategy = loaderOptions?.hydrogen?.cache || defaultStrategy || DEFAULT_CACHE_STRATEGY;
|
|
97
97
|
const queryHash = await hashQuery(query, params);
|
|
@@ -227,5 +227,5 @@ function useQuery(query, params, options) {
|
|
|
227
227
|
return useQuery$1(query, params, options);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
export { DEFAULT_API_VERSION, DEFAULT_CACHE_STRATEGY, Query, createSanityContext, useImageUrl, useImageUrlBuilder, useQuery, useSanityProviderValue };
|
|
230
|
+
export { DEFAULT_API_VERSION, DEFAULT_CACHE_STRATEGY, Query, createSanityContext, getPerspectiveFromUrl, useImageUrl, useImageUrlBuilder, useQuery, useSanityProviderValue };
|
|
231
231
|
//# 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":"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"}
|
|
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":";;;;;;;;;;;;;;AAGO,MAAM,mBAAA,GAAsB;AAG5B,MAAM,yBAA0C,SAAA;;ACiBvD,IAAI,wBAAA,GAA2B,KAAA;AAC/B,IAAI,gCAAA,GAAmC,KAAA;AACvC,IAAI,qBAAA,GAAwB,KAAA;AA0J5B,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;AAGJ,MAAA,MAAM,cAAA,GAAiB,qBAAA,CAAsB,OAAA,CAAQ,GAAG,CAAA;AAExD,MAAA,IACE,cAAA,KAAmB,MAAA,IACnB,EAAE,KAAA,CAAM,OAAA,CAAQ,cAAc,CAAA,IAAK,CAAC,wBAAA,CAAyBA,WAAU,CAAA,CAAA,EACvE;AACA,QAAA,WAAA,GAAc,cAAA;AAAA,MAChB,CAAA,MAAA,IAAW,wBAAA,CAAyBA,WAAU,CAAA,EAAG;AAC/C,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;AAC5D,MAAA,MAAM,EAAC,eAAA,EAAe,GAAI,MAAM,OAAO,sBAAsB,CAAA;AAC7D,MAAA,eAAA,CAAgB,MAAM,CAAA;AAGtB,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;AAEvD,QAAA,MAAM,eAAA,GACJ,cAAA,IAAkB,CAAC,aAAA,EAAe,WAAA,GAC9B,EAAC,GAAG,aAAA,EAAe,WAAA,EAAa,MAAA,CAAO,MAAA,EAAO,CAAE,aAAgC,GAChF,aAAA;AACN,QAAA,OAAO,MAAM,SAAA,CAAuC,KAAA,EAAO,MAAA,EAAQ,eAAe,CAAA;AAAA,MACpF;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;;ACjXO,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;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hydrogen-sanity",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Sanity.io toolkit for Hydrogen",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
"dependencies": {
|
|
130
130
|
"@sanity/comlink": "^4.0.1",
|
|
131
131
|
"@sanity/core-loader": "^2.0.5",
|
|
132
|
+
"fast-deep-equal": "^3.1.3",
|
|
132
133
|
"@sanity/image-url": "^2.0.3",
|
|
133
134
|
"@sanity/presentation-comlink": "^2.0.1",
|
|
134
135
|
"@sanity/preview-url-secret": "^4.0.2",
|
|
@@ -143,7 +144,7 @@
|
|
|
143
144
|
"@sanity/client": "^7.14.0",
|
|
144
145
|
"@sanity/pkg-utils": "^10.3.2",
|
|
145
146
|
"@sanity/semantic-release-preset": "^6.0.0",
|
|
146
|
-
"@shopify/hydrogen": "~
|
|
147
|
+
"@shopify/hydrogen": "~2026.1.0",
|
|
147
148
|
"@testing-library/react": "^16.3.1",
|
|
148
149
|
"@types/react": "^18.3.27",
|
|
149
150
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
@@ -166,7 +167,7 @@
|
|
|
166
167
|
},
|
|
167
168
|
"peerDependencies": {
|
|
168
169
|
"@sanity/client": "^7",
|
|
169
|
-
"@shopify/hydrogen": "~2025.5.0 || ~2025.7.0",
|
|
170
|
+
"@shopify/hydrogen": "~2025.5.0 || ~2025.7.0 || ~2026.1.0",
|
|
170
171
|
"react": "^18.2.0 || ^19.0.0",
|
|
171
172
|
"react-router": "^7.6.0",
|
|
172
173
|
"vite": "^5.1.0 || ^6.2.1"
|
package/src/context.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
471
|
-
expect(
|
|
594
|
+
await context.loadQuery<boolean>(query, params)
|
|
595
|
+
expect(setServerClient).toHaveBeenCalledTimes(2)
|
|
472
596
|
})
|
|
473
597
|
|
|
474
|
-
it('should call `setServerClient`
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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 {createDataAttribute} from '@sanity/core-loader/create-data-attribute'
|
|
8
9
|
export type {EncodeDataAttributeFunction} from '@sanity/core-loader/encode-data-attribute'
|
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
|
-
|
|
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
|
|
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
|
|
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,13 +46,17 @@ function LiveModeClient(props: LiveModeProps): ReactNode {
|
|
|
45
46
|
|
|
46
47
|
const sanityProvider = useSanityProviderValue()
|
|
47
48
|
|
|
48
|
-
//
|
|
49
|
+
// Maintain reference stability for stegaProps when content is unchanged
|
|
49
50
|
// This prevents unnecessary client recreation when parent component re-renders
|
|
50
|
-
const stableStegaProps =
|
|
51
|
-
|
|
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
|
|
52
58
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
-
|
|
54
|
-
)
|
|
59
|
+
}, [stegaProps])
|
|
55
60
|
|
|
56
61
|
const client = useMemo(() => {
|
|
57
62
|
const baseClient = createClient({
|
|
@@ -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(
|
|
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
|
-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import type {HistoryRefresh} from '@sanity/visual-editing'
|
|
2
|
-
import {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import {startTransition, useCallback, useEffect, useState} from 'react'
|
|
3
3
|
import {useRevalidator} from 'react-router'
|
|
4
4
|
import {useEffectEvent} from 'use-effect-event'
|
|
5
5
|
|
|
@@ -23,14 +23,15 @@ export function useRefresh(): {
|
|
|
23
23
|
const [revalidatorLoading, setRevalidatorLoading] = useState(false)
|
|
24
24
|
|
|
25
25
|
// Handle revalidator state transitions internally
|
|
26
|
-
// useEffectEvent provides stable identity while reading latest state
|
|
27
26
|
const handleRevalidatorState = useEffectEvent(() => {
|
|
28
27
|
if (revalidatorPromise && revalidator.state === 'loading') {
|
|
29
|
-
setRevalidatorLoading(true)
|
|
28
|
+
startTransition(() => setRevalidatorLoading(true))
|
|
30
29
|
} else if (revalidatorPromise && revalidatorLoading && revalidator.state === 'idle') {
|
|
31
30
|
revalidatorPromise()
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
startTransition(() => {
|
|
32
|
+
setRevalidatorPromise(null)
|
|
33
|
+
setRevalidatorLoading(false)
|
|
34
|
+
})
|
|
34
35
|
}
|
|
35
36
|
})
|
|
36
37
|
|