hydrogen-sanity 5.0.0 → 5.1.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/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # hydrogen-sanity
2
2
 
3
3
  > [!TIP]
4
- > **Upgrading from v4?** See the [migration guide](https://github.com/sanity-io/hydrogen-sanity/blob/main/package/MIGRATE-v4-to-v5.md) for breaking changes and new features. 🎉
4
+ > **Upgrading from v4?** See the [migration guide](https://github.com/sanity-io/hydrogen-sanity/blob/main/packages/hydrogen-sanity/MIGRATE-v4-to-v5.md) for breaking changes and new features. 🎉
5
5
 
6
- [Sanity.io](https://www.sanity.io) toolkit for [Hydrogen](https://hydrogen.shopify.dev/). Requires `@shopify/hydrogen >= 2025.5.1`.
6
+ [Sanity.io](https://www.sanity.io) toolkit for [Hydrogen](https://hydrogen.shopify.dev/). Requires `@shopify/hydrogen >= 2025.5.0`.
7
7
 
8
8
  Learn more about [getting started with Sanity](https://www.sanity.io/docs/getting-started).
9
9
 
10
10
  **Features:**
11
11
 
12
- - TypeScript support with [Sanity TypeGen](https://www.sanity.io/docs/sanity-typegen).
12
+ - Drop-in preview mode handling with pre-built route and session management.
13
13
  - Opinionated data fetching that automatically adapts to preview mode.
14
14
  - Interactive live preview with automatic loader detection for [Visual Editing](https://www.sanity.io/docs/loaders-and-overlays) in Sanity's Presentation tool.
15
+ - Optimized image URL generation hooks with Sanity's image URL builder.
16
+ - TypeScript support with [Sanity TypeGen](https://www.sanity.io/docs/sanity-typegen).
15
17
 
16
18
  > [!TIP]
17
19
  >
@@ -21,6 +23,7 @@ Learn more about [getting started with Sanity](https://www.sanity.io/docs/gettin
21
23
  - [Add Vite plugin](#add-vite-plugin)
22
24
  - [Usage](#usage)
23
25
  - [Satisfy TypeScript](#satisfy-typescript)
26
+ - [Set up the Sanity provider](#set-up-the-sanity-provider)
24
27
  - [Interacting with Sanity data](#interacting-with-sanity-data)
25
28
  - [Recommended: Using `query` and `Query` together](#recommended-using-query-and-query-together)
26
29
  - [Alternative: Cached queries using `loadQuery`](#alternative-cached-queries-using-loadquery)
@@ -84,27 +87,47 @@ export default defineConfig({
84
87
  Create the Sanity context and pass it through to your application, and optionally, configure the preview mode if you plan to setup Visual Editing
85
88
 
86
89
  > [!NOTE]
87
- > The examples below are up-to-date as of `npm create @shopify/hydrogen@2025.5.1`
90
+ > The examples below are up-to-date as of `npm create @shopify/hydrogen@2025.7.0`
88
91
 
89
92
  ```diff
90
93
  // ./lib/context.ts
91
94
 
92
95
  // ...all other imports
93
- + import {createSanityContext} from 'hydrogen-sanity'
96
+ + import {createSanityContext, type SanityContext} from 'hydrogen-sanity'
97
+
98
+ // Define the additional context object
99
+ const additionalContext = {
100
+ // Additional context for custom properties, CMS clients, 3P SDKs, etc.
101
+ // These will be available as both context.propertyName and context.get(propertyContext)
102
+ // Example of complex objects that could be added:
103
+ // cms: await createCMSClient(env),
104
+ // reviews: await createReviewsClient(env),
105
+ } as const;
106
+
107
+ // Automatically augment HydrogenAdditionalContext with the additional context type
108
+ type AdditionalContextType = typeof additionalContext;
109
+
110
+ declare global {
111
+ interface HydrogenAdditionalContext extends AdditionalContextType {
112
+ +
113
+ + // Augment `HydrogenAdditionalContext` with the Sanity context
114
+ + sanity: SanityContext;
115
+ }
116
+ }
94
117
 
95
- export async function createAppLoadContext(
118
+ export async function createHydrogenRouterContext(
96
119
  request: Request,
97
120
  env: Env,
98
121
  executionContext: ExecutionContext,
99
122
  ) {
100
- // ... Leave all other functions like the Hydrogen context as-is
123
+ // ... Leave all other functions as-is
101
124
  const waitUntil = executionContext.waitUntil.bind(executionContext)
102
125
  const [cache, session] = await Promise.all([
103
126
  caches.open('hydrogen'),
104
127
  AppSession.init(request, [env.SESSION_SECRET]),
105
128
  ])
106
129
 
107
- + // 1. Configure the Sanity Loader and preview mode
130
+ + // Initialize the Sanity context
108
131
  + const sanity = await createSanityContext({
109
132
  + request,
110
133
  +
@@ -132,12 +155,29 @@ export async function createAppLoadContext(
132
155
  + // strategy: CacheShort() | null,
133
156
  + })
134
157
 
135
- + // 2. Make Sanity available to loaders and actions in the request context
136
- return {
137
- ...hydrogenContext,
138
- + sanity,
139
- }
158
+
159
+ + // Make `sanity` available to loaders and actions in the request context
160
+ const hydrogenContext = createHydrogenContext(
161
+ {
162
+ env,
163
+ request,
164
+ cache,
165
+ waitUntil,
166
+ session,
167
+ i18n: {language: 'EN', country: 'US'},
168
+ cart: {
169
+ queryFragment: CART_QUERY_FRAGMENT,
170
+ },
171
+ },
172
+ + {
173
+ + ...additionalContext
174
+ + sanity,
175
+ + } as const,
176
+ + )
177
+ +
178
+ return hydrogenContext
140
179
  }
180
+
141
181
  ```
142
182
 
143
183
  Learn more about [Sanity's JavaScript client configuration](https://www.sanity.io/docs/js-client).
@@ -184,6 +224,73 @@ declare global {
184
224
  }
185
225
  ```
186
226
 
227
+ ### Set up the Sanity provider
228
+
229
+ You now need to wrap your app with the Sanity provider to make Sanity context available to client-side hooks and components like `useImageUrl` and `Query`.
230
+
231
+ **Update entry.server.tsx**
232
+
233
+ Wrap your app with the Sanity provider in your server entry point:
234
+
235
+ ```diff
236
+ // ./app/entry.server.tsx
237
+
238
+ export default async function handleRequest(
239
+ request: Request,
240
+ responseStatusCode: number,
241
+ responseHeaders: Headers,
242
+ reactRouterContext: EntryContext,
243
+ - context: AppLoadContext,
244
+ + context: HydrogenRouterContextProvider,
245
+ ) {
246
+ + const {SanityProvider} = context.sanity
247
+
248
+ // ... CSP setup etc ...
249
+
250
+ const body = await renderToReadableStream(
251
+ <NonceProvider>
252
+ + <SanityProvider>
253
+ <ServerRouter context={reactRouterContext} url={request.url} nonce={nonce} />
254
+ + </SanityProvider>
255
+ </NonceProvider>,
256
+ // ... render options
257
+ )
258
+ }
259
+ ```
260
+
261
+ **Update root.tsx**
262
+
263
+ Add the `Sanity` component to your root layout:
264
+
265
+ ```diff
266
+ // ./app/root.tsx
267
+
268
+ + import {Sanity} from 'hydrogen-sanity'
269
+
270
+ export function Layout({children}: {children?: React.ReactNode}) {
271
+ const nonce = useNonce()
272
+
273
+ return (
274
+ <html lang="en">
275
+ <head>
276
+ <meta charSet="utf-8" />
277
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
278
+ <Meta />
279
+ <Links />
280
+ </head>
281
+ <body>
282
+ {children}
283
+
284
+ + {/* Add Sanity client-side script */}
285
+ + <Sanity nonce={nonce} />
286
+ <ScrollRestoration nonce={nonce} />
287
+ <Scripts nonce={nonce} />
288
+ </body>
289
+ </html>
290
+ )
291
+ }
292
+ ```
293
+
187
294
  ## Interacting with Sanity data
188
295
 
189
296
  ### Recommended: Using `query` and `Query` together
@@ -495,16 +602,16 @@ For Visual Editing to work, you need to configure Sanity preview mode in your co
495
602
  // ./lib/context.ts
496
603
 
497
604
  // ...all other imports
498
- import {createSanityContext} from 'hydrogen-sanity'
605
+ import {createSanityContext, type SanityContext} from 'hydrogen-sanity'
499
606
  + import {PreviewSession} from 'hydrogen-sanity/preview/session'
500
607
  + import {isPreviewEnabled} from 'hydrogen-sanity/preview'
501
608
 
502
- export async function createAppLoadContext(
609
+ export async function createHydrogenRouterContext(
503
610
  request: Request,
504
611
  env: Env,
505
612
  executionContext: ExecutionContext,
506
613
  ) {
507
- // ... Leave all other functions like the Hydrogen context as-is
614
+ // ... Leave all other functions as-is
508
615
  const waitUntil = executionContext.waitUntil.bind(executionContext)
509
616
  - const [cache, session] = await Promise.all([
510
617
  + const [cache, session, previewSession] = await Promise.all([
@@ -537,11 +644,6 @@ export async function createAppLoadContext(
537
644
  + session: previewSession,
538
645
  + },
539
646
  })
540
-
541
- return {
542
- ...hydrogenContext,
543
- sanity,
544
- }
545
647
  }
546
648
  ```
547
649
 
@@ -675,18 +777,19 @@ Since Sanity Studio's Presentation tool displays the storefront inside an iframe
675
777
  // ./app/entry.server.tsx
676
778
 
677
779
  // ...all other imports
678
- + import type {AppLoadContext, EntryContext} from 'react-router'
679
780
 
680
781
  export default async function handleRequest(
681
782
  request: Request,
682
783
  responseStatusCode: number,
683
784
  responseHeaders: Headers,
684
785
  reactRouterContext: EntryContext,
685
- context: AppLoadContext,
786
+ - context: AppLoadContext,
787
+ + context: HydrogenRouterContextProvider,
686
788
  ) {
687
- + const projectId = context.env.SANITY_PROJECT_ID
688
- + const studioHostname = context.env.SANITY_STUDIO_HOSTNAME || 'http://localhost:3333'
689
- + const isPreviewEnabled = context.sanity.preview?.enabled
789
+ + const {env, sanity} = context
790
+ + const projectId = env.SANITY_PROJECT_ID
791
+ + const studioHostname = env.SANITY_STUDIO_HOSTNAME || 'http://localhost:3333'
792
+ + const isPreviewEnabled = sanity.preview?.enabled
690
793
 
691
794
  const {nonce, header, NonceProvider} = createContentSecurityPolicy({
692
795
  // If your storefront and Studio are on separate domains...
@@ -774,8 +877,31 @@ The following example configures Sanity Client and provides it in the request co
774
877
 
775
878
  // ...all other imports
776
879
  + import {createClient} from '@sanity/client'
880
+ + import {createSanityContext, type SanityContext} from 'hydrogen-sanity'
881
+
882
+ // Define the additional context object
883
+ const additionalContext = {
884
+ // Additional context for custom properties, CMS clients, 3P SDKs, etc.
885
+ // These will be available as both context.propertyName and context.get(propertyContext)
886
+ // Example of complex objects that could be added:
887
+ // cms: await createCMSClient(env),
888
+ // reviews: await createReviewsClient(env),
889
+ } as const;
890
+
777
891
 
778
- export async function createAppLoadContext(
892
+ // Automatically augment HydrogenAdditionalContext with the additional context type
893
+ type AdditionalContextType = typeof additionalContext;
894
+
895
+ declare global {
896
+ interface HydrogenAdditionalContext extends AdditionalContextType {
897
+ +
898
+ + // Augment `HydrogenAdditionalContext` with the Sanity context
899
+ + sanity: SanityContext;
900
+ + withCache: WithCache;
901
+ }
902
+ }
903
+
904
+ export async function createHydrogenRouterContext(
779
905
  request: Request,
780
906
  env: Env,
781
907
  executionContext: ExecutionContext,
@@ -791,14 +917,26 @@ export async function createAppLoadContext(
791
917
  + useCdn: process.env.NODE_ENV === 'production',
792
918
  + })
793
919
 
794
- // Pass it along to every request by
795
- // adding it to `handleRequest`
796
- return {
797
- ...hydrogenContext,
798
-
799
- + sanity,
800
- + withCache,
801
- }
920
+ const hydrogenContext = createHydrogenContext(
921
+ {
922
+ env,
923
+ request,
924
+ cache,
925
+ waitUntil,
926
+ session,
927
+ i18n: {language: 'EN', country: 'US'},
928
+ cart: {
929
+ queryFragment: CART_QUERY_FRAGMENT,
930
+ },
931
+ },
932
+ + {
933
+ + ...additionalContext,
934
+ + sanity,
935
+ + withCache,
936
+ + } as const,
937
+ + )
938
+ +
939
+ + return hydrogenContext
802
940
  }
803
941
  ```
804
942
 
@@ -829,8 +967,8 @@ export async function loader({context, params}: LoaderArgs) {
829
967
 
830
968
  ## Migration Guides
831
969
 
832
- - [From `v3` to `v4`](https://github.com/sanity-io/hydrogen-sanity/blob/main/package/MIGRATE-v3-to-v4.md)
833
- - [From `v4` to `v5`](https://github.com/sanity-io/hydrogen-sanity/blob/main/package/MIGRATE-v4-to-v5.md)
970
+ - [From `v3` to `v4`](https://github.com/sanity-io/hydrogen-sanity/blob/main/packages/hydrogen-sanity/MIGRATE-v3-to-v4.md)
971
+ - [From `v4` to `v5`](https://github.com/sanity-io/hydrogen-sanity/blob/main/packages/hydrogen-sanity/MIGRATE-v4-to-v5.md)
834
972
 
835
973
  ## License
836
974
 
@@ -1,8 +1,7 @@
1
1
  import { jsx, Fragment } from 'react/jsx-runtime';
2
- import { useEffect } from 'react';
3
2
 
4
3
  function assertSanityProviderValue(value) {
5
- if (!value) {
4
+ if (typeof value === "undefined") {
6
5
  throw new Error(
7
6
  "Failed to find a Sanity provider value. Did you forget to wrap your app in a `SanityProvider`?"
8
7
  );
@@ -18,9 +17,7 @@ function SanityProvider({
18
17
  value,
19
18
  children
20
19
  }) {
21
- useEffect(() => {
22
- setProviderValue(value);
23
- }, [value]);
20
+ setProviderValue(value);
24
21
  return /* @__PURE__ */ jsx(Fragment, { children });
25
22
  }
26
23
  function Sanity(props) {
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sources":["../../src/provider.tsx"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type {InitializedClientConfig} from '@sanity/client'\nimport type {HTMLProps, PropsWithChildren, ReactNode} from 'react'\nimport {useEffect} from 'react'\n\n/**\n * Contains essential Sanity client configuration and preview/stega state.\n */\nexport interface SanityProviderValue\n extends Required<\n Pick<\n InitializedClientConfig,\n 'projectId' | 'dataset' | 'apiHost' | 'apiVersion' | 'perspective'\n >\n > {\n previewEnabled: boolean\n stegaEnabled: boolean\n}\n\n/**\n * Type guard that asserts a value is a valid SanityProviderValue.\n * Throws an error if the provider value is missing or invalid.\n */\nexport function assertSanityProviderValue(value: unknown): value is SanityProviderValue {\n if (!value) {\n throw new Error(\n 'Failed to find a Sanity provider value. Did you forget to wrap your app in a `SanityProvider`?',\n )\n }\n\n return true\n}\n\n/**\n * Hook that retrieves the current Sanity provider configuration.\n * Must be used within a SanityProvider component tree.\n */\nexport function useSanityProviderValue(): SanityProviderValue {\n const providerValue = (globalThis as any)[\n Symbol.for('Sanity Provider')\n ] as SanityProviderValue | null\n\n assertSanityProviderValue(providerValue)\n\n return providerValue!\n}\n\n/**\n * Provider that makes Sanity configuration available to child components.\n * Serializes configuration across server-client boundary via globalThis.\n */\nexport function SanityProvider({\n value,\n children,\n}: PropsWithChildren<{value: SanityProviderValue}>): ReactNode {\n useEffect(() => {\n setProviderValue(value)\n }, [value])\n\n return <>{children}</>\n}\n\n/**\n * Script component that hydrates Sanity configuration on the client side.\n * Injects provider configuration into the client bundle for SSR compatibility.\n */\nexport function Sanity(props: SanityProps): ReactNode {\n const providerValue = useSanityProviderValue()\n\n assertSanityProviderValue(providerValue)\n\n return (\n <script\n {...props}\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{\n __html: `(${setProviderValue})(${JSON.stringify(providerValue)})`,\n }}\n suppressHydrationWarning\n />\n )\n}\n\n/**\n * Props for the Sanity script component.\n * Extends HTMLScriptElement props while excluding conflicting attributes.\n */\nexport type SanityProps = Omit<\n HTMLProps<HTMLScriptElement>,\n | 'children'\n | 'async'\n | 'defer'\n | 'src'\n | 'type'\n | 'noModule'\n | 'dangerouslySetInnerHTML'\n | 'suppressHydrationWarning'\n>\n\nfunction setProviderValue(value: SanityProviderValue) {\n ;(globalThis as any)[Symbol.for('Sanity Provider')] = Object.freeze(value)\n}\n"],"names":[],"mappings":";;;AAuBO,SAAS,0BAA0B,KAAA,EAA8C;AACtF,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,sBAAA,GAA8C;AAC5D,EAAA,MAAM,aAAA,GAAiB,UAAA,CACrB,MAAA,CAAO,GAAA,CAAI,iBAAiB,CAC9B,CAAA;AAEA,EAAA,yBAAA,CAA0B,aAAa,CAAA;AAEvC,EAAA,OAAO,aAAA;AACT;AAMO,SAAS,cAAA,CAAe;AAAA,EAC7B,KAAA;AAAA,EACA;AACF,CAAA,EAA+D;AAC7D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,gBAAA,CAAiB,KAAK,CAAA;AAAA,EACxB,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,uCAAU,QAAA,EAAS,CAAA;AACrB;AAMO,SAAS,OAAO,KAAA,EAA+B;AACpD,EAAA,MAAM,gBAAgB,sBAAA,EAAuB;AAE7C,EAAA,yBAAA,CAA0B,aAAa,CAAA;AAEvC,EAAA,uBACE,GAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACE,GAAG,KAAA;AAAA,MAEJ,uBAAA,EAAyB;AAAA,QACvB,QAAQ,CAAA,CAAA,EAAI,gBAAgB,KAAK,IAAA,CAAK,SAAA,CAAU,aAAa,CAAC,CAAA,CAAA;AAAA,OAChE;AAAA,MACA,wBAAA,EAAwB;AAAA;AAAA,GAC1B;AAEJ;AAkBA,SAAS,iBAAiB,KAAA,EAA4B;AACnD,EAAC,UAAA,CAAmB,OAAO,GAAA,CAAI,iBAAiB,CAAC,CAAA,GAAI,MAAA,CAAO,OAAO,KAAK,CAAA;AAC3E;;;;"}
1
+ {"version":3,"file":"provider.js","sources":["../../src/provider.tsx"],"sourcesContent":["import type {InitializedClientConfig} from '@sanity/client'\nimport type {HTMLProps, PropsWithChildren, ReactNode} from 'react'\n\n/**\n * Contains essential Sanity client configuration and preview/stega state.\n */\nexport interface SanityProviderValue\n extends Required<\n Pick<\n InitializedClientConfig,\n 'projectId' | 'dataset' | 'apiHost' | 'apiVersion' | 'perspective'\n >\n > {\n previewEnabled: boolean\n stegaEnabled: boolean\n}\n\n/**\n * Type guard that asserts a value is a valid SanityProviderValue.\n * Throws an error if the provider value is missing or invalid.\n */\nexport function assertSanityProviderValue(value: unknown): value is SanityProviderValue {\n if (typeof value === 'undefined') {\n throw new Error(\n 'Failed to find a Sanity provider value. Did you forget to wrap your app in a `SanityProvider`?',\n )\n }\n\n return true\n}\n\n/**\n * Hook that retrieves the current Sanity provider configuration.\n * Must be used within a SanityProvider component tree.\n */\nexport function useSanityProviderValue(): SanityProviderValue {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const providerValue = (globalThis as any)[Symbol.for('Sanity Provider')]\n assertSanityProviderValue(providerValue)\n return providerValue\n}\n\n/**\n * Provider that makes Sanity configuration available to child components.\n * Serializes configuration across server-client boundary via globalThis.\n */\nexport function SanityProvider({\n value,\n children,\n}: PropsWithChildren<{value: SanityProviderValue}>): ReactNode {\n setProviderValue(value)\n return <>{children}</>\n}\n\n/**\n * Script component that hydrates Sanity configuration on the client side.\n * Injects provider configuration into the client bundle for SSR compatibility.\n */\nexport function Sanity(props: SanityProps): ReactNode {\n const providerValue = useSanityProviderValue()\n assertSanityProviderValue(providerValue)\n\n return (\n <script\n {...props}\n // eslint-disable-next-line react/no-danger\n dangerouslySetInnerHTML={{\n __html: `(${setProviderValue})(${JSON.stringify(providerValue)})`,\n }}\n suppressHydrationWarning\n />\n )\n}\n\n/**\n * Props for the Sanity script component.\n * Extends HTMLScriptElement props while excluding conflicting attributes.\n */\nexport type SanityProps = Omit<\n HTMLProps<HTMLScriptElement>,\n | 'children'\n | 'async'\n | 'defer'\n | 'src'\n | 'type'\n | 'noModule'\n | 'dangerouslySetInnerHTML'\n | 'suppressHydrationWarning'\n>\n\nfunction setProviderValue(value: SanityProviderValue) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ;(globalThis as any)[Symbol.for('Sanity Provider')] = Object.freeze(value)\n}\n"],"names":[],"mappings":";;AAqBO,SAAS,0BAA0B,KAAA,EAA8C;AACtF,EAAA,IAAI,OAAO,UAAU,WAAA,EAAa;AAChC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,sBAAA,GAA8C;AAE5D,EAAA,MAAM,aAAA,GAAiB,UAAA,CAAmB,MAAA,CAAO,GAAA,CAAI,iBAAiB,CAAC,CAAA;AACvE,EAAA,yBAAA,CAA0B,aAAa,CAAA;AACvC,EAAA,OAAO,aAAA;AACT;AAMO,SAAS,cAAA,CAAe;AAAA,EAC7B,KAAA;AAAA,EACA;AACF,CAAA,EAA+D;AAC7D,EAAA,gBAAA,CAAiB,KAAK,CAAA;AACtB,EAAA,uCAAU,QAAA,EAAS,CAAA;AACrB;AAMO,SAAS,OAAO,KAAA,EAA+B;AACpD,EAAA,MAAM,gBAAgB,sBAAA,EAAuB;AAC7C,EAAA,yBAAA,CAA0B,aAAa,CAAA;AAEvC,EAAA,uBACE,GAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACE,GAAG,KAAA;AAAA,MAEJ,uBAAA,EAAyB;AAAA,QACvB,QAAQ,CAAA,CAAA,EAAI,gBAAgB,KAAK,IAAA,CAAK,SAAA,CAAU,aAAa,CAAC,CAAA,CAAA;AAAA,OAChE;AAAA,MACA,wBAAA,EAAwB;AAAA;AAAA,GAC1B;AAEJ;AAkBA,SAAS,iBAAiB,KAAA,EAA4B;AAEnD,EAAC,UAAA,CAAmB,OAAO,GAAA,CAAI,iBAAiB,CAAC,CAAA,GAAI,MAAA,CAAO,OAAO,KAAK,CAAA;AAC3E;;;;"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CacheLong, createWithCache } from '@shopify/hydrogen';
2
2
  import { SanityClient, createClient } from '@sanity/client';
3
- import { createElement, useMemo, lazy, useState, useEffect, startTransition, Suspense, useId } from 'react';
3
+ import { createElement, useMemo, lazy, Suspense, useSyncExternalStore, useId, useEffect } from 'react';
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';
@@ -163,6 +163,15 @@ function useImageUrl(source) {
163
163
  function SanityQueryFallback() {
164
164
  return null;
165
165
  }
166
+ function useIsHydrated() {
167
+ return useSyncExternalStore(
168
+ // eslint-disable-next-line no-empty-function
169
+ () => () => {
170
+ },
171
+ () => true,
172
+ () => false
173
+ );
174
+ }
166
175
  const QueryClient = isServer() ? SanityQueryFallback : lazy(
167
176
  () => (
168
177
  /**
@@ -183,12 +192,7 @@ function Query({
183
192
  ...suspenseProps
184
193
  }) {
185
194
  const isPreviewMode = usePreviewMode();
186
- const [isHydrated, setIsHydrated] = useState(false);
187
- useEffect(() => {
188
- startTransition(() => {
189
- setIsHydrated(true);
190
- });
191
- }, []);
195
+ const isHydrated = useIsHydrated();
192
196
  if (isPreviewMode && isHydrated) {
193
197
  return /* @__PURE__ */ jsx(Suspense, { ...suspenseProps, fallback: suspenseProps.fallback ?? /* @__PURE__ */ jsx(SanityQueryFallback, {}), children: /* @__PURE__ */ jsx(
194
198
  QueryClient,
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\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 // Set server client for react-loader when in preview mode\n const {setServerClient} = await import('@sanity/react-loader')\n setServerClient(client)\n }\n }\n\n // Server client will be initialized lazily on first loadQuery call\n\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 const sanityContext: SanityContext = {\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 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 }\n\n return sanityContext\n}\n","import {default as createImageUrlBuilder} from '@sanity/image-url'\nimport type {ImageUrlBuilder} from '@sanity/image-url/lib/types/builder'\nimport type {SanityImageSource, SanityModernClientLike} from '@sanity/image-url/lib/types/types'\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 {ImageUrlBuilder} from '@sanity/image-url/lib/types/builder'\nexport type * from '@sanity/image-url/lib/types/types'\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 {\n lazy,\n type ReactNode,\n startTransition,\n Suspense,\n type SuspenseProps,\n useEffect,\n useState,\n} 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\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>\n extends Omit<QueryClientProps<Result, Query>, 'options'> {\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, setIsHydrated] = useState(false)\n\n // Mark component as hydrated after initial render to prevent hydration mismatches\n useEffect(() => {\n startTransition(() => {\n setIsHydrated(true)\n })\n }, [])\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;AA0JvC,eAAsB,oBACpB,OAAA,EACwB;AACxB,EAAA,MAAM,EAAC,KAAA,EAAO,SAAA,GAAY,MAAM,OAAA,CAAQ,SAAQ,EAAG,OAAA,EAAS,OAAA,EAAS,eAAA,EAAe,GAAI,OAAA;AACxF,EAAA,MAAM,SAAA,GAAY,QAAQ,eAAA,CAAgB,EAAC,OAAO,SAAA,EAAW,OAAA,EAAQ,CAAA,GAAI,IAAA;AACzE,EAAA,IAAI,MAAA,GACF,QAAQ,MAAA,YAAkB,YAAA,GAAe,QAAQ,MAAA,GAAS,YAAA,CAAa,QAAQ,MAAM,CAAA;AAEvF,EAAA,IAAI,MAAA,CAAO,MAAA,EAAO,CAAE,UAAA,KAAe,GAAA,EAAK;AACtC,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa,aAAA,IAAiB,CAAC,wBAAA,EAA0B;AACvE,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN;AAAA,0CAAA,EACoC,mBAAmB,CAAA;AAAA;AAAA,IAAA,CAAA,CAEzD,IAAA;AAAK,OACL;AAEA,MAAA,wBAAA,GAA2B,IAAA;AAAA,IAC7B;AAEA,IAAA,MAAA,GAAS,MAAA,CAAO,UAAA,CAAW,EAAC,UAAA,EAAY,qBAAoB,CAAA;AAAA,EAC9D;AAGA,EAAA,IAAI,cAAA,GAAiB,KAAA;AACrB,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,IAAI,CAAC,QAAQ,KAAA,EAAO;AAClB,MAAA,MAAM,IAAI,MAAM,yCAAyC,CAAA;AAAA,IAC3D;AAEA,IAAA,cAAA,GAAiB,iBAAiB,MAAA,CAAO,MAAA,EAAO,CAAE,SAAA,EAAY,QAAQ,OAAO,CAAA;AAE7E,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,MAAMA,WAAAA,GAAa,MAAA,CAAO,MAAA,EAAO,CAAE,UAAA;AACnC,MAAA,IAAI,WAAA;AACJ,MAAA,IAAI,wBAAA,CAAyBA,WAAU,CAAA,EAAG;AACxC,QAAA,WAAA,GAAc,cAAA,CAAe,QAAQ,OAAO,CAAA;AAAA,MAC9C,CAAA,MAAO;AACL,QAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa,aAAA,IAAiB,CAAC,gCAAA,EAAkC;AAC/E,UAAA,OAAA,CAAQ,IAAA;AAAA,YACN,iBAAiBA,WAAU,CAAA,yJAAA;AAAA,WAC7B;AAEA,UAAA,gCAAA,GAAmC,IAAA;AAAA,QACrC;AACA,QAAA,WAAA,GAAc,eAAA;AAAA,MAChB;AAEA,MAAA,MAAA,GAAS,OAAO,UAAA,CAAW;AAAA,QACzB,MAAA,EAAQ,KAAA;AAAA,QACR,OAAO,OAAA,CAAQ,KAAA;AAAA,QACf;AAAA,OACD,CAAA;AAGD,MAAA,MAAM,EAAC,eAAA,EAAe,GAAI,MAAM,OAAO,sBAAsB,CAAA;AAC7D,MAAA,eAAA,CAAgB,MAAM,CAAA;AAAA,IACxB;AAAA,EACF;AAIA,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,MAAM,aAAA,GAA+B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKnC,MAAM,SAAA,CACJ,KAAA,EACA,MAAA,EACA,aAAA,EAC4D;AAC5D,MAAA,IAAI,CAAC,aAAa,cAAA,EAAgB;AAChC,QAAA,MAAM,EAAC,SAAA,EAAS,GAAI,MAAM,OAAO,sBAAsB,CAAA;AACvD,QAAA,OAAO,MAAM,SAAA,CAAuC,KAAA,EAAO,MAAA,EAAQ,aAAa,CAAA;AAAA,MAClF;AAEA,MAAA,MAAM,aAAA,GACJ,aAAA,EAAe,QAAA,EAAU,KAAA,IAAS,eAAA,IAAmB,sBAAA;AACvD,MAAA,MAAM,SAAA,GAAY,MAAM,SAAA,CAAU,KAAA,EAAO,MAAM,CAAA;AAC/C,MAAA,MAAM,iBAAA,GAAoB,aAAA,EAAe,QAAA,EAAU,iBAAA,KAAsB,MAAM,IAAA,CAAA;AAE/E,MAAA,OAAO,MAAM,SAAA,CAAU,GAAA;AAAA,QACrB,EAAC,QAAA,EAAU,SAAA,EAAW,aAAA,EAAe,iBAAA,EAAiB;AAAA,QACtD,OAAO;AAAA,UACL;AAAA,SACF,KAEK;AAEH,UAAA,MAAM,WAAA,GAAc,aAAA,EAAe,QAAA,EAAU,KAAA,EAAO,WAAA,IAAe,cAAA;AAEnE,UAAA,YAAA,CAAa;AAAA,YACX;AAAA,WACD,CAAA;AAED,UAAA,MAAM,EAAC,SAAA,EAAS,GAAI,MAAM,OAAO,sBAAsB,CAAA;AACvD,UAAA,OAAO,MAAM,SAAA,CAAuC,KAAA,EAAO,MAAA,EAAQ,aAAa,CAAA;AAAA,QAClF;AAAA,OACF;AAAA,IACF,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,KAAA,CACJ,KAAA,EACA,MAAA,GAA2C,IAC3C,YAAA,EAIsC;AACtC,MAAA,IAAI,CAAC,aAAa,cAAA,EAAgB;AAChC,QAAA,OAAO,MAAM,MAAA,CAAO,KAAA,CAAmC,KAAA,EAAO,QAAQ,YAAY,CAAA;AAAA,MACpF;AAEA,MAAA,MAAM,aAAA,GACJ,YAAA,EAAc,QAAA,EAAU,KAAA,IAAS,eAAA,IAAmB,sBAAA;AACtD,MAAA,MAAM,SAAA,GAAY,MAAM,SAAA,CAAU,KAAA,EAAO,MAAM,CAAA;AAE/C,MAAA,OAAO,MAAM,SAAA,CAAU,GAAA;AAAA,QACrB,EAAC,QAAA,EAAU,SAAA,EAAW,aAAA,EAAe,iBAAA,EAAmB,MAAM,IAAA,EAAI;AAAA,QAClE,OAAO,EAAC,YAAA,EAAY,KAAsE;AAExF,UAAA,MAAM,WAAA,GAAc,YAAA,EAAc,QAAA,EAAU,KAAA,EAAO,WAAA,IAAe,cAAA;AAElE,UAAA,YAAA,CAAa;AAAA,YACX;AAAA,WACD,CAAA;AAED,UAAA,OAAO,MAAM,MAAA,CAAO,KAAA,CAAmC,KAAA,EAAO,QAAQ,YAAY,CAAA;AAAA,QACpF;AAAA,OACF;AAAA,IACF,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAM,KAAA,CACJ,KAAA,EACA,MAAA,EACA,YAAA,EAC0F;AAC1F,MAAA,OAAO,MAAA,CAAO,iBAAiB,IAAA,CAAK,SAAA,GAAY,KAAK,KAAA,EAAO,KAAA,EAAO,QAAQ,YAAY,CAAA;AAAA,IACzF,CAAA;AAAA;AAAA,IAGA,MAAA;AAAA;AAAA,IAGA,SAAS,OAAA,GAAU,EAAC,GAAG,OAAA,EAAS,OAAA,EAAS,gBAAc,GAAI,MAAA;AAAA;AAAA;AAAA;AAAA,IAK3D,cAAA,CAAe,EAAC,QAAA,EAAQ,EAA8B;AACpD,MAAA,OAAO,aAAA;AAAA,QACL,cAAA;AAAA,QACA;AAAA,UACE,KAAA,EAAO,MAAA,CAAO,MAAA,CAAO,aAAa;AAAA,SACpC;AAAA,QACA;AAAA,OACF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,OAAO,aAAA;AACT;;AC7VO,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;;ACNA,SAAS,mBAAA,GAAiC;AACxC,EAAA,OAAO,IAAA;AACT;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;AAoBM,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,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAS,KAAK,CAAA;AAGlD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,eAAA,CAAgB,MAAM;AACpB,MAAA,aAAA,CAAc,IAAI,CAAA;AAAA,IACpB,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,IAAI,iBAAiB,UAAA,EAAY;AAC/B,IAAA,uBACE,GAAA,CAAC,YAAU,GAAG,aAAA,EAAe,UAAU,aAAA,CAAc,QAAA,oBAAY,GAAA,CAAC,mBAAA,EAAA,EAAoB,CAAA,EACpF,QAAA,kBAAA,GAAA;AAAA,MAAC,WAAA;AAAA,MAAA;AAAA,QACC,KAAA;AAAA,QACA,MAAA;AAAA,QACA,OAAA;AAAA,QAEC;AAAA;AAAA,KACH,EACF,CAAA;AAAA,EAEJ;AAGA,EAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,OAAA,EAAwC,uBAAuB,CAAA;AACzF;;ACrFO,SAAS,QAAA,CACd,KAAA,EACA,MAAA,EACA,OAAA,EACmD;AAEnD,EAAA,MAAM,KAAK,KAAA,EAAM;AAGjB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,UAAA,GAAa,cAAc,EAAE,CAAA;AACnC,IAAA,OAAO,UAAA;AAAA,EACT,CAAA,EAAG,CAAC,EAAE,CAAC,CAAA;AAGP,EAAA,OAAOC,UAAA,CAA+B,KAAA,EAAO,MAAA,EAAQ,OAAO,CAAA;AAC9D;;;;"}
1
+ {"version":3,"file":"index.js","sources":["../src/constants.ts","../src/context.ts","../src/image.ts","../src/Query.tsx","../src/visual-editing/useQuery.tsx"],"sourcesContent":["import {CacheLong, type CachingStrategy} from '@shopify/hydrogen'\n\n/** Default Sanity API version with perspective stack support */\nexport const DEFAULT_API_VERSION = 'v2025-02-19'\n\n/** Default Hydrogen caching strategy for Sanity queries */\nexport const DEFAULT_CACHE_STRATEGY: CachingStrategy = CacheLong()\n","import {\n type Any,\n type ClientConfig,\n type ClientPerspective,\n type ClientReturn,\n createClient,\n type QueryParams,\n type QueryWithoutParams,\n type ResponseQueryOptions,\n SanityClient,\n} from '@sanity/client'\nimport type {QueryResponseInitial} from '@sanity/react-loader'\nimport {type CachingStrategy, createWithCache, type HydrogenSession} from '@shopify/hydrogen'\nimport {createElement, type PropsWithChildren, type ReactNode} from 'react'\n\nimport {DEFAULT_API_VERSION, DEFAULT_CACHE_STRATEGY} from './constants'\nimport type {SanityPreviewSession} from './preview/session'\nimport {isPreviewEnabled} from './preview/utils'\nimport {SanityProvider, type SanityProviderValue} from './provider'\nimport type {CacheActionFunctionParam, WaitUntil} from './types'\nimport {getPerspective} from './utils'\nimport {hashQuery, supportsPerspectiveStack} from './utils'\n\nlet didWarnAboutNoApiVersion = false\nlet didWarnAboutNoPerspectiveSupport = 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 // Set server client for react-loader when in preview mode\n const {setServerClient} = await import('@sanity/react-loader')\n setServerClient(client)\n }\n }\n\n // Server client will be initialized lazily on first loadQuery call\n\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 const sanityContext: SanityContext = {\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 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 }\n\n return sanityContext\n}\n","import {default as createImageUrlBuilder} from '@sanity/image-url'\nimport type {ImageUrlBuilder} from '@sanity/image-url/lib/types/builder'\nimport type {SanityImageSource, SanityModernClientLike} from '@sanity/image-url/lib/types/types'\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 {ImageUrlBuilder} from '@sanity/image-url/lib/types/builder'\nexport type * from '@sanity/image-url/lib/types/types'\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>\n extends Omit<QueryClientProps<Result, Query>, 'options'> {\n query: Query\n params?: QueryParams | QueryWithoutParams\n options: {\n initial: ClientReturn<Query, Result> | QueryResponseInitial<ClientReturn<Query, Result>>\n } & LoadQueryOptions<ClientReturn<Query, Result>>\n children: (\n data: ClientReturn<Query, Result>,\n encodeDataAttribute: EncodeDataAttributeFunction,\n ) => ReactNode\n}\n\n/**\n * Query component that provides live updates in preview mode and static data otherwise.\n *\n * @public\n */\nexport function Query<Result = Any, Query extends string = string>({\n query,\n params,\n options,\n children,\n ...suspenseProps\n}: QueryProps<Result, Query> & Omit<SuspenseProps, 'children'>): ReactNode {\n const isPreviewMode = usePreviewMode()\n const isHydrated = useIsHydrated()\n\n // If in preview mode and hydrated, render the client component\n if (isPreviewMode && isHydrated) {\n return (\n <Suspense {...suspenseProps} fallback={suspenseProps.fallback ?? <SanityQueryFallback />}>\n <QueryClient<Result, Query>\n query={query}\n params={params}\n options={options as QueryClientProps<Result, Query>['options']}\n >\n {children}\n </QueryClient>\n </Suspense>\n )\n }\n\n // Render static data in non-preview mode or during hydration\n return children(options.initial as ClientReturn<Query, Result>, noopEncodeDataAttribute)\n}\n","import {useQuery as _useQuery, type UseQueryOptionsDefinedInitial} from '@sanity/react-loader'\nimport {useEffect, useId} from 'react'\n\nimport {registerQuery} from './registry'\n\n/**\n * Automatically registers with the query detection system.\n * This enables automatic live mode detection in `VisualEditing` components.\n */\nexport function useQuery<QueryResponseResult = unknown>(\n query: string,\n params?: Record<string, unknown>,\n options?: UseQueryOptionsDefinedInitial<QueryResponseResult>,\n): ReturnType<typeof _useQuery<QueryResponseResult>> {\n // Generate stable ID for this `useQuery` instance\n const id = useId()\n\n // Register this `useQuery` instance with the detection system\n useEffect(() => {\n const unregister = registerQuery(id)\n return unregister\n }, [id])\n\n // Call the original `useQuery` with all the same arguments\n return _useQuery<QueryResponseResult>(query, params, options)\n}\n"],"names":["apiVersion","_useQuery"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hydrogen-sanity",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Sanity.io toolkit for Hydrogen",
5
5
  "keywords": [
6
6
  "sanity",
@@ -109,7 +109,7 @@
109
109
  "@sanity/client": "^7.9.0",
110
110
  "@sanity/pkg-utils": "^7.11.9",
111
111
  "@sanity/semantic-release-preset": "^5.0.0",
112
- "@shopify/hydrogen": "~2025.5.0",
112
+ "@shopify/hydrogen": "~2025.7.0",
113
113
  "@testing-library/react": "^16.3.0",
114
114
  "@types/react": "^18.3.24",
115
115
  "@typescript-eslint/eslint-plugin": "^8.41.0",
@@ -124,7 +124,7 @@
124
124
  "groq": "^4.5.0",
125
125
  "react": "^18.3.1",
126
126
  "react-dom": "^18.3.1",
127
- "react-router": "7.6.0",
127
+ "react-router": "7.9.2",
128
128
  "semantic-release": "^24.2.7",
129
129
  "typescript": "^5.9.2",
130
130
  "vite": "^6.3.5",
@@ -132,9 +132,9 @@
132
132
  },
133
133
  "peerDependencies": {
134
134
  "@sanity/client": "^7",
135
- "@shopify/hydrogen": "~2025.5.0",
135
+ "@shopify/hydrogen": "~2025.5.0 || ~2025.7.0",
136
136
  "react": "^18.2.0",
137
- "react-router": "7.6.0",
137
+ "react-router": "7.9.2",
138
138
  "vite": "^5.1.0 || ^6.2.1"
139
139
  },
140
140
  "engines": {
package/src/Query.tsx CHANGED
@@ -1,15 +1,7 @@
1
1
  import type {Any, ClientReturn, QueryParams, QueryWithoutParams} from '@sanity/client'
2
2
  import type {EncodeDataAttributeFunction} from '@sanity/core-loader/encode-data-attribute'
3
3
  import type {QueryResponseInitial} from '@sanity/react-loader'
4
- import {
5
- lazy,
6
- type ReactNode,
7
- startTransition,
8
- Suspense,
9
- type SuspenseProps,
10
- useEffect,
11
- useState,
12
- } from 'react'
4
+ import {lazy, type ReactNode, Suspense, type SuspenseProps, useSyncExternalStore} from 'react'
13
5
 
14
6
  import type {LoadQueryOptions} from './context'
15
7
  import {usePreviewMode} from './preview/hooks'
@@ -23,6 +15,19 @@ function SanityQueryFallback(): ReactNode {
23
15
  return null
24
16
  }
25
17
 
18
+ /**
19
+ * Simple hydration store to avoid hydration mismatches.
20
+ * Returns false on server, true on client after hydration.
21
+ */
22
+ function useIsHydrated(): boolean {
23
+ return useSyncExternalStore(
24
+ // eslint-disable-next-line no-empty-function
25
+ () => () => {},
26
+ () => true,
27
+ () => false,
28
+ )
29
+ }
30
+
26
31
  const QueryClient = isServer()
27
32
  ? SanityQueryFallback
28
33
  : (lazy(
@@ -66,14 +71,7 @@ export function Query<Result = Any, Query extends string = string>({
66
71
  ...suspenseProps
67
72
  }: QueryProps<Result, Query> & Omit<SuspenseProps, 'children'>): ReactNode {
68
73
  const isPreviewMode = usePreviewMode()
69
- const [isHydrated, setIsHydrated] = useState(false)
70
-
71
- // Mark component as hydrated after initial render to prevent hydration mismatches
72
- useEffect(() => {
73
- startTransition(() => {
74
- setIsHydrated(true)
75
- })
76
- }, [])
74
+ const isHydrated = useIsHydrated()
77
75
 
78
76
  // If in preview mode and hydrated, render the client component
79
77
  if (isPreviewMode && isHydrated) {
package/src/provider.tsx CHANGED
@@ -1,7 +1,5 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
1
  import type {InitializedClientConfig} from '@sanity/client'
3
2
  import type {HTMLProps, PropsWithChildren, ReactNode} from 'react'
4
- import {useEffect} from 'react'
5
3
 
6
4
  /**
7
5
  * Contains essential Sanity client configuration and preview/stega state.
@@ -22,7 +20,7 @@ export interface SanityProviderValue
22
20
  * Throws an error if the provider value is missing or invalid.
23
21
  */
24
22
  export function assertSanityProviderValue(value: unknown): value is SanityProviderValue {
25
- if (!value) {
23
+ if (typeof value === 'undefined') {
26
24
  throw new Error(
27
25
  'Failed to find a Sanity provider value. Did you forget to wrap your app in a `SanityProvider`?',
28
26
  )
@@ -36,13 +34,10 @@ export function assertSanityProviderValue(value: unknown): value is SanityProvid
36
34
  * Must be used within a SanityProvider component tree.
37
35
  */
38
36
  export function useSanityProviderValue(): SanityProviderValue {
39
- const providerValue = (globalThis as any)[
40
- Symbol.for('Sanity Provider')
41
- ] as SanityProviderValue | null
42
-
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ const providerValue = (globalThis as any)[Symbol.for('Sanity Provider')]
43
39
  assertSanityProviderValue(providerValue)
44
-
45
- return providerValue!
40
+ return providerValue
46
41
  }
47
42
 
48
43
  /**
@@ -53,10 +48,7 @@ export function SanityProvider({
53
48
  value,
54
49
  children,
55
50
  }: PropsWithChildren<{value: SanityProviderValue}>): ReactNode {
56
- useEffect(() => {
57
- setProviderValue(value)
58
- }, [value])
59
-
51
+ setProviderValue(value)
60
52
  return <>{children}</>
61
53
  }
62
54
 
@@ -66,7 +58,6 @@ export function SanityProvider({
66
58
  */
67
59
  export function Sanity(props: SanityProps): ReactNode {
68
60
  const providerValue = useSanityProviderValue()
69
-
70
61
  assertSanityProviderValue(providerValue)
71
62
 
72
63
  return (
@@ -98,5 +89,6 @@ export type SanityProps = Omit<
98
89
  >
99
90
 
100
91
  function setProviderValue(value: SanityProviderValue) {
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
93
  ;(globalThis as any)[Symbol.for('Sanity Provider')] = Object.freeze(value)
102
94
  }