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 +21 -0
- package/README.md +372 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.esm.js +152 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
- package/src/client.ts +109 -0
- package/src/index.ts +3 -0
- package/src/preview.tsx +123 -0
- package/src/types.ts +21 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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":"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"}
|
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
package/src/preview.tsx
ADDED
|
@@ -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>
|