hydrogen-sanity 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Sanity.io <hello@sanity.io>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,372 @@
1
+ # hydrogen-sanity
2
+
3
+ > **Warning**
4
+ >
5
+ > Please be advised that `hydrogen-sanity` is still under development and available in pre-release. This package could change before it's officially released, so check back for updates and please provide any feedback you might have here.
6
+
7
+ [Sanity.io](https://www.sanity.io) toolkit for [Hydrogen](https://hydrogen.shopify.dev/)
8
+
9
+ **Features:**
10
+
11
+ - Cacheable queries to Sanity APICDN
12
+ - Client-side live real-time preview using an API token
13
+
14
+ > **Note**
15
+ >
16
+ > Using this package isn't strictly required for working with Sanity in a Hydrogen storefront. If you'd like to use `@sanity/client` directly, see [Using `@sanity/client` directly](#using-sanityclient-directly) below.
17
+
18
+ ## Installation
19
+
20
+ ```sh
21
+ npm install hydrogen-sanity@beta
22
+ ```
23
+
24
+ ```sh
25
+ yarn add hydrogen-sanity@beta
26
+ ```
27
+
28
+ ```sh
29
+ pnpm install hydrogen-sanity@beta
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Update the server file to include the Sanity client:
35
+
36
+ ```ts
37
+ // ./server.ts
38
+
39
+ // ...all other imports
40
+ import {createSanityClient, PreviewSession} from 'hydrogen-sanity';
41
+
42
+ // Inside the default export
43
+ export default () => {
44
+
45
+ // 1. Add check for Preview Session
46
+ const [cache, session, previewSession] = await Promise.all([
47
+ caches.open('hydrogen'),
48
+ HydrogenSession.init(request, [env.SESSION_SECRET]),
49
+ // 👇 Add preview session
50
+ PreviewSession.init(request, [env.SESSION_SECRET]),
51
+ ]);
52
+
53
+ // Leave all other functions like the storefront client as-is
54
+ const {storefront} = createStorefrontClient({ ... })
55
+
56
+ // 2. Add the Sanity client
57
+ const sanity = createSanityClient({
58
+ cache,
59
+ waitUntil,
60
+ // Optionally, pass session and token to enable live-preview
61
+ preview:
62
+ env.SANITY_PREVIEW_SECRET && env.SANITY_API_TOKEN
63
+ ? {
64
+ session: previewSession,
65
+ token: env.SANITY_API_TOKEN,
66
+ }
67
+ : undefined,
68
+ // Pass configuration options for Sanity client
69
+ config: {
70
+ projectId: env.SANITY_PROJECT_ID,
71
+ dataset: env.SANITY_DATASET,
72
+ apiVersion: env.SANITY_API_VERSION ?? '2023-03-30',
73
+ useCdn: process.env.NODE_ENV === 'production',
74
+ },
75
+ });
76
+
77
+ // 3. Add Sanity client to the request handler inside getLoadContext
78
+ const handleRequest = createRequestHandler({
79
+ // ...other settings
80
+ getLoadContext: () => ({
81
+ // ...other providers
82
+ sanity,
83
+ }),
84
+ });
85
+ }
86
+ ```
87
+
88
+ Update your environment variables with settings from your Sanity project. Copy these from https://www.sanity.io/manage or run `npx sanity@latest init --env` to fill the minimum required values from a new or existing project.
89
+
90
+ ```sh
91
+ # Project ID
92
+ SANITY_PROJECT_ID=""
93
+ # Dataset name
94
+ SANITY_DATASET=""
95
+ # (Optional) Sanity API version
96
+ SANITY_API_VERSION=""
97
+ # Sanity token to authenticate requests in "preview" mode,
98
+ # with `viewer` role or higher access
99
+ # https://www.sanity.io/docs/http-auth
100
+ SANITY_API_TOKEN=""
101
+ # Secret for authenticating preview mode
102
+ SANITY_PREVIEW_SECRET=""
103
+ ```
104
+
105
+ ### Satisfy TypeScript
106
+
107
+ Update the environment variables in `Env`
108
+
109
+ ```ts
110
+ // ./remix.env.d.ts
111
+ import type {Sanity} from 'hydrogen-sanity'
112
+
113
+ declare global {
114
+ // ...other Types
115
+
116
+ interface Env {
117
+ // ...other variables
118
+ SANITY_PREVIEW_SECRET: string
119
+ SANITY_API_TOKEN: string
120
+ SANITY_PROJECT_ID: string
121
+ SANITY_DATASET: string
122
+ SANITY_API_VERSION: string
123
+ }
124
+ }
125
+
126
+ declare module '@shopify/remix-oxygen' {
127
+ export interface AppLoadContext {
128
+ // ...other Types
129
+ sanity: Sanity
130
+ }
131
+ }
132
+ ```
133
+
134
+ ### Fetching Sanity data with `query`
135
+
136
+ Query Sanity API and cache the response (defaults to `CacheLong` caching strategy):
137
+
138
+ ```ts
139
+ export async function loader({context, params}: LoaderArgs) {
140
+ const homepage = await context.sanity.query({
141
+ query: `*[_type == "page" && _id == $id][0]`,
142
+ params: {
143
+ id: 'home',
144
+ },
145
+ // optionally pass a caching strategy
146
+ // cache: CacheShort()
147
+ })
148
+
149
+ return json({
150
+ homepage,
151
+ })
152
+ }
153
+ ```
154
+
155
+ To use other client methods, or to use `fetch` without caching, the Sanity client is also available:
156
+
157
+ ```ts
158
+ export async function loader({context, params}: LoaderArgs) {
159
+ const homepage = await context.sanity.client.fetch(
160
+ `*[_type == "page" && _id == $id][0]`,
161
+ {id: 'home'}
162
+ );
163
+
164
+ return json({
165
+ homepage,
166
+ });
167
+ ```
168
+
169
+ ### Live preview
170
+
171
+ Enable real-time, live preview by streaming dataset updates to the browser.
172
+
173
+ First setup your root route to enable preview mode across the entire application, if the preview session is found:
174
+
175
+ ```tsx
176
+ // ./app/root.tsx
177
+
178
+ // ...other imports
179
+ import {Preview, type PreviewData, isPreviewModeEnabled} from 'hydrogen-sanity'
180
+
181
+ export async function loader({context}: LoaderArgs) {
182
+ const preview: PreviewData | undefined = isPreviewModeEnabled(context.sanity.preview)
183
+ ? {
184
+ projectId: context.sanity.preview.projectId,
185
+ dataset: context.sanity.preview.dataset,
186
+ token: context.sanity.preview.token,
187
+ }
188
+ : undefined
189
+
190
+ return json({
191
+ // ... other loader data
192
+ preview,
193
+ })
194
+ }
195
+
196
+ export default function App() {
197
+ const {preview, ...data} = useLoaderData<typeof loader>()
198
+
199
+ return (
200
+ <html>
201
+ <head>
202
+ <meta charSet="utf-8" />
203
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
204
+ <Meta />
205
+ <Links />
206
+ </head>
207
+ <body>
208
+ {/* 👇 Wrap <Outlet /> in Preview component */}
209
+ <Preview preview={preview}>
210
+ <Outlet />
211
+ </Preview>
212
+ <ScrollRestoration />
213
+ <Scripts />
214
+ </body>
215
+ </html>
216
+ )
217
+ }
218
+ ```
219
+
220
+ You can also pass a `ReactNode` to render a loading indicator or adjust the default message:
221
+
222
+ ```tsx
223
+ import {PreviewLoading} from '~/components/PreviewLoading';
224
+
225
+ // pass a string or your own React component to show while data is loading
226
+ <Preview preview={preview} fallback={<PreviewLoading />}>
227
+ ```
228
+
229
+ Next, for any route that needs to render a preview, wrap it in a `Preview` component which re-runs the same query client-side but will render draft content in place of published content, if it exists. Updating in real-time as changes are streamed in.
230
+
231
+ The `usePreview` hook conditionally renders the preview component if the preview session is found, otherwise, it renders the default component.
232
+
233
+ ```tsx
234
+ // Any route file, such as ./app/routes/index.tsx
235
+
236
+ // ...all other imports
237
+ import {usePreviewComponent, usePreviewContext} from 'hydrogen-sanity'
238
+
239
+ // ...all other exports like `loader` and `meta`
240
+ // Tip: In preview mode, pass "query" and "params" from the loader to the component
241
+
242
+ // Default export where content is rendered
243
+ export default function Index() {
244
+ // Get initial data
245
+ const {homepage} = useLoaderData<typeof loader>()
246
+ // Conditionally render preview-enabled component (see below)
247
+ const Component = usePreviewComponent(Route, Preview)
248
+
249
+ return <Component homepage={homepage} />
250
+ }
251
+
252
+ // Renders Sanity content, whether powered by Preview or not
253
+ function Route({homepage}) {
254
+ return <>{/* ...render homepage using data */}</>
255
+ }
256
+
257
+ // Fetches content client-side and renders live updates of draft content
258
+ function Preview(props) {
259
+ const {usePreview} = usePreviewContext()!
260
+ const homepage = usePreview(
261
+ `*[_type == "page" && _id == $id][0]`,
262
+ {id: 'home'},
263
+ // the initial data from the loader, which
264
+ // can help speed up loading
265
+ props.homepage
266
+ )
267
+
268
+ return <Route homepage={homepage} />
269
+ }
270
+ ```
271
+
272
+ ### Entering preview mode
273
+
274
+ For users to enter preview mode, they will need to visit a route that sets a preview cookie. The logic of what routes should set the preview cookie is up to you, in this example, it checks if a parameter in the URL matches one of your environment variables on the server.
275
+
276
+ ```tsx
277
+ // ./app/routes/api.preview.tsx
278
+ import {LoaderFunction, redirect} from '@shopify/remix-oxygen'
279
+
280
+ export const loader: LoaderFunction = async function ({request, context}) {
281
+ const {env, sanity} = context
282
+ const {searchParams} = new URL(request.url)
283
+
284
+ if (
285
+ !sanity.preview?.session ||
286
+ !searchParams.has('secret') ||
287
+ searchParams.get('secret') !== env.SANITY_PREVIEW_SECRET
288
+ ) {
289
+ throw new Response('Invalid secret', {
290
+ status: 401,
291
+ statusText: 'Unauthorized',
292
+ })
293
+ }
294
+
295
+ sanity.preview.session.set('projectId', env.SANITY_PROJECT_ID)
296
+
297
+ return redirect(`/`, {
298
+ status: 307,
299
+ headers: {
300
+ 'Set-Cookie': await sanity.preview.session.commit(),
301
+ },
302
+ })
303
+ }
304
+ ```
305
+
306
+ ## Limits
307
+
308
+ The real-time preview isn't optimized and comes with a configured limit of 3000 documents. You can experiment with larger datasets by configuring the hook with `documentLimit: <Integer>`. Be aware that this might significantly affect the preview performance.
309
+ You may use the `includeTypes` option to reduce the amount of documents and reduce the risk of hitting the `documentLimit`:
310
+
311
+ ## Using `@sanity/client` directly
312
+
313
+ For whatever reason, if you choose not to use `hydrogen-sanity` you can still use `@sanity/client` to get Sanity content into your Hydrogen storefront:
314
+
315
+ ```ts
316
+ // ./server.ts
317
+
318
+ // ...all other imports
319
+ import {createClient} from '@sanity/client';
320
+
321
+ export default {
322
+ // ... all other functions
323
+
324
+ // Create the Sanity Client
325
+ const sanity = createClient({
326
+ projectId: env.SANITY_PROJECT_ID,
327
+ dataset: env.SANITY_DATASET,
328
+ apiVersion: env.SANITY_API_VERSION ?? '2023-03-30',
329
+ useCdn: process.env.NODE_ENV === 'production',
330
+ });
331
+
332
+ // Pass it along to every request by
333
+ // adding it to `handleRequest`
334
+ const handleRequest = createRequestHandler({
335
+ // ...other settings
336
+ getLoadContext: () => ({
337
+ // ...other context items
338
+ sanity
339
+ }),
340
+ });
341
+ }
342
+ ```
343
+
344
+ Then, in your loaders you'll have access to the client in the request context:
345
+
346
+ ```ts
347
+ export async function loader({context, params}: LoaderArgs) {
348
+ const homepage = await context.sanity.fetch(
349
+ `*[_type == "page" && _id == $id][0]`,
350
+ {id: 'home'}
351
+ );
352
+
353
+ return json({
354
+ homepage,
355
+ });
356
+ ```
357
+
358
+ ## License
359
+
360
+ [MIT](LICENSE) © Sanity.io <hello@sanity.io>
361
+
362
+ ## Develop & test
363
+
364
+ This plugin uses [@sanity/pkg-utils](https://github.com/sanity-io/pkg-utils)
365
+ with default configuration for build & watch scripts.
366
+
367
+ ### Release new version
368
+
369
+ Run ["CI & Release" workflow](https://github.com/sanity-io/hydrogen-sanity/actions/workflows/main.yml).
370
+ Make sure to select the main branch and check "Release new version".
371
+
372
+ Semantic release will only release on configured branches, so it is safe to run release on any branch.
@@ -0,0 +1,125 @@
1
+ import type {CacheShort} from '@shopify/hydrogen'
2
+ import {ClientConfig} from '@sanity/client'
3
+ import {ElementType} from 'react'
4
+ import {JSX as JSX_2} from 'react/jsx-runtime'
5
+ import {Params} from '@sanity/preview-kit'
6
+ import {ReactNode} from 'react'
7
+ import {SanityClient} from '@sanity/client'
8
+ import {Session} from '@shopify/remix-oxygen'
9
+ import {SessionStorage} from '@shopify/remix-oxygen'
10
+
11
+ /** @see https://shopify.dev/docs/custom-storefronts/hydrogen/data-fetching/cache#caching-strategies */
12
+ export declare type CachingStrategy = ReturnType<typeof CacheShort>
13
+
14
+ /**
15
+ * Create Sanity provider with API client.
16
+ */
17
+ export declare function createSanityClient(options: CreateSanityClientOptions): Sanity
18
+
19
+ declare type CreateSanityClientOptions = EnvironmentOptions & {
20
+ config: ClientConfig & Required<Pick<ClientConfig, 'projectId' | 'dataset'>>
21
+ preview?: {
22
+ session: PreviewSession
23
+ token: string
24
+ }
25
+ }
26
+
27
+ export declare type EnvironmentOptions = {
28
+ /**
29
+ * A Cache API instance.
30
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Cache
31
+ */
32
+ cache: Cache
33
+ /**
34
+ * A runtime utility for serverless environments
35
+ * @see https://developers.cloudflare.com/workers/runtime-apis/fetch-event/#waituntil
36
+ */
37
+ waitUntil: ExecutionContext['waitUntil']
38
+ }
39
+
40
+ declare interface ExecutionContext {
41
+ waitUntil(promise: Promise<any>): void
42
+ }
43
+
44
+ export declare function isPreviewModeEnabled(preview: Sanity['preview']): preview is {
45
+ session: PreviewSession
46
+ } & PreviewData
47
+
48
+ /**
49
+ * Conditionally apply `PreviewSuspense` boundary
50
+ * @see https://www.sanity.io/docs/preview-content-on-site
51
+ */
52
+ export declare function Preview(props: PreviewProps): JSX_2.Element
53
+
54
+ export declare type PreviewData = {
55
+ projectId: string
56
+ dataset: string
57
+ token: string
58
+ }
59
+
60
+ export declare type PreviewProps = {
61
+ children: ReactNode
62
+ fallback?: ReactNode
63
+ preview?: PreviewData
64
+ }
65
+
66
+ export declare class PreviewSession {
67
+ private sessionStorage
68
+ private session
69
+ constructor(sessionStorage: SessionStorage, session: Session)
70
+ static init(request: Request, secrets: string[]): Promise<PreviewSession>
71
+ has(key: string): boolean
72
+ destroy(): Promise<string>
73
+ set(key: string, value: any): void
74
+ commit(): Promise<string>
75
+ }
76
+
77
+ export declare type Sanity = {
78
+ client: SanityClient
79
+ preview?:
80
+ | ({
81
+ session: PreviewSession
82
+ } & PreviewData)
83
+ | {
84
+ session: PreviewSession
85
+ }
86
+ query<T>(options: useSanityQuery): Promise<T>
87
+ }
88
+
89
+ /**
90
+ * Create an SHA-256 hash as a hex string
91
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
92
+ */
93
+ export declare function sha256(message: string): Promise<string>
94
+
95
+ declare type UsePreview = <R = any, P extends Params = Params, Q extends string = string>(
96
+ query: Q,
97
+ params?: P,
98
+ serverSnapshot?: R
99
+ ) => R
100
+
101
+ /**
102
+ * Select and memoize which component to render based on preview mode
103
+ */
104
+ export declare function usePreviewComponent<T>(
105
+ component: ElementType<T>,
106
+ preview: ElementType<T>
107
+ ): ElementType<T>
108
+
109
+ export declare const usePreviewContext: () =>
110
+ | {
111
+ /**
112
+ * Query Sanity and subscribe to changes, optionally
113
+ * passing a server snapshot to speed up hydration
114
+ */
115
+ usePreview: UsePreview
116
+ }
117
+ | undefined
118
+
119
+ declare type useSanityQuery = {
120
+ query: string
121
+ params?: Record<string, unknown>
122
+ cache?: CachingStrategy
123
+ }
124
+
125
+ export {}
@@ -0,0 +1,152 @@
1
+ import { createClient } from '@sanity/client';
2
+ import { createWithCache_unstable, CacheLong } from '@shopify/hydrogen';
3
+ import { jsx, Fragment } from 'react/jsx-runtime';
4
+ import { definePreview, PreviewSuspense } from '@sanity/preview-kit';
5
+ import { createCookieSessionStorage } from '@shopify/remix-oxygen';
6
+ import { createContext, useContext, useMemo } from 'react';
7
+ function createSanityClient(options) {
8
+ const {
9
+ cache,
10
+ waitUntil,
11
+ preview,
12
+ config
13
+ } = options;
14
+ const withCache = createWithCache_unstable({
15
+ cache,
16
+ waitUntil
17
+ });
18
+ const sanity = {
19
+ client: createClient(config),
20
+ async query(_ref) {
21
+ let {
22
+ query,
23
+ params,
24
+ cache: strategy = CacheLong()
25
+ } = _ref;
26
+ const queryHash = await hashQuery(query, params);
27
+ return withCache(queryHash, strategy, () => sanity.client.fetch(query, params));
28
+ }
29
+ };
30
+ if (preview) {
31
+ sanity.preview = {
32
+ session: preview.session
33
+ };
34
+ if (preview.session.has("projectId")) {
35
+ sanity.preview = {
36
+ ...sanity.preview,
37
+ projectId: config.projectId,
38
+ dataset: config.dataset,
39
+ token: preview.token
40
+ };
41
+ sanity.client = sanity.client.withConfig({
42
+ useCdn: false,
43
+ token: preview.token
44
+ });
45
+ sanity.query = _ref2 => {
46
+ let {
47
+ query,
48
+ params
49
+ } = _ref2;
50
+ return sanity.client.fetch(query, params);
51
+ };
52
+ }
53
+ }
54
+ return sanity;
55
+ }
56
+ function isPreviewModeEnabled(preview) {
57
+ return (preview == null ? void 0 : preview.token) !== null;
58
+ }
59
+ async function sha256(message) {
60
+ const messageBuffer = await new TextEncoder().encode(message);
61
+ const hashBuffer = await crypto.subtle.digest("SHA-256", messageBuffer);
62
+ return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
63
+ }
64
+ function hashQuery(query, params) {
65
+ let hash = query;
66
+ if (params !== null) {
67
+ hash += JSON.stringify(params);
68
+ }
69
+ return sha256(hash);
70
+ }
71
+ class PreviewSession {
72
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
73
+ constructor(sessionStorage, session) {
74
+ this.sessionStorage = sessionStorage;
75
+ this.session = session;
76
+ }
77
+ static async init(request, secrets) {
78
+ const storage = createCookieSessionStorage({
79
+ cookie: {
80
+ name: "__preview",
81
+ httpOnly: true,
82
+ sameSite: true,
83
+ secrets
84
+ }
85
+ });
86
+ const session = await storage.getSession(request.headers.get("Cookie"));
87
+ return new this(storage, session);
88
+ }
89
+ has(key) {
90
+ return this.session.has(key);
91
+ }
92
+ // get(key: string) {
93
+ // return this.session.get(key);
94
+ // }
95
+ destroy() {
96
+ return this.sessionStorage.destroySession(this.session);
97
+ }
98
+ // unset(key: string) {
99
+ // this.session.unset(key);
100
+ // }
101
+ set(key, value) {
102
+ this.session.set(key, value);
103
+ }
104
+ commit() {
105
+ return this.sessionStorage.commitSession(this.session);
106
+ }
107
+ }
108
+ const PreviewContext = createContext(void 0);
109
+ const usePreviewContext = () => useContext(PreviewContext);
110
+ function Preview(props) {
111
+ var _a;
112
+ const {
113
+ children,
114
+ preview
115
+ } = props;
116
+ if (!(preview == null ? void 0 : preview.token)) {
117
+ return /* @__PURE__ */jsx(Fragment, {
118
+ children
119
+ });
120
+ }
121
+ const fallback = (_a = props.fallback) != null ? _a : /* @__PURE__ */jsx("div", {
122
+ children: "Loading preview..."
123
+ });
124
+ const {
125
+ projectId,
126
+ dataset,
127
+ token
128
+ } = preview;
129
+ const _usePreview = definePreview({
130
+ projectId,
131
+ dataset,
132
+ overlayDrafts: true
133
+ });
134
+ function usePreview(query, params, serverSnapshot) {
135
+ return _usePreview(token, query, params, serverSnapshot);
136
+ }
137
+ return /* @__PURE__ */jsx(PreviewContext.Provider, {
138
+ value: {
139
+ usePreview
140
+ },
141
+ children: /* @__PURE__ */jsx(PreviewSuspense, {
142
+ fallback,
143
+ children
144
+ })
145
+ });
146
+ }
147
+ function usePreviewComponent(component, preview) {
148
+ const isPreview = Boolean(usePreviewContext());
149
+ return useMemo(() => isPreview ? preview : component, [component, isPreview, preview]);
150
+ }
151
+ export { Preview, PreviewSession, createSanityClient, isPreviewModeEnabled, sha256, usePreviewComponent, usePreviewContext };
152
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":["../src/client.ts","../src/preview.tsx"],"sourcesContent":["import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'\n// eslint-disable-next-line camelcase\nimport {CacheLong, createWithCache_unstable} from '@shopify/hydrogen'\n\nimport type {PreviewData, PreviewSession} from './preview'\nimport type {CachingStrategy, EnvironmentOptions} from './types'\n\ntype CreateSanityClientOptions = EnvironmentOptions & {\n config: ClientConfig & Required<Pick<ClientConfig, 'projectId' | 'dataset'>>\n preview?: {\n session: PreviewSession\n token: string\n }\n}\n\ntype useSanityQuery = {\n query: string\n params?: Record<string, unknown>\n cache?: CachingStrategy\n}\n\nexport type Sanity = {\n client: SanityClient\n preview?: ({session: PreviewSession} & PreviewData) | {session: PreviewSession}\n query<T>(options: useSanityQuery): Promise<T>\n}\n\n/**\n * Create Sanity provider with API client.\n */\nexport function createSanityClient(options: CreateSanityClientOptions): Sanity {\n const {cache, waitUntil, preview, config} = options\n const withCache = createWithCache_unstable({\n cache,\n waitUntil,\n })\n\n const sanity: Sanity = {\n client: createClient(config),\n async query({query, params, cache: strategy = CacheLong()}) {\n const queryHash = await hashQuery(query, params)\n\n return withCache(queryHash, strategy, () => sanity.client.fetch(query, params))\n },\n }\n\n if (preview) {\n sanity.preview = {session: preview.session}\n\n if (preview.session.has('projectId')) {\n sanity.preview = {\n ...sanity.preview,\n projectId: config.projectId,\n dataset: config.dataset,\n token: preview.token,\n }\n\n sanity.client = sanity.client.withConfig({\n useCdn: false,\n token: preview.token,\n })\n\n sanity.query = ({query, params}) => {\n return sanity.client.fetch(query, params)\n }\n }\n }\n\n return sanity\n}\n\nexport function isPreviewModeEnabled(\n preview: Sanity['preview']\n): preview is {session: PreviewSession} & PreviewData {\n // @ts-expect-error\n return preview?.token !== null\n}\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 */\nfunction hashQuery(\n query: useSanityQuery['query'],\n params: useSanityQuery['params']\n): Promise<string> {\n let hash = query\n\n if (params !== null) {\n hash += JSON.stringify(params)\n }\n\n return sha256(hash)\n}\n","/* eslint-disable react/require-default-props */\nimport {definePreview, type Params, PreviewSuspense} from '@sanity/preview-kit'\nimport {createCookieSessionStorage, type Session, type SessionStorage} from '@shopify/remix-oxygen'\nimport {createContext, ElementType, type ReactNode, useContext, useMemo} from 'react'\n\ntype UsePreview = <R = any, P extends Params = Params, Q extends string = string>(\n query: Q,\n params?: P,\n serverSnapshot?: R\n) => R\n\nexport type PreviewData = {\n projectId: string\n dataset: string\n token: string\n}\n\nexport type PreviewProps = {\n children: ReactNode\n fallback?: ReactNode\n preview?: PreviewData\n}\n\nexport class PreviewSession {\n // eslint-disable-next-line no-useless-constructor, no-empty-function\n constructor(private sessionStorage: SessionStorage, private session: Session) {}\n\n static async init(request: Request, secrets: string[]) {\n const storage = createCookieSessionStorage({\n cookie: {\n name: '__preview',\n httpOnly: true,\n sameSite: true,\n secrets,\n },\n })\n\n const session = await storage.getSession(request.headers.get('Cookie'))\n\n return new this(storage, session)\n }\n\n has(key: string) {\n return this.session.has(key)\n }\n\n // get(key: string) {\n // return this.session.get(key);\n // }\n\n destroy() {\n return this.sessionStorage.destroySession(this.session)\n }\n\n // unset(key: string) {\n // this.session.unset(key);\n // }\n\n set(key: string, value: any) {\n this.session.set(key, value)\n }\n\n commit() {\n return this.sessionStorage.commitSession(this.session)\n }\n}\n\nconst PreviewContext = createContext<\n | {\n /**\n * Query Sanity and subscribe to changes, optionally\n * passing a server snapshot to speed up hydration\n */\n usePreview: UsePreview\n }\n | undefined\n>(undefined)\n\nexport const usePreviewContext = () => useContext(PreviewContext)\n\n/**\n * Conditionally apply `PreviewSuspense` boundary\n * @see https://www.sanity.io/docs/preview-content-on-site\n */\nexport function Preview(props: PreviewProps) {\n const {children, preview} = props\n\n if (!preview?.token) {\n return <>{children}</>\n }\n\n const fallback = props.fallback ?? <div>Loading preview...</div>\n const {projectId, dataset, token} = preview\n const _usePreview = definePreview({\n projectId,\n dataset,\n overlayDrafts: true,\n })\n\n function usePreview<R = any, P extends Params = Params, Q extends string = string>(\n query: Q,\n params?: P,\n serverSnapshot?: R\n ): R {\n return _usePreview(token, query, params, serverSnapshot)\n }\n usePreview satisfies UsePreview\n\n return (\n <PreviewContext.Provider value={{usePreview}}>\n <PreviewSuspense fallback={fallback}>{children}</PreviewSuspense>\n </PreviewContext.Provider>\n )\n}\n\n/**\n * Select and memoize which component to render based on preview mode\n */\nexport function usePreviewComponent<T>(component: ElementType<T>, preview: ElementType<T>) {\n const isPreview = Boolean(usePreviewContext())\n\n return useMemo(() => (isPreview ? preview : component), [component, isPreview, preview])\n}\n"],"names":["createSanityClient","options","cache","waitUntil","preview","config","withCache","createWithCache_unstable","sanity","client","createClient","query","params","strategy","CacheLong","_ref","queryHash","hashQuery","fetch","session","has","projectId","dataset","token","withConfig","useCdn","_ref2","isPreviewModeEnabled","sha256","message","messageBuffer","TextEncoder","encode","hashBuffer","crypto","subtle","digest","Array","from","Uint8Array","map","b","toString","padStart","join","hash","JSON","stringify","PreviewSession","constructor","sessionStorage","init","request","secrets","storage","createCookieSessionStorage","cookie","name","httpOnly","sameSite","getSession","headers","get","key","destroy","destroySession","set","value","commit","commitSession","PreviewContext","createContext","usePreviewContext","useContext","Preview","props","_a","children","fallback","jsx","_usePreview","definePreview","overlayDrafts","usePreview","serverSnapshot","Provider","PreviewSuspense","usePreviewComponent","component","isPreview","Boolean","useMemo"],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', {
4
+ value: true
5
+ });
6
+ var client = require('@sanity/client');
7
+ var hydrogen = require('@shopify/hydrogen');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+ var previewKit = require('@sanity/preview-kit');
10
+ var remixOxygen = require('@shopify/remix-oxygen');
11
+ var react = require('react');
12
+ function createSanityClient(options) {
13
+ const {
14
+ cache,
15
+ waitUntil,
16
+ preview,
17
+ config
18
+ } = options;
19
+ const withCache = hydrogen.createWithCache_unstable({
20
+ cache,
21
+ waitUntil
22
+ });
23
+ const sanity = {
24
+ client: client.createClient(config),
25
+ async query(_ref) {
26
+ let {
27
+ query,
28
+ params,
29
+ cache: strategy = hydrogen.CacheLong()
30
+ } = _ref;
31
+ const queryHash = await hashQuery(query, params);
32
+ return withCache(queryHash, strategy, () => sanity.client.fetch(query, params));
33
+ }
34
+ };
35
+ if (preview) {
36
+ sanity.preview = {
37
+ session: preview.session
38
+ };
39
+ if (preview.session.has("projectId")) {
40
+ sanity.preview = {
41
+ ...sanity.preview,
42
+ projectId: config.projectId,
43
+ dataset: config.dataset,
44
+ token: preview.token
45
+ };
46
+ sanity.client = sanity.client.withConfig({
47
+ useCdn: false,
48
+ token: preview.token
49
+ });
50
+ sanity.query = _ref2 => {
51
+ let {
52
+ query,
53
+ params
54
+ } = _ref2;
55
+ return sanity.client.fetch(query, params);
56
+ };
57
+ }
58
+ }
59
+ return sanity;
60
+ }
61
+ function isPreviewModeEnabled(preview) {
62
+ return (preview == null ? void 0 : preview.token) !== null;
63
+ }
64
+ async function sha256(message) {
65
+ const messageBuffer = await new TextEncoder().encode(message);
66
+ const hashBuffer = await crypto.subtle.digest("SHA-256", messageBuffer);
67
+ return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
68
+ }
69
+ function hashQuery(query, params) {
70
+ let hash = query;
71
+ if (params !== null) {
72
+ hash += JSON.stringify(params);
73
+ }
74
+ return sha256(hash);
75
+ }
76
+ class PreviewSession {
77
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
78
+ constructor(sessionStorage, session) {
79
+ this.sessionStorage = sessionStorage;
80
+ this.session = session;
81
+ }
82
+ static async init(request, secrets) {
83
+ const storage = remixOxygen.createCookieSessionStorage({
84
+ cookie: {
85
+ name: "__preview",
86
+ httpOnly: true,
87
+ sameSite: true,
88
+ secrets
89
+ }
90
+ });
91
+ const session = await storage.getSession(request.headers.get("Cookie"));
92
+ return new this(storage, session);
93
+ }
94
+ has(key) {
95
+ return this.session.has(key);
96
+ }
97
+ // get(key: string) {
98
+ // return this.session.get(key);
99
+ // }
100
+ destroy() {
101
+ return this.sessionStorage.destroySession(this.session);
102
+ }
103
+ // unset(key: string) {
104
+ // this.session.unset(key);
105
+ // }
106
+ set(key, value) {
107
+ this.session.set(key, value);
108
+ }
109
+ commit() {
110
+ return this.sessionStorage.commitSession(this.session);
111
+ }
112
+ }
113
+ const PreviewContext = react.createContext(void 0);
114
+ const usePreviewContext = () => react.useContext(PreviewContext);
115
+ function Preview(props) {
116
+ var _a;
117
+ const {
118
+ children,
119
+ preview
120
+ } = props;
121
+ if (!(preview == null ? void 0 : preview.token)) {
122
+ return /* @__PURE__ */jsxRuntime.jsx(jsxRuntime.Fragment, {
123
+ children
124
+ });
125
+ }
126
+ const fallback = (_a = props.fallback) != null ? _a : /* @__PURE__ */jsxRuntime.jsx("div", {
127
+ children: "Loading preview..."
128
+ });
129
+ const {
130
+ projectId,
131
+ dataset,
132
+ token
133
+ } = preview;
134
+ const _usePreview = previewKit.definePreview({
135
+ projectId,
136
+ dataset,
137
+ overlayDrafts: true
138
+ });
139
+ function usePreview(query, params, serverSnapshot) {
140
+ return _usePreview(token, query, params, serverSnapshot);
141
+ }
142
+ return /* @__PURE__ */jsxRuntime.jsx(PreviewContext.Provider, {
143
+ value: {
144
+ usePreview
145
+ },
146
+ children: /* @__PURE__ */jsxRuntime.jsx(previewKit.PreviewSuspense, {
147
+ fallback,
148
+ children
149
+ })
150
+ });
151
+ }
152
+ function usePreviewComponent(component, preview) {
153
+ const isPreview = Boolean(usePreviewContext());
154
+ return react.useMemo(() => isPreview ? preview : component, [component, isPreview, preview]);
155
+ }
156
+ exports.Preview = Preview;
157
+ exports.PreviewSession = PreviewSession;
158
+ exports.createSanityClient = createSanityClient;
159
+ exports.isPreviewModeEnabled = isPreviewModeEnabled;
160
+ exports.sha256 = sha256;
161
+ exports.usePreviewComponent = usePreviewComponent;
162
+ exports.usePreviewContext = usePreviewContext;
163
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/client.ts","../src/preview.tsx"],"sourcesContent":["import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'\n// eslint-disable-next-line camelcase\nimport {CacheLong, createWithCache_unstable} from '@shopify/hydrogen'\n\nimport type {PreviewData, PreviewSession} from './preview'\nimport type {CachingStrategy, EnvironmentOptions} from './types'\n\ntype CreateSanityClientOptions = EnvironmentOptions & {\n config: ClientConfig & Required<Pick<ClientConfig, 'projectId' | 'dataset'>>\n preview?: {\n session: PreviewSession\n token: string\n }\n}\n\ntype useSanityQuery = {\n query: string\n params?: Record<string, unknown>\n cache?: CachingStrategy\n}\n\nexport type Sanity = {\n client: SanityClient\n preview?: ({session: PreviewSession} & PreviewData) | {session: PreviewSession}\n query<T>(options: useSanityQuery): Promise<T>\n}\n\n/**\n * Create Sanity provider with API client.\n */\nexport function createSanityClient(options: CreateSanityClientOptions): Sanity {\n const {cache, waitUntil, preview, config} = options\n const withCache = createWithCache_unstable({\n cache,\n waitUntil,\n })\n\n const sanity: Sanity = {\n client: createClient(config),\n async query({query, params, cache: strategy = CacheLong()}) {\n const queryHash = await hashQuery(query, params)\n\n return withCache(queryHash, strategy, () => sanity.client.fetch(query, params))\n },\n }\n\n if (preview) {\n sanity.preview = {session: preview.session}\n\n if (preview.session.has('projectId')) {\n sanity.preview = {\n ...sanity.preview,\n projectId: config.projectId,\n dataset: config.dataset,\n token: preview.token,\n }\n\n sanity.client = sanity.client.withConfig({\n useCdn: false,\n token: preview.token,\n })\n\n sanity.query = ({query, params}) => {\n return sanity.client.fetch(query, params)\n }\n }\n }\n\n return sanity\n}\n\nexport function isPreviewModeEnabled(\n preview: Sanity['preview']\n): preview is {session: PreviewSession} & PreviewData {\n // @ts-expect-error\n return preview?.token !== null\n}\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 */\nfunction hashQuery(\n query: useSanityQuery['query'],\n params: useSanityQuery['params']\n): Promise<string> {\n let hash = query\n\n if (params !== null) {\n hash += JSON.stringify(params)\n }\n\n return sha256(hash)\n}\n","/* eslint-disable react/require-default-props */\nimport {definePreview, type Params, PreviewSuspense} from '@sanity/preview-kit'\nimport {createCookieSessionStorage, type Session, type SessionStorage} from '@shopify/remix-oxygen'\nimport {createContext, ElementType, type ReactNode, useContext, useMemo} from 'react'\n\ntype UsePreview = <R = any, P extends Params = Params, Q extends string = string>(\n query: Q,\n params?: P,\n serverSnapshot?: R\n) => R\n\nexport type PreviewData = {\n projectId: string\n dataset: string\n token: string\n}\n\nexport type PreviewProps = {\n children: ReactNode\n fallback?: ReactNode\n preview?: PreviewData\n}\n\nexport class PreviewSession {\n // eslint-disable-next-line no-useless-constructor, no-empty-function\n constructor(private sessionStorage: SessionStorage, private session: Session) {}\n\n static async init(request: Request, secrets: string[]) {\n const storage = createCookieSessionStorage({\n cookie: {\n name: '__preview',\n httpOnly: true,\n sameSite: true,\n secrets,\n },\n })\n\n const session = await storage.getSession(request.headers.get('Cookie'))\n\n return new this(storage, session)\n }\n\n has(key: string) {\n return this.session.has(key)\n }\n\n // get(key: string) {\n // return this.session.get(key);\n // }\n\n destroy() {\n return this.sessionStorage.destroySession(this.session)\n }\n\n // unset(key: string) {\n // this.session.unset(key);\n // }\n\n set(key: string, value: any) {\n this.session.set(key, value)\n }\n\n commit() {\n return this.sessionStorage.commitSession(this.session)\n }\n}\n\nconst PreviewContext = createContext<\n | {\n /**\n * Query Sanity and subscribe to changes, optionally\n * passing a server snapshot to speed up hydration\n */\n usePreview: UsePreview\n }\n | undefined\n>(undefined)\n\nexport const usePreviewContext = () => useContext(PreviewContext)\n\n/**\n * Conditionally apply `PreviewSuspense` boundary\n * @see https://www.sanity.io/docs/preview-content-on-site\n */\nexport function Preview(props: PreviewProps) {\n const {children, preview} = props\n\n if (!preview?.token) {\n return <>{children}</>\n }\n\n const fallback = props.fallback ?? <div>Loading preview...</div>\n const {projectId, dataset, token} = preview\n const _usePreview = definePreview({\n projectId,\n dataset,\n overlayDrafts: true,\n })\n\n function usePreview<R = any, P extends Params = Params, Q extends string = string>(\n query: Q,\n params?: P,\n serverSnapshot?: R\n ): R {\n return _usePreview(token, query, params, serverSnapshot)\n }\n usePreview satisfies UsePreview\n\n return (\n <PreviewContext.Provider value={{usePreview}}>\n <PreviewSuspense fallback={fallback}>{children}</PreviewSuspense>\n </PreviewContext.Provider>\n )\n}\n\n/**\n * Select and memoize which component to render based on preview mode\n */\nexport function usePreviewComponent<T>(component: ElementType<T>, preview: ElementType<T>) {\n const isPreview = Boolean(usePreviewContext())\n\n return useMemo(() => (isPreview ? preview : component), [component, isPreview, preview])\n}\n"],"names":["createSanityClient","options","cache","waitUntil","preview","config","withCache","createWithCache_unstable","sanity","client","createClient","query","params","strategy","CacheLong","_ref","queryHash","hashQuery","fetch","session","has","projectId","dataset","token","withConfig","useCdn","_ref2","isPreviewModeEnabled","sha256","message","messageBuffer","TextEncoder","encode","hashBuffer","crypto","subtle","digest","Array","from","Uint8Array","map","b","toString","padStart","join","hash","JSON","stringify","PreviewSession","constructor","sessionStorage","init","request","secrets","storage","createCookieSessionStorage","cookie","name","httpOnly","sameSite","getSession","headers","get","key","destroy","destroySession","set","value","commit","commitSession","PreviewContext","createContext","usePreviewContext","useContext","Preview","props","_a","children","fallback","jsx","_usePreview","definePreview","overlayDrafts","usePreview","serverSnapshot","Provider","PreviewSuspense","usePreviewComponent","component","isPreview","Boolean","useMemo"],"mappings":";;;;;;;;;;;AA8BO,SAASA,mBAAmBC,OAA4C,EAAA;EAC7E,MAAM;IAACC,KAAA;IAAOC,SAAW;IAAAC,OAAA;IAASC;GAAU,GAAAJ,OAAA;EAC5C,MAAMK,YAAYC,QAAAA,CAAAA,wBAAyB,CAAA;IACzCL,KAAA;IACAC;EAAA,CACD,CAAA;EAED,MAAMK,MAAiB,GAAA;IACrBC,MAAA,EAAQC,oBAAaL,MAAM,CAAA;IAC3B,MAAMM,YAAsD;MAAA,IAAhD;QAACA,KAAA;QAAOC;QAAQV,KAAO,EAAAW,QAAA,GAAWC,QAAU,CAAAA,SAAA,CAAA;OAAI,GAAAC,IAAA;MAC1D,MAAMC,SAAY,GAAA,MAAMC,SAAU,CAAAN,KAAA,EAAOC,MAAM,CAAA;MAExC,OAAAN,SAAA,CAAUU,WAAWH,QAAU,EAAA,MAAML,OAAOC,MAAO,CAAAS,KAAA,CAAMP,KAAO,EAAAC,MAAM,CAAC,CAAA;IAChF;EAAA,CACF;EAEA,IAAIR,OAAS,EAAA;IACXI,MAAA,CAAOJ,OAAU,GAAA;MAACe,OAAS,EAAAf,OAAA,CAAQe;IAAO,CAAA;IAE1C,IAAIf,OAAQ,CAAAe,OAAA,CAAQC,GAAI,CAAA,WAAW,CAAG,EAAA;MACpCZ,MAAA,CAAOJ,OAAU,GAAA;QACf,GAAGI,MAAO,CAAAJ,OAAA;QACViB,WAAWhB,MAAO,CAAAgB,SAAA;QAClBC,SAASjB,MAAO,CAAAiB,OAAA;QAChBC,OAAOnB,OAAQ,CAAAmB;MAAA,CACjB;MAEOf,MAAA,CAAAC,MAAA,GAASD,MAAO,CAAAC,MAAA,CAAOe,UAAW,CAAA;QACvCC,MAAQ,EAAA,KAAA;QACRF,OAAOnB,OAAQ,CAAAmB;MAAA,CAChB,CAAA;MAEDf,MAAA,CAAOG,KAAQ,GAAAe,KAAA,IAAqB;QAAA,IAApB;UAACf,KAAA;UAAOC;SAAY,GAAAc,KAAA;QAClC,OAAOlB,MAAO,CAAAC,MAAA,CAAOS,KAAM,CAAAP,KAAA,EAAOC,MAAM,CAAA;MAAA,CAC1C;IACF;EACF;EAEO,OAAAJ,MAAA;AACT;AAEO,SAASmB,qBACdvB,OACoD,EAAA;EAEpD,OAAA,CAAOA,mCAASmB,KAAU,MAAA,IAAA;AAC5B;AAMA,eAAsBK,OAAOC,OAAkC,EAAA;EAE7D,MAAMC,gBAAgB,MAAM,IAAIC,WAAY,CAAA,CAAA,CAAEC,OAAOH,OAAO,CAAA;EAE5D,MAAMI,aAAa,MAAMC,MAAA,CAAOC,MAAO,CAAAC,MAAA,CAAO,WAAWN,aAAa,CAAA;EAE/D,OAAAO,KAAA,CAAMC,KAAK,IAAIC,UAAA,CAAWN,UAAU,CAAC,CAAA,CACzCO,IAAKC,CAAA,IAAMA,EAAEC,QAAS,CAAA,EAAE,EAAEC,QAAS,CAAA,CAAA,EAAG,GAAG,CAAC,CAAA,CAC1CC,KAAK,EAAE,CAAA;AACZ;AAMA,SAAS3B,SAAAA,CACPN,OACAC,MACiB,EAAA;EACjB,IAAIiC,IAAO,GAAAlC,KAAA;EAEX,IAAIC,WAAW,IAAM,EAAA;IACXiC,IAAA,IAAAC,IAAA,CAAKC,UAAUnC,MAAM,CAAA;EAC/B;EAEA,OAAOgB,OAAOiB,IAAI,CAAA;AACpB;ACrFO,MAAMG,cAAe,CAAA;EAAA;EAE1BC,WAAAA,CAAoBC,gBAAwC/B,OAAkB,EAAA;IAA1D,IAAA,CAAA+B,cAAA,GAAAA,cAAA;IAAwC,IAAA,CAAA/B,OAAA,GAAAA,OAAA;EAAmB;EAE/E,aAAagC,IAAKA,CAAAC,OAAA,EAAkBC,OAAmB,EAAA;IACrD,MAAMC,UAAUC,WAAAA,CAAAA,0BAA2B,CAAA;MACzCC,MAAQ,EAAA;QACNC,IAAM,EAAA,WAAA;QACNC,QAAU,EAAA,IAAA;QACVC,QAAU,EAAA,IAAA;QACVN;MACF;IAAA,CACD,CAAA;IAEK,MAAAlC,OAAA,GAAU,MAAMmC,OAAQ,CAAAM,UAAA,CAAWR,QAAQS,OAAQ,CAAAC,GAAA,CAAI,QAAQ,CAAC,CAAA;IAE/D,OAAA,IAAI,IAAK,CAAAR,OAAA,EAASnC,OAAO,CAAA;EAClC;EAEAC,IAAI2C,GAAa,EAAA;IACR,OAAA,IAAA,CAAK5C,OAAQ,CAAAC,GAAA,CAAI2C,GAAG,CAAA;EAC7B;EAAA;EAAA;EAAA;EAMAC,OAAUA,CAAA,EAAA;IACR,OAAO,IAAK,CAAAd,cAAA,CAAee,cAAe,CAAA,IAAA,CAAK9C,OAAO,CAAA;EACxD;EAAA;EAAA;EAAA;EAMA+C,GAAAA,CAAIH,KAAaI,KAAY,EAAA;IACtB,IAAA,CAAAhD,OAAA,CAAQ+C,GAAI,CAAAH,GAAA,EAAKI,KAAK,CAAA;EAC7B;EAEAC,MAASA,CAAA,EAAA;IACP,OAAO,IAAK,CAAAlB,cAAA,CAAemB,aAAc,CAAA,IAAA,CAAKlD,OAAO,CAAA;EACvD;AACF;AAEA,MAAMmD,cAAA,GAAiBC,KAAAA,CAAAA,cASrB,KAAS,CAAA,CAAA;AAEE,MAAAC,iBAAA,GAAoBA,CAAA,KAAMC,KAAA,CAAAA,UAAA,CAAWH,cAAc,CAAA;AAMzD,SAASI,QAAQC,KAAqB,EAAA;EApF7C,IAAAC,EAAA;EAqFQ,MAAA;IAACC,QAAU;IAAAzE;EAAW,CAAA,GAAAuE,KAAA;EAExB,IAAA,EAACvE,mCAASmB,KAAO,CAAA,EAAA;IACnB,OAAA;MAAUsD;IAAS,CAAA,CAAA;EACrB;EAEA,MAAMC,YAAWF,EAAM,GAAAD,KAAA,CAAAG,QAAA,KAAN,IAAkB,GAAAF,EAAA,GAAAG,eAAAA,UAAAA,CAAAA,GAAA,CAAC;IAAIF,QAAkB,EAAA;EAAA,CAAA,CAAA;EAC1D,MAAM;IAACxD,SAAA;IAAWC,OAAS;IAAAC;EAAA,CAAS,GAAAnB,OAAA;EACpC,MAAM4E,cAAcC,UAAAA,CAAAA,aAAc,CAAA;IAChC5D,SAAA;IACAC,OAAA;IACA4D,aAAe,EAAA;EAAA,CAChB,CAAA;EAEQ,SAAAC,UAAAA,CACPxE,KACA,EAAAC,MAAA,EACAwE,cACG,EAAA;IACH,OAAOJ,WAAY,CAAAzD,KAAA,EAAOZ,KAAO,EAAAC,MAAA,EAAQwE,cAAc,CAAA;EACzD;EAGA,OACGL,eAAAA,UAAAA,CAAAA,GAAA,CAAAT,cAAA,CAAee,QAAf,EAAA;IAAwBlB,KAAO,EAAA;MAACgB;IAAU,CAAA;IACzCN,QAAC,EAAA,eAAAE,UAAA,CAAAA,GAAA,CAAAO,UAAA,CAAAA,eAAA,EAAA;MAAgBR,QAAqB;MAAAD;IAAS,CAAA;EACjD,CAAA,CAAA;AAEJ;AAKgB,SAAAU,mBAAAA,CAAuBC,WAA2BpF,OAAyB,EAAA;EACnF,MAAAqF,SAAA,GAAYC,OAAQ,CAAAlB,iBAAA,CAAA,CAAmB,CAAA;EAEtC,OAAAmB,KAAA,CAAAA,OAAA,CAAQ,MAAOF,SAAY,GAAArF,OAAA,GAAUoF,WAAY,CAACA,SAAA,EAAWC,SAAW,EAAArF,OAAO,CAAC,CAAA;AACzF;;;;;;;"}
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "hydrogen-sanity",
3
+ "version": "1.0.0",
4
+ "description": "Sanity.io toolkit for Hydrogen",
5
+ "keywords": [
6
+ "sanity",
7
+ "sanity.io",
8
+ "shopify",
9
+ "hydrogen",
10
+ "remix",
11
+ "live",
12
+ "preview"
13
+ ],
14
+ "homepage": "https://github.com/sanity-io/hydrogen-sanity#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/sanity-io/hydrogen-sanity/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+ssh://git@github.com/sanity-io/hydrogen-sanity.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Sanity.io <hello@sanity.io> <noah@sanity.io>",
24
+ "sideEffects": false,
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "source": "./src/index.ts",
29
+ "require": "./dist/index.js",
30
+ "import": "./dist/index.esm.js",
31
+ "default": "./dist/index.esm.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "main": "./dist/index.js",
36
+ "module": "./dist/index.esm.js",
37
+ "source": "./src/index.ts",
38
+ "types": "./dist/index.d.ts",
39
+ "files": [
40
+ "dist",
41
+ "src"
42
+ ],
43
+ "scripts": {
44
+ "build": "run-s clean && pkg-utils build --strict && pkg-utils --strict",
45
+ "clean": "rimraf dist",
46
+ "format": "prettier --write --cache --ignore-unknown .",
47
+ "link-watch": "plugin-kit link-watch",
48
+ "lint": "eslint .",
49
+ "prepublishOnly": "run-s build",
50
+ "watch": "pkg-utils watch --strict",
51
+ "prepare": "husky install"
52
+ },
53
+ "dependencies": {
54
+ "@sanity/client": "^6.0.1",
55
+ "@sanity/preview-kit": "^1.5.2"
56
+ },
57
+ "devDependencies": {
58
+ "@commitlint/cli": "^17.6.3",
59
+ "@commitlint/config-conventional": "^17.6.3",
60
+ "@sanity/pkg-utils": "^2.2.14",
61
+ "@sanity/semantic-release-preset": "^4.1.1",
62
+ "@shopify/hydrogen": "^2023.4.0",
63
+ "@shopify/remix-oxygen": "^1.0.5",
64
+ "@types/react": "^18.2.6",
65
+ "@typescript-eslint/eslint-plugin": "^5.59.5",
66
+ "@typescript-eslint/parser": "^5.59.5",
67
+ "eslint": "^8.40.0",
68
+ "eslint-config-prettier": "^8.8.0",
69
+ "eslint-config-sanity": "^6.0.0",
70
+ "eslint-plugin-prettier": "^4.2.1",
71
+ "eslint-plugin-react": "^7.32.2",
72
+ "eslint-plugin-react-hooks": "^4.6.0",
73
+ "eslint-plugin-simple-import-sort": "^10.0.0",
74
+ "husky": "^8.0.3",
75
+ "lint-staged": "^13.2.2",
76
+ "npm-run-all": "^4.1.5",
77
+ "prettier": "^2.8.8",
78
+ "prettier-plugin-packagejson": "^2.4.3",
79
+ "react": "^18.2.0",
80
+ "react-dom": "^18.2.0",
81
+ "rimraf": "^5.0.0",
82
+ "typescript": "^5.0.4"
83
+ },
84
+ "peerDependencies": {
85
+ "@shopify/hydrogen": "^2023.4.0",
86
+ "@shopify/remix-oxygen": "^1.0.0",
87
+ "react": "^18.0.0",
88
+ "react-dom": "^18.0.0"
89
+ },
90
+ "engines": {
91
+ "node": ">=16"
92
+ }
93
+ }
package/src/client.ts ADDED
@@ -0,0 +1,109 @@
1
+ import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'
2
+ // eslint-disable-next-line camelcase
3
+ import {CacheLong, createWithCache_unstable} from '@shopify/hydrogen'
4
+
5
+ import type {PreviewData, PreviewSession} from './preview'
6
+ import type {CachingStrategy, EnvironmentOptions} from './types'
7
+
8
+ type CreateSanityClientOptions = EnvironmentOptions & {
9
+ config: ClientConfig & Required<Pick<ClientConfig, 'projectId' | 'dataset'>>
10
+ preview?: {
11
+ session: PreviewSession
12
+ token: string
13
+ }
14
+ }
15
+
16
+ type useSanityQuery = {
17
+ query: string
18
+ params?: Record<string, unknown>
19
+ cache?: CachingStrategy
20
+ }
21
+
22
+ export type Sanity = {
23
+ client: SanityClient
24
+ preview?: ({session: PreviewSession} & PreviewData) | {session: PreviewSession}
25
+ query<T>(options: useSanityQuery): Promise<T>
26
+ }
27
+
28
+ /**
29
+ * Create Sanity provider with API client.
30
+ */
31
+ export function createSanityClient(options: CreateSanityClientOptions): Sanity {
32
+ const {cache, waitUntil, preview, config} = options
33
+ const withCache = createWithCache_unstable({
34
+ cache,
35
+ waitUntil,
36
+ })
37
+
38
+ const sanity: Sanity = {
39
+ client: createClient(config),
40
+ async query({query, params, cache: strategy = CacheLong()}) {
41
+ const queryHash = await hashQuery(query, params)
42
+
43
+ return withCache(queryHash, strategy, () => sanity.client.fetch(query, params))
44
+ },
45
+ }
46
+
47
+ if (preview) {
48
+ sanity.preview = {session: preview.session}
49
+
50
+ if (preview.session.has('projectId')) {
51
+ sanity.preview = {
52
+ ...sanity.preview,
53
+ projectId: config.projectId,
54
+ dataset: config.dataset,
55
+ token: preview.token,
56
+ }
57
+
58
+ sanity.client = sanity.client.withConfig({
59
+ useCdn: false,
60
+ token: preview.token,
61
+ })
62
+
63
+ sanity.query = ({query, params}) => {
64
+ return sanity.client.fetch(query, params)
65
+ }
66
+ }
67
+ }
68
+
69
+ return sanity
70
+ }
71
+
72
+ export function isPreviewModeEnabled(
73
+ preview: Sanity['preview']
74
+ ): preview is {session: PreviewSession} & PreviewData {
75
+ // @ts-expect-error
76
+ return preview?.token !== null
77
+ }
78
+
79
+ /**
80
+ * Create an SHA-256 hash as a hex string
81
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
82
+ */
83
+ export async function sha256(message: string): Promise<string> {
84
+ // encode as UTF-8
85
+ const messageBuffer = await new TextEncoder().encode(message)
86
+ // hash the message
87
+ const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer)
88
+ // convert bytes to hex string
89
+ return Array.from(new Uint8Array(hashBuffer))
90
+ .map((b) => b.toString(16).padStart(2, '0'))
91
+ .join('')
92
+ }
93
+
94
+ /**
95
+ * Hash query and its parameters for use as cache key
96
+ * NOTE: Oxygen deployment will break if the cache key is long or contains `\n`
97
+ */
98
+ function hashQuery(
99
+ query: useSanityQuery['query'],
100
+ params: useSanityQuery['params']
101
+ ): Promise<string> {
102
+ let hash = query
103
+
104
+ if (params !== null) {
105
+ hash += JSON.stringify(params)
106
+ }
107
+
108
+ return sha256(hash)
109
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './client'
2
+ export * from './preview'
3
+ export * from './types'
@@ -0,0 +1,123 @@
1
+ /* eslint-disable react/require-default-props */
2
+ import {definePreview, type Params, PreviewSuspense} from '@sanity/preview-kit'
3
+ import {createCookieSessionStorage, type Session, type SessionStorage} from '@shopify/remix-oxygen'
4
+ import {createContext, ElementType, type ReactNode, useContext, useMemo} from 'react'
5
+
6
+ type UsePreview = <R = any, P extends Params = Params, Q extends string = string>(
7
+ query: Q,
8
+ params?: P,
9
+ serverSnapshot?: R
10
+ ) => R
11
+
12
+ export type PreviewData = {
13
+ projectId: string
14
+ dataset: string
15
+ token: string
16
+ }
17
+
18
+ export type PreviewProps = {
19
+ children: ReactNode
20
+ fallback?: ReactNode
21
+ preview?: PreviewData
22
+ }
23
+
24
+ export class PreviewSession {
25
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
26
+ constructor(private sessionStorage: SessionStorage, private session: Session) {}
27
+
28
+ static async init(request: Request, secrets: string[]) {
29
+ const storage = createCookieSessionStorage({
30
+ cookie: {
31
+ name: '__preview',
32
+ httpOnly: true,
33
+ sameSite: true,
34
+ secrets,
35
+ },
36
+ })
37
+
38
+ const session = await storage.getSession(request.headers.get('Cookie'))
39
+
40
+ return new this(storage, session)
41
+ }
42
+
43
+ has(key: string) {
44
+ return this.session.has(key)
45
+ }
46
+
47
+ // get(key: string) {
48
+ // return this.session.get(key);
49
+ // }
50
+
51
+ destroy() {
52
+ return this.sessionStorage.destroySession(this.session)
53
+ }
54
+
55
+ // unset(key: string) {
56
+ // this.session.unset(key);
57
+ // }
58
+
59
+ set(key: string, value: any) {
60
+ this.session.set(key, value)
61
+ }
62
+
63
+ commit() {
64
+ return this.sessionStorage.commitSession(this.session)
65
+ }
66
+ }
67
+
68
+ const PreviewContext = createContext<
69
+ | {
70
+ /**
71
+ * Query Sanity and subscribe to changes, optionally
72
+ * passing a server snapshot to speed up hydration
73
+ */
74
+ usePreview: UsePreview
75
+ }
76
+ | undefined
77
+ >(undefined)
78
+
79
+ export const usePreviewContext = () => useContext(PreviewContext)
80
+
81
+ /**
82
+ * Conditionally apply `PreviewSuspense` boundary
83
+ * @see https://www.sanity.io/docs/preview-content-on-site
84
+ */
85
+ export function Preview(props: PreviewProps) {
86
+ const {children, preview} = props
87
+
88
+ if (!preview?.token) {
89
+ return <>{children}</>
90
+ }
91
+
92
+ const fallback = props.fallback ?? <div>Loading preview...</div>
93
+ const {projectId, dataset, token} = preview
94
+ const _usePreview = definePreview({
95
+ projectId,
96
+ dataset,
97
+ overlayDrafts: true,
98
+ })
99
+
100
+ function usePreview<R = any, P extends Params = Params, Q extends string = string>(
101
+ query: Q,
102
+ params?: P,
103
+ serverSnapshot?: R
104
+ ): R {
105
+ return _usePreview(token, query, params, serverSnapshot)
106
+ }
107
+ usePreview satisfies UsePreview
108
+
109
+ return (
110
+ <PreviewContext.Provider value={{usePreview}}>
111
+ <PreviewSuspense fallback={fallback}>{children}</PreviewSuspense>
112
+ </PreviewContext.Provider>
113
+ )
114
+ }
115
+
116
+ /**
117
+ * Select and memoize which component to render based on preview mode
118
+ */
119
+ export function usePreviewComponent<T>(component: ElementType<T>, preview: ElementType<T>) {
120
+ const isPreview = Boolean(usePreviewContext())
121
+
122
+ return useMemo(() => (isPreview ? preview : component), [component, isPreview, preview])
123
+ }
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type {CacheShort} from '@shopify/hydrogen'
2
+
3
+ interface ExecutionContext {
4
+ waitUntil(promise: Promise<any>): void
5
+ }
6
+
7
+ export type EnvironmentOptions = {
8
+ /**
9
+ * A Cache API instance.
10
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Cache
11
+ */
12
+ cache: Cache
13
+ /**
14
+ * A runtime utility for serverless environments
15
+ * @see https://developers.cloudflare.com/workers/runtime-apis/fetch-event/#waituntil
16
+ */
17
+ waitUntil: ExecutionContext['waitUntil']
18
+ }
19
+
20
+ /** @see https://shopify.dev/docs/custom-storefronts/hydrogen/data-fetching/cache#caching-strategies */
21
+ export type CachingStrategy = ReturnType<typeof CacheShort>