hydrogen-sanity 6.1.1 → 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.
@@ -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);
@@ -36,6 +37,16 @@ function getPerspective(session) {
36
37
  validateApiPerspective(perspective);
37
38
  return perspective;
38
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
+ }
39
50
  function isSanityPreviewSession(session) {
40
51
  return isHydrogenSession(session) && "has" in session && typeof session.has === "function" && "destroy" in session && typeof session.destroy === "function";
41
52
  }
@@ -46,5 +57,5 @@ function isServer() {
46
57
  return typeof document === "undefined";
47
58
  }
48
59
 
49
- export { getPerspective, hashQuery, isHydrogenSession, isSanityPreviewSession, isServer, sanitizePerspective, supportsPerspectiveStack };
60
+ export { getPerspective, getPerspectiveFromUrl, hashQuery, isHydrogenSession, isSanityPreviewSession, isServer, sanitizePerspective, supportsPerspectiveStack };
50
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 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 * 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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"}
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
- if (supportsPerspectiveStack(apiVersion2)) {
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
- if (!didInitializeLoader) {
82
- const { setServerClient } = await import('@sanity/react-loader');
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
- return await loadQuery(query, params, loaderOptions);
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.1.1",
3
+ "version": "6.2.0",
4
4
  "description": "Sanity.io toolkit for Hydrogen",
5
5
  "keywords": [
6
6
  "sanity",
@@ -439,6 +439,131 @@ describe('stegaEnabled serialization', () => {
439
439
  })
440
440
  })
441
441
 
442
+ describe('perspective resolution priority', () => {
443
+ beforeEach(() => {
444
+ vi.clearAllMocks()
445
+ })
446
+
447
+ it('should use URL param perspective over session value', async () => {
448
+ const previewSession = new PreviewSession()
449
+ previewSession.set('projectId', projectId)
450
+ previewSession.set('perspective', 'drafts')
451
+
452
+ const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts')
453
+
454
+ const context = await createSanityContext({
455
+ request,
456
+ cache,
457
+ client,
458
+ preview: {
459
+ token: 'my-token',
460
+ session: previewSession,
461
+ },
462
+ })
463
+
464
+ expect(context.client.config().perspective).toEqual(['releaseId', 'drafts'])
465
+ })
466
+
467
+ it('should fall back to session perspective when URL param is absent', async () => {
468
+ const previewSession = new PreviewSession()
469
+ previewSession.set('projectId', projectId)
470
+ previewSession.set('perspective', 'drafts')
471
+
472
+ const request = new Request('https://example.com/')
473
+
474
+ const context = await createSanityContext({
475
+ request,
476
+ cache,
477
+ client,
478
+ preview: {
479
+ token: 'my-token',
480
+ session: previewSession,
481
+ },
482
+ })
483
+
484
+ expect(context.client.config().perspective).toEqual(['drafts'])
485
+ })
486
+
487
+ it('should pass perspective explicitly to loadQuery in preview mode', async () => {
488
+ const previewSession = new PreviewSession()
489
+ previewSession.set('projectId', projectId)
490
+ previewSession.set('perspective', 'drafts')
491
+
492
+ const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts')
493
+
494
+ const context = await createSanityContext({
495
+ request,
496
+ cache,
497
+ client,
498
+ preview: {
499
+ token: 'my-token',
500
+ session: previewSession,
501
+ },
502
+ })
503
+
504
+ await context.loadQuery<boolean>(query, params)
505
+
506
+ expect(loadQuery).toHaveBeenCalledWith(
507
+ query,
508
+ params,
509
+ expect.objectContaining({
510
+ perspective: ['releaseId', 'drafts'],
511
+ }),
512
+ )
513
+ })
514
+
515
+ it('should ignore URL param perspective stack when API version is too old', async () => {
516
+ const previewSession = new PreviewSession()
517
+ previewSession.set('projectId', projectId)
518
+
519
+ const oldClient = createClient({
520
+ projectId,
521
+ dataset: 'my-dataset',
522
+ apiVersion: '2024-01-01',
523
+ })
524
+
525
+ const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts')
526
+
527
+ const context = await createSanityContext({
528
+ request,
529
+ cache,
530
+ client: oldClient,
531
+ preview: {
532
+ token: 'my-token',
533
+ session: previewSession,
534
+ },
535
+ })
536
+
537
+ // Should fall back to previewDrafts since API version doesn't support stacks
538
+ expect(context.client.config().perspective).toBe('previewDrafts')
539
+ })
540
+
541
+ it('should accept single URL param perspective even with old API version', async () => {
542
+ const previewSession = new PreviewSession()
543
+ previewSession.set('projectId', projectId)
544
+
545
+ const oldClient = createClient({
546
+ projectId,
547
+ dataset: 'my-dataset',
548
+ apiVersion: '2024-01-01',
549
+ })
550
+
551
+ const request = new Request('https://example.com/?sanity-preview-perspective=drafts')
552
+
553
+ const context = await createSanityContext({
554
+ request,
555
+ cache,
556
+ client: oldClient,
557
+ preview: {
558
+ token: 'my-token',
559
+ session: previewSession,
560
+ },
561
+ })
562
+
563
+ expect(context.client.config().perspective).toBe('drafts')
564
+ })
565
+ })
566
+
442
567
  describe('lazy-initialize loaders', () => {
443
568
  const request = new Request('https://example.com')
444
569
 
@@ -456,22 +581,21 @@ describe('lazy-initialize loaders', () => {
456
581
  expect(setServerClient).not.toHaveBeenCalled()
457
582
  })
458
583
 
459
- it('should allow `loadQuery` to be called not in preview mode', async () => {
584
+ it('should call `setServerClient` on every `loadQuery` invocation', async () => {
460
585
  const context = await createSanityContext({
461
586
  request,
462
587
  cache,
463
588
  client,
464
589
  })
465
590
 
466
- // loadQuery should work in non-preview mode (backwards compatibility)
467
- // The actual loadQuery function will be called from the mocked module
468
591
  await context.loadQuery<boolean>(query, params)
592
+ expect(setServerClient).toHaveBeenCalledTimes(1)
469
593
 
470
- // Verify the mock was called (actual behavior testing is done in other tests)
471
- expect(loadQuery).toHaveBeenCalled()
594
+ await context.loadQuery<boolean>(query, params)
595
+ expect(setServerClient).toHaveBeenCalledTimes(2)
472
596
  })
473
597
 
474
- it('should call `setServerClient` on first `loadQuery` invocation in preview mode', async () => {
598
+ it('should call `setServerClient` with the preview-configured client', async () => {
475
599
  const previewSession = new PreviewSession()
476
600
  previewSession.set('projectId', projectId)
477
601
 
@@ -485,17 +609,11 @@ describe('lazy-initialize loaders', () => {
485
609
  },
486
610
  })
487
611
 
488
- // First call to `loadQuery`
489
612
  await context.loadQuery<boolean>(query, params)
490
613
 
491
- // Should be called with the preview-configured client
492
- // Check the most recent call
493
- const latestCall = setServerClient.mock.calls[setServerClient.mock.calls.length - 1]
494
- if (latestCall) {
495
- const calledWithClient = latestCall[0]
496
- expect(calledWithClient.config().useCdn).toBe(false)
497
- expect(calledWithClient.config().token).toBe('my-token')
498
- }
614
+ const calledWithClient = setServerClient.mock.calls[0][0]
615
+ expect(calledWithClient.config().useCdn).toBe(false)
616
+ expect(calledWithClient.config().token).toBe('my-token')
499
617
  })
500
618
 
501
619
  it('should display warning when `loadQuery` called outside preview mode in development', async () => {
package/src/context.ts CHANGED
@@ -18,13 +18,12 @@ import type {SanityPreviewSession} from './preview/session'
18
18
  import {isPreviewEnabled} from './preview/utils'
19
19
  import {SanityProvider, type SanityProviderValue} from './provider'
20
20
  import type {CacheActionFunctionParam, WaitUntil} from './types'
21
- import {getPerspective} from './utils'
21
+ import {getPerspective, getPerspectiveFromUrl} from './utils'
22
22
  import {hashQuery, supportsPerspectiveStack} from './utils'
23
23
 
24
24
  let didWarnAboutNoApiVersion = false
25
25
  let didWarnAboutNoPerspectiveSupport = false
26
26
  let didWarnAboutLoadQuery = false
27
- let didInitializeLoader = false
28
27
 
29
28
  export type CreateSanityContextOptions = {
30
29
  request: Request
@@ -213,7 +212,16 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
213
212
  if (previewEnabled) {
214
213
  const apiVersion = client.config().apiVersion
215
214
  let perspective: ClientPerspective
216
- if (supportsPerspectiveStack(apiVersion)) {
215
+
216
+ // Prefer URL param over session — the cookie may lag behind the iframe reload.
217
+ const urlPerspective = getPerspectiveFromUrl(request.url)
218
+
219
+ if (
220
+ urlPerspective !== undefined &&
221
+ !(Array.isArray(urlPerspective) && !supportsPerspectiveStack(apiVersion))
222
+ ) {
223
+ perspective = urlPerspective
224
+ } else if (supportsPerspectiveStack(apiVersion)) {
217
225
  perspective = getPerspective(preview.session)
218
226
  } else {
219
227
  if (process.env.NODE_ENV === 'development' && !didWarnAboutNoPerspectiveSupport) {
@@ -256,12 +264,8 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
256
264
  params: QueryParams | QueryWithoutParams,
257
265
  loaderOptions?: LoadQueryOptions<ClientReturn<Query, Result>>,
258
266
  ): Promise<QueryResponseInitial<ClientReturn<Query, Result>>> {
259
- // Lazy initialize the loader on first call with the configured client
260
- if (!didInitializeLoader) {
261
- const {setServerClient} = await import('@sanity/react-loader')
262
- setServerClient(client)
263
- didInitializeLoader = true
264
- }
267
+ const {setServerClient} = await import('@sanity/react-loader')
268
+ setServerClient(client)
265
269
 
266
270
  // Warn users to migrate to `query` method when using loadQuery outside preview mode
267
271
  if (!previewEnabled && process.env.NODE_ENV === 'development' && !didWarnAboutLoadQuery) {
@@ -273,7 +277,12 @@ You can find the latest version in the Sanity changelog: https://www.sanity.io/c
273
277
 
274
278
  if (!withCache || previewEnabled) {
275
279
  const {loadQuery} = await import('@sanity/react-loader')
276
- return await loadQuery<ClientReturn<Query, Result>>(query, params, loaderOptions)
280
+ // Override the singleton's possibly-stale perspective with the per-request value.
281
+ const resolvedOptions =
282
+ previewEnabled && !loaderOptions?.perspective
283
+ ? {...loaderOptions, perspective: client.config().perspective as ClientPerspective}
284
+ : loaderOptions
285
+ return await loadQuery<ClientReturn<Query, Result>>(query, params, resolvedOptions)
277
286
  }
278
287
 
279
288
  const cacheStrategy =
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export {createSanityContext, type SanityContext} from './context'
3
3
  export {useImageUrl, useImageUrlBuilder} from './image'
4
4
  export {Sanity, useSanityProviderValue} from './provider'
5
5
  export {Query, type QueryProps} from './Query'
6
+ export {getPerspectiveFromUrl} from './utils'
6
7
  export {useQuery} from './visual-editing/useQuery'
7
8
  export {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', () => {
@@ -105,6 +105,47 @@ describe('getPerspective', () => {
105
105
  })
106
106
  })
107
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
+ })
147
+ })
148
+
108
149
  describe('isPreviewEnabled', () => {
109
150
  const projectId = 'test-project-id'
110
151
 
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'
@@ -95,6 +96,21 @@ export function getPerspective(session: SanityPreviewSession | HydrogenSession):
95
96
  return perspective
96
97
  }
97
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
+
98
114
  /**
99
115
  * Type guard that checks if a session object is a SanityPreviewSession.
100
116
  * Validates presence of required methods: has, destroy (in addition to Hydrogen session methods).