UNPKG

hydrogen-sanity

Version:
381 lines (330 loc) 13.6 kB
import { type Any, type ClientConfig, type ClientPerspective, type ClientReturn, createClient, type QueryParams, type QueryWithoutParams, type ResponseQueryOptions, SanityClient, } from '@sanity/client' import type {QueryResponseInitial} from '@sanity/react-loader' import {type CachingStrategy, createWithCache, type HydrogenSession} from '@shopify/hydrogen' import {createElement, type PropsWithChildren, type ReactNode} from 'react' import {DEFAULT_API_VERSION, DEFAULT_CACHE_STRATEGY} from './constants' import type {SanityPreviewSession} from './preview/session' import {isPreviewEnabled} from './preview/utils' import {SanityProvider, type SanityProviderValue} from './provider' import type {CacheActionFunctionParam, WaitUntil} from './types' import {getPerspective, getPerspectiveFromUrl} from './utils' import {hashQuery, supportsPerspectiveStack} from './utils' let didWarnAboutNoApiVersion = false let didWarnAboutNoPerspectiveSupport = false let didWarnAboutLoadQuery = false export type CreateSanityContextOptions = { request: Request cache?: Cache | undefined waitUntil?: WaitUntil | undefined /** * Sanity client or configuration to use. */ client: SanityClient | ClientConfig /** * The default caching strategy to use for `loadQuery` subrequests. * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies * * Defaults to `CacheLong` */ defaultStrategy?: CachingStrategy | null /** * Configuration for enabling preview mode. */ preview?: { token: string session: SanityPreviewSession | HydrogenSession } } interface RequestInit { hydrogen?: { /** * The caching strategy to use for the subrequest. * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies */ cache?: CachingStrategy /** * Optional debugging information to be displayed in the subrequest profiler. * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request */ debug?: { displayName: string } } } type HydrogenResponseQueryOptions = Omit<ResponseQueryOptions, 'next' | 'cache'> & { hydrogen?: 'hydrogen' extends keyof RequestInit ? RequestInit['hydrogen'] : never } export type LoadQueryOptions<T> = Pick< HydrogenResponseQueryOptions, 'perspective' | 'hydrogen' | 'useCdn' | 'stega' | 'headers' | 'tag' > & { hydrogen?: { /** * The caching strategy to use for the subrequest. * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies */ cache?: CachingStrategy /** * Optional debugging information to be displayed in the subrequest profiler. * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request */ debug?: { displayName: string } /** * Whether to cache the result of the query or not. * @defaultValue () => true */ shouldCacheResult?: (value: QueryResponseInitial<T>) => boolean } } export type FetchOptions<T> = HydrogenResponseQueryOptions & { hydrogen?: { /** * The caching strategy to use for the subrequest. * @see https://shopify.dev/docs/custom-storefronts/hydrogen/caching#caching-strategies */ cache?: CachingStrategy /** * Optional debugging information to be displayed in the subrequest profiler. * @see https://shopify.dev/docs/custom-storefronts/hydrogen/debugging/subrequest-profiler#how-to-provide-more-debug-information-for-a-request */ debug?: { displayName: string } /** * Whether to cache the result of the query or not. * @defaultValue () => true */ shouldCacheResult?: (value: QueryResponseInitial<T>) => boolean } } export interface SanityContext { /** * Query Sanity using the loader. * @see https://www.sanity.io/docs/loaders */ loadQuery<Result = Any, Query extends string = string>( query: Query, params?: QueryParams | QueryWithoutParams, options?: LoadQueryOptions<ClientReturn<Query, Result>>, ): Promise<QueryResponseInitial<ClientReturn<Query, Result>>> /** * Query Sanity using direct client fetch with Hydrogen caching. * Use this when you need direct client results without react-loader integration. * Automatically disables caching in preview mode for real-time updates. */ fetch<Result = Any, Query extends string = string>( query: Query, params?: QueryParams | QueryWithoutParams, options?: FetchOptions<Result>, ): Promise<ClientReturn<Query, Result>> /** * Conditionally query Sanity using either loadQuery (for preview mode) or fetch (for static mode). * This optimizes bundle size by only loading @sanity/react-loader dependencies when in preview mode. */ query<Result = Any, Query extends string = string>( query: Query, params?: QueryParams | QueryWithoutParams, options?: LoadQueryOptions<ClientReturn<Query, Result>> & FetchOptions<Result>, ): Promise<QueryResponseInitial<ClientReturn<Query, Result>> | ClientReturn<Query, Result>> /** * The Sanity client, automatically configured for preview mode when enabled. * Uses preview token, perspective, and CDN settings based on session state. */ client: SanityClient preview?: CreateSanityContextOptions['preview'] & { /** * Whether preview mode is currently enabled based on session detection */ enabled: boolean } SanityProvider: (props: PropsWithChildren<object>) => ReactNode } /** * @public */ export async function createSanityContext( options: CreateSanityContextOptions, ): Promise<SanityContext> { const {cache, waitUntil = () => Promise.resolve(), request, preview, defaultStrategy} = options const withCache = cache ? createWithCache({cache, waitUntil, request}) : null let client = options.client instanceof SanityClient ? options.client : createClient(options.client) if (client.config().apiVersion === '1') { if (process.env.NODE_ENV === 'development' && !didWarnAboutNoApiVersion) { console.warn( ` No API version specified, defaulting to \`${DEFAULT_API_VERSION}\` which supports perspectives and Content Releases. You can find the latest version in the Sanity changelog: https://www.sanity.io/changelog. `.trim(), ) didWarnAboutNoApiVersion = true } client = client.withConfig({apiVersion: DEFAULT_API_VERSION}) } // Determine if preview is enabled and configure the client accordingly let previewEnabled = false if (preview) { if (!preview.token) { throw new Error('Enabling preview mode requires a token.') } previewEnabled = isPreviewEnabled(client.config().projectId!, preview.session) if (previewEnabled) { const apiVersion = client.config().apiVersion let perspective: ClientPerspective // Prefer URL param over session — the cookie may lag behind the iframe reload. const urlPerspective = getPerspectiveFromUrl(request.url) if ( urlPerspective !== undefined && !(Array.isArray(urlPerspective) && !supportsPerspectiveStack(apiVersion)) ) { perspective = urlPerspective } else if (supportsPerspectiveStack(apiVersion)) { perspective = getPerspective(preview.session) } else { if (process.env.NODE_ENV === 'development' && !didWarnAboutNoPerspectiveSupport) { console.warn( `API version \`${apiVersion}\` does not support perspective stacks. Using \`previewDrafts\` perspective. Consider upgrading to \`v2025-02-19\` or later for full perspective support.`, ) didWarnAboutNoPerspectiveSupport = true } perspective = 'previewDrafts' } client = client.withConfig({ useCdn: false, token: preview.token, perspective, }) } } // Server client will be initialized lazily on first loadQuery call const {apiHost, projectId, dataset, apiVersion} = client.config() const providerValue: SanityProviderValue = { projectId: projectId!, dataset: dataset!, apiHost, apiVersion: apiVersion!, previewEnabled, perspective: client.config().perspective || 'published', stegaEnabled: client.config().stega?.enabled ?? false, } return { /** * Loads a Sanity query with client-side loader support and Hydrogen cache integration. * Bypasses Hydrogen cache in preview mode. */ async loadQuery<Result = Any, Query extends string = string>( query: Query, params: QueryParams | QueryWithoutParams, loaderOptions?: LoadQueryOptions<ClientReturn<Query, Result>>, ): Promise<QueryResponseInitial<ClientReturn<Query, Result>>> { const {setServerClient} = await import('@sanity/react-loader') setServerClient(client) // Warn users to migrate to `query` method when using loadQuery outside preview mode if (!previewEnabled && process.env.NODE_ENV === 'development' && !didWarnAboutLoadQuery) { console.warn( `\`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.`, ) didWarnAboutLoadQuery = true } if (!withCache || previewEnabled) { const {loadQuery} = await import('@sanity/react-loader') // Override the singleton's possibly-stale perspective with the per-request value. const resolvedOptions = previewEnabled && !loaderOptions?.perspective ? {...loaderOptions, perspective: client.config().perspective as ClientPerspective} : loaderOptions return await loadQuery<ClientReturn<Query, Result>>(query, params, resolvedOptions) } const cacheStrategy = loaderOptions?.hydrogen?.cache || defaultStrategy || DEFAULT_CACHE_STRATEGY const queryHash = await hashQuery(query, params) const shouldCacheResult = loaderOptions?.hydrogen?.shouldCacheResult ?? (() => true) return await withCache.run( {cacheKey: queryHash, cacheStrategy, shouldCacheResult}, async ({ addDebugData, }: CacheActionFunctionParam): Promise< QueryResponseInitial<ClientReturn<Query, Result>> > => { // Name displayed in the subrequest profiler const displayName = loaderOptions?.hydrogen?.debug?.displayName || 'query Sanity' addDebugData({ displayName, }) const {loadQuery} = await import('@sanity/react-loader') return await loadQuery<ClientReturn<Query, Result>>(query, params, loaderOptions) }, ) }, /** * Executes a Sanity query with Hydrogen cache integration. * Direct client fetch without loader integration. Bypasses cache in preview mode. */ async fetch<Result = Any, Query extends string = string>( query: Query, params: QueryParams | QueryWithoutParams = {}, fetchOptions?: Pick< LoadQueryOptions<Result>, 'perspective' | 'hydrogen' | 'useCdn' | 'headers' | 'tag' >, ): Promise<ClientReturn<Query, Result>> { if (!withCache || previewEnabled) { return await client.fetch<ClientReturn<Query, Result>>(query, params, fetchOptions) } const cacheStrategy = fetchOptions?.hydrogen?.cache || defaultStrategy || DEFAULT_CACHE_STRATEGY const queryHash = await hashQuery(query, params) return await withCache.run( {cacheKey: queryHash, cacheStrategy, shouldCacheResult: () => true}, async ({addDebugData}: CacheActionFunctionParam): Promise<ClientReturn<Query, Result>> => { // Name displayed in the subrequest profiler const displayName = fetchOptions?.hydrogen?.debug?.displayName || 'fetch Sanity' addDebugData({ displayName, }) return await client.fetch<ClientReturn<Query, Result>>(query, params, fetchOptions) }, ) }, /** * Automatic query method that automatically adapts based on preview mode state. * Uses `loadQuery` (with client-side loader integration) when preview is enabled, `fetch` otherwise. * Bypasses cache in preview mode. */ async query<Result = Any, Query extends string = string>( query: Query, params?: QueryParams | QueryWithoutParams, queryOptions?: LoadQueryOptions<ClientReturn<Query, Result>> & FetchOptions<Result>, ): Promise<QueryResponseInitial<ClientReturn<Query, Result>> | ClientReturn<Query, Result>> { return await (previewEnabled ? this.loadQuery : this.fetch)(query, params, queryOptions) }, /** The configured Sanity client instance */ client, /** Preview configuration with session-based state, undefined when preview is not configured */ preview: preview ? {...preview, enabled: previewEnabled} : undefined, /** * React Provider component that serializes Sanity configuration across server-client boundary. */ SanityProvider({children}: PropsWithChildren<object>) { return createElement( SanityProvider, { value: Object.freeze(providerValue), }, children, ) }, } satisfies SanityContext }