Skip to content

Commit

Permalink
fix: ready the first test
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan committed Aug 12, 2022
1 parent 8928d6e commit 8b00952
Show file tree
Hide file tree
Showing 18 changed files with 299 additions and 114 deletions.
2 changes: 1 addition & 1 deletion demo/sanity.env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/* eslint-disable no-process-env */
export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!
// eslint-disable-next-line no-process-env
export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"dependencies": {
"@sanity/client": "^3.3.3",
"@sanity/groq-store": "^0.4.0",
"eventsource": "^2.0.2",
"groq": "^2.29.3",
"use-sync-external-store": "^1.2.0"
},
Expand Down Expand Up @@ -116,6 +117,7 @@
"peerDependencies": {
"next": "^12.2.4",
"react": "^16.3 || ^17 || ^18",
"react-dom": "^16.3 || ^17 || ^18",
"sanity": "^3.0.0-dev-preview.12",
"styled-components": "^5.3.5"
},
Expand All @@ -125,7 +127,10 @@
"targets": {
"cjs": {
"distDir": "./lib/cjs",
"source": "./src/studio.ts",
"source": [
"./src/studio/index.ts",
"./src/preview/index.ts"
],
"outputFormat": "commonjs",
"isLibrary": true,
"context": "browser",
Expand All @@ -136,7 +141,10 @@
},
"esm": {
"distDir": "./lib/esm",
"source": "./src/studio.ts",
"source": [
"./src/studio/index.ts",
"./src/preview/index.ts"
],
"outputFormat": "esmodule",
"isLibrary": true,
"context": "browser",
Expand Down
5 changes: 4 additions & 1 deletion pages/api/exit-preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default async function exit(_, res) {
import type {NextApiRequest, NextApiResponse} from 'next'

// eslint-disable-next-line require-await
export default async function exit(_: NextApiRequest, res: NextApiResponse): Promise<void> {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData()

Expand Down
6 changes: 4 additions & 2 deletions pages/api/preview-cookie-only.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {NextApiRequest, NextApiResponse} from 'next'
/* eslint-disable no-process-env */

export default async function preview(req, res) {
// eslint-disable-next-line require-await, consistent-return
export default async function preview(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const secret = process.env.NEXT_PUBLIC_PREVIEW_SECRET
// Only require a secret when in production
if (!secret && process.env.NODE_ENV === 'production') {
Expand All @@ -16,7 +18,7 @@ export default async function preview(req, res) {
title: 'Preview Mode: Cookie',
description:
"Requires the user to be authenticated to the Studio, and allow cross-origin cookies (Safari doesn't)",
authMode: null,
authMode: 'cookie',
token: null,
})
res.writeHead(307, {Location: '/'})
Expand Down
4 changes: 3 additions & 1 deletion pages/api/preview-token-only.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {NextApiRequest, NextApiResponse} from 'next'
/* eslint-disable no-process-env */

export default async function preview(req, res) {
// eslint-disable-next-line require-await, consistent-return
export default async function preview(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const secret = process.env.NEXT_PUBLIC_PREVIEW_SECRET
// Only require a secret when in production
if (!secret && process.env.NODE_ENV === 'production') {
Expand Down
4 changes: 3 additions & 1 deletion pages/api/preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {NextApiRequest, NextApiResponse} from 'next'
/* eslint-disable no-process-env */

export default async function preview(req, res) {
// eslint-disable-next-line require-await, consistent-return
export default async function preview(req: NextApiRequest, res: NextApiResponse): Promise<void> {
const secret = process.env.NEXT_PUBLIC_PREVIEW_SECRET
// Only require a secret when in production
if (!secret && process.env.NODE_ENV === 'production') {
Expand Down
1 change: 1 addition & 0 deletions pages/fixtures/preview-cookie-only/[[...studio]].tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-process-env */
import {useMemo} from 'react'
import type {WorkspaceOptions} from 'sanity'
import _config from 'sanity.config'
Expand Down
1 change: 1 addition & 0 deletions pages/fixtures/preview-token-only/[[...studio]].tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-process-env */
import {useMemo} from 'react'
import type {WorkspaceOptions} from 'sanity'
import _config from 'sanity.config'
Expand Down
46 changes: 29 additions & 17 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @next/next/no-img-element */
import {dataset, projectId} from 'demo/sanity.env'
import {urlForImage} from 'demo/sanity.helpers'
import {getClient, indexQuery, overlayDrafts} from 'demo/sanity.server'
Expand All @@ -10,27 +11,31 @@ type Props = {
allPosts: any[]
title: string
description: string
authMode: any
token: any
preview: boolean
}

// eslint-disable-next-line no-warning-comments
// @TODO swap to React.lazy + Suspense when Studio is ready for React v18
const PreviewSubscription = dynamic(() => import('src/preview'))
const PreviewMode = dynamic(() => import('src/preview'))

export default function IndexPage(props: Props) {
const [posts, setPosts] = useState(props.allPosts)
console.log('IndexPage', props)
const [authState, setAuthState] = useState('checking')

return (
<>
{props.preview && (
<PreviewSubscription
<PreviewMode
projectId={projectId}
dataset={dataset}
initialData={props.allPosts}
initial={props.allPosts}
query={indexQuery}
setData={setPosts}
onChange={setPosts}
authMode={props.authMode}
token={props.token}
onAuth={setAuthState}
/>
)}
<div className="relative px-4 pt-16 pb-20 bg-gray-50 sm:px-6 lg:pt-24 lg:pb-28 lg:px-8">
Expand All @@ -47,10 +52,14 @@ export default function IndexPage(props: Props) {
</p>
</div>
{props.preview && (
<div className="text-center">
<Link prefetch={false} href="/api/exit-preview">
<a>Exit preview</a>
</Link>
<div>
<div>authState: {JSON.stringify(authState)}</div>
<div>authMode: {JSON.stringify(props.authMode)}</div>
<div className="text-center">
<Link prefetch={false} href="/api/exit-preview">
<a>Exit preview</a>
</Link>
</div>
</div>
)}
<div className="grid max-w-lg gap-5 mx-auto mt-12 lg:grid-cols-3 lg:max-w-none">
Expand Down Expand Up @@ -87,7 +96,7 @@ export default function IndexPage(props: Props) {
{post.author?.image && (
<img
className="w-10 h-10 rounded-full"
src={urlForImage(post.author?.image)}
src={urlForImage(post.author?.image).url()}
alt=""
/>
)}
Expand Down Expand Up @@ -116,20 +125,23 @@ export default function IndexPage(props: Props) {
)
}

export const getStaticProps: GetStaticProps = async ({preview = false, previewData = {}}) => {
let client = getClient(preview)
if (previewData?.token) {
client = client.withConfig({token: previewData.token})
}
export const getStaticProps: GetStaticProps<any, any, any> = async ({
preview = false,
previewData = {},
}) => {
const client =
preview && previewData?.token
? getClient(false).withConfig({token: previewData.token})
: getClient(preview)
const allPosts = overlayDrafts(await client.fetch(indexQuery))
return {
props: {
allPosts,
preview,
title: previewData?.title || 'Blog fixture',
description: previewData?.description || 'Used to test preview mode',
authMode: previewData?.authMode,
token: previewData?.token,
authMode: previewData?.authMode || null,
token: previewData?.token || null,
},
// If webhooks isn't setup then attempt to re-generate in 1 minute intervals
// eslint-disable-next-line no-process-env
Expand Down
42 changes: 42 additions & 0 deletions src/preview/PreviewMode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {memo, useEffect} from 'react'

import {type PreviewSubscriptionProps, PreviewSubscription} from './PreviewSubscription'
import {PreviewSubscriptionWithToken} from './PreviewSubscriptionWithToken'
import {useAuthenticated} from './useAuthenticated'

export interface PreviewModeProps extends PreviewSubscriptionProps {
authMode: 'dual' | 'token' | 'cookie'
onAuth: (authState: 'token' | 'cookie' | 'failed') => void
}
const PreviewModeComponent = ({authMode, onAuth, ...props}: PreviewModeProps) => {
const {projectId, token} = props
const authState = useAuthenticated({projectId, authMode, token})

useEffect(() => {
if (onAuth && authState !== 'checking') {
onAuth(authState)
}
}, [authState, onAuth])

if (authState === 'failed' && !onAuth) {
throw new Error('Failed to authenticate, provide an onAuth callback to silence this error')
}

switch (authState) {
case 'checking':
case 'failed':
return null
case 'token':
return !props.EventSource && props.token ? (
<PreviewSubscriptionWithToken {...props} token={props.token!} />
) : (
<PreviewSubscription {...props} />
)
case 'cookie':
return <PreviewSubscription {...props} />
default:
throw new Error(`Unknown auth state: ${authState}`)
}
}

export const PreviewMode = memo(PreviewModeComponent)
94 changes: 8 additions & 86 deletions src/preview/PreviewSubscription.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,18 @@
import {groqStore} from '@sanity/groq-store'
import EventSource from 'eventsource'
import {
type Dispatch,
type SetStateAction,
type TransitionStartFunction,
memo,
useEffect,
useMemo,
} from 'react'
import {memo, useEffect} from 'react'
import {unstable_batchedUpdates} from 'react-dom'
import type {GroqStoreEventSource} from 'src/types'
import {useSyncExternalStore} from 'use-sync-external-store/shim'

export type Params = Record<string, unknown>
import {type SyncGroqStoreHookProps, useSyncGroqStore} from './useSyncGroqStore'

export interface PreviewSubscriptionProps {
// required stuff
setData: Dispatch<SetStateAction<any[]>>
initialData: any
projectId: string
dataset: string
query: string
// optional stuff
params?: Params
documentLimit?: number
startTransition?: TransitionStartFunction
// Both or neither
// @TODO setup typing that enforce this condition
token?: string
EventSource?: GroqStoreEventSource
export interface PreviewSubscriptionProps extends SyncGroqStoreHookProps {
onChange: (data: any) => void
}
function PreviewSubscriptionComponent(props: PreviewSubscriptionProps) {
console.log('PreviewSubscription', props, groqStore)

const forwardedProps = useMemo(() => {
return props.token && !props.EventSource ? {...props, EventSource} : props
}, [props])
const data = useSyncGroqStore(forwardedProps)
console.log('sync data', data)

const {setData, startTransition = unstable_batchedUpdates} = props
const PreviewSubscriptionComponent = ({onChange, ...props}: PreviewSubscriptionProps) => {
const data = useSyncGroqStore(props)
useEffect(() => {
console.count('data changed', data)
startTransition(() => setData(data))
}, [data, setData])
unstable_batchedUpdates(() => onChange(data))
}, [data, onChange])

return null
}

export const PreviewSubscription = memo(PreviewSubscriptionComponent)

type SyncGroqStore = {
getSnapshot: () => any
getServerSnapshot: () => any
subscribe: (onStoreChange: () => void) => () => void
}

const useGroqStore = (props: PreviewSubscriptionProps): SyncGroqStore => {
const {initialData, projectId, dataset, documentLimit, token, EventSource, params, query} =
props
return useMemo<SyncGroqStore>(() => {
let snapshot: any = initialData
return {
getSnapshot: () => snapshot,
subscribe: (onStoreChange: () => void) => {
console.log('subscribe', props)
const store = groqStore({
projectId,
dataset,
documentLimit,
token,
EventSource,
listen: true,
overlayDrafts: true,
subscriptionThrottleMs: 1,
})
const subscription = store.subscribe(query, params as any, (err, result) => {
if (err) {
throw err
} else {
snapshot = result
onStoreChange()
}
})

return () => subscription.unsubscribe()
},
}
}, [EventSource, dataset, documentLimit, initialData, params, projectId, props, query, token])
}

const useSyncGroqStore = (props: PreviewSubscriptionProps) => {
const store = useGroqStore(props)
return useSyncExternalStore(store.subscribe, store.getSnapshot)
}
26 changes: 26 additions & 0 deletions src/preview/PreviewSubscriptionWithToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import EventSource from 'eventsource'
import {memo} from 'react'

import {type PreviewSubscriptionProps, PreviewSubscription} from './PreviewSubscription'

// EventSource is a very chonky boi, that's why this is in a separate component
// eslint-disable-next-line no-warning-comments
// @TODO implement code-splitting and lazy loading for this component

export interface PreviewSubscriptionWithTokenProps
// EventSource is provided by this component, if you're providing it then just use PreviewSubscription directly
extends Omit<PreviewSubscriptionProps, 'EventSource' | 'token'> {
token: string
}
const PreviewSubscriptionWithTokenComponent = ({
token,
...props
}: PreviewSubscriptionWithTokenProps) => {
if (!token) {
throw new TypeError('token is required')
}

return <PreviewSubscription {...props} token={token} EventSource={EventSource} />
}

export const PreviewSubscriptionWithToken = memo(PreviewSubscriptionWithTokenComponent)
9 changes: 7 additions & 2 deletions src/preview/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {PreviewSubscription} from './PreviewSubscription'
import {PreviewMode} from './PreviewMode'

export default PreviewSubscription
export default PreviewMode
export * from './PreviewMode'
export * from './PreviewSubscription'
export * from './PreviewSubscriptionWithToken'
export * from './useAuthenticated'
export * from './useGroqStore'
export * from './useSyncGroqStore'

0 comments on commit 8b00952

Please sign in to comment.