Skip to content

Commit

Permalink
feat: add stega support to the core client (#495)
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan authored Jan 26, 2024
1 parent 18f3bfc commit a1abe4a
Show file tree
Hide file tree
Showing 21 changed files with 327 additions and 544 deletions.
2 changes: 1 addition & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"extends": ["github>sanity-io/renovate-config", ":reviewer(team:ecosystem)"],
"packageRules": [
{
"matchPackageNames": ["get-it"],
"matchPackageNames": ["@vercel/stega", "get-it"],
"rangeStrategy": "bump",
"semanticCommitType": "fix",
"groupName": null,
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,10 +657,10 @@ useContentSourceMap(resultSourceMap)

#### Using [Visual editing][visual-editing] with steganography

A turnkey integration with [Visual editing][visual-editing] is available in [`@sanity/client/stega`]. It creates edit intent links for all the string values in your query result, using [steganography](https://npmjs.com/package/@vercel/stega) under the hood.
A turnkey integration with [Visual editing][visual-editing] is available in [`@sanity/client`], with additional utils available on [`@sanity/client/stega`]. It creates edit intent links for all the string values in your query result, using [steganography](https://npmjs.com/package/@vercel/stega) under the hood. The code that handles stega is lazy loaded on demand when `client.fetch` is called, if `client.config().stega.enabled` is `true`.

```ts
import {createClient} from '@sanity/client/stega'
import {createClient} from '@sanity/client'
const client = createClient({
// ...base config options
Expand Down Expand Up @@ -713,7 +713,7 @@ const debugClient = client.withConfig({
})
```

Removing stega from part of the result:
Removing stega from part of the result, available on [`@sanity/client/stega`]:

```ts
import {vercelStegaCleanAll} from '@sanity/client/stega'
Expand All @@ -728,7 +728,7 @@ const videoAsset = vercelStegaCleanAll(result.videoAsset)
If you want to create an edit link to something that isn't a string, or a field that isn't rendered directly, like a `slug` used in a URL but not rendered on the page, you can use the `resolveEditUrl` function.

```ts
import {createClient} from '@sanity/client' // or '@sanity/client/stega'
import {createClient} from '@sanity/client'
import {resolveEditUrl} from '@sanity/client/csm'

const client = createClient({
Expand Down
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {defineConfig} from '@sanity/pkg-utils'
export default defineConfig({
tsconfig: 'tsconfig.dist.json',

external: (prev) => prev.filter((id) => id !== '@vercel/stega'),

extract: {
rules: {
'ae-forgotten-export': 'error',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sanity/client",
"version": "6.11.3",
"version": "6.11.4-canary.0",
"description": "Client for retrieving, creating and patching data from Sanity.io",
"keywords": [
"sanity",
Expand Down
44 changes: 40 additions & 4 deletions src/SanityClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,17 @@ export class ObservableSanityClient {
* @param newConfig - New client configuration properties, shallowly merged with existing configuration
*/
withConfig(newConfig?: Partial<ClientConfig>): ObservableSanityClient {
return new ObservableSanityClient(this.#httpRequest, {...this.config(), ...newConfig})
const thisConfig = this.config()
return new ObservableSanityClient(this.#httpRequest, {
...thisConfig,
...newConfig,
stega: {
...(thisConfig.stega || {}),
...(typeof newConfig?.stega === 'boolean'
? {enabled: newConfig.stega}
: newConfig?.stega || {}),
},
})
}

/**
Expand Down Expand Up @@ -157,7 +167,14 @@ export class ObservableSanityClient {
params?: Q,
options: FilteredResponseQueryOptions | UnfilteredResponseQueryOptions = {},
): Observable<RawQueryResponse<R> | R> {
return dataMethods._fetch<R, Q>(this, this.#httpRequest, query, params, options)
return dataMethods._fetch<R, Q>(
this,
this.#httpRequest,
this.#clientConfig.stega,
query,
params,
options,
)
}

/**
Expand Down Expand Up @@ -738,7 +755,17 @@ export class SanityClient {
* @param newConfig - New client configuration properties, shallowly merged with existing configuration
*/
withConfig(newConfig?: Partial<ClientConfig>): SanityClient {
return new SanityClient(this.#httpRequest, {...this.config(), ...newConfig})
const thisConfig = this.config()
return new SanityClient(this.#httpRequest, {
...thisConfig,
...newConfig,
stega: {
...(thisConfig.stega || {}),
...(typeof newConfig?.stega === 'boolean'
? {enabled: newConfig.stega}
: newConfig?.stega || {}),
},
})
}

/**
Expand Down Expand Up @@ -783,7 +810,16 @@ export class SanityClient {
params?: Q,
options: FilteredResponseQueryOptions | UnfilteredResponseQueryOptions = {},
): Promise<RawQueryResponse<R> | R> {
return lastValueFrom(dataMethods._fetch<R, Q>(this, this.#httpRequest, query, params, options))
return lastValueFrom(
dataMethods._fetch<R, Q>(
this,
this.#httpRequest,
this.#clientConfig.stega,
query,
params,
options,
),
)
}

/**
Expand Down
46 changes: 34 additions & 12 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const defaultConfig = {
apiHost: 'https://api.sanity.io',
apiVersion: '1',
useProjectHostname: true,
stega: {enabled: false},
} satisfies ClientConfig

const LOCALHOSTS = ['localhost', '127.0.0.1', '0.0.0.0']
Expand Down Expand Up @@ -44,12 +45,24 @@ export const initConfig = (
config: Partial<ClientConfig>,
prevConfig: Partial<ClientConfig>,
): InitializedClientConfig => {
const specifiedConfig = Object.assign({}, prevConfig, config)
const specifiedConfig = {
...prevConfig,
...config,
stega: {
...(typeof prevConfig.stega === 'boolean'
? {enabled: prevConfig.stega}
: prevConfig.stega || defaultConfig.stega),
...(typeof config.stega === 'boolean' ? {enabled: config.stega} : config.stega || {}),
},
}
if (!specifiedConfig.apiVersion) {
warnings.printNoApiVersionSpecifiedWarning()
}

const newConfig = Object.assign({} as InitializedClientConfig, defaultConfig, specifiedConfig)
const newConfig = {
...defaultConfig,
...specifiedConfig,
} as InitializedClientConfig
const projectBased = newConfig.useProjectHostname

if (typeof Promise === 'undefined') {
Expand All @@ -65,20 +78,29 @@ export const initConfig = (
validateApiPerspective(newConfig.perspective)
}

if (
'encodeSourceMapAtPath' in newConfig ||
'encodeSourceMap' in newConfig ||
'studioUrl' in newConfig ||
'logger' in newConfig
) {
if ('encodeSourceMap' in newConfig) {
throw new Error(
`It looks like you're using options meant for '@sanity/preview-kit/client', such as 'encodeSourceMapAtPath', 'encodeSourceMap', 'studioUrl' and 'logger'. Make sure you're using the right import.`,
`It looks like you're using options meant for '@sanity/preview-kit/client'. 'encodeSourceMap' is not supported in '@sanity/client'. Did you mean 'stega.enabled'?`,
)
}

if ('stega' in newConfig && newConfig['stega'] !== undefined && newConfig['stega'] !== false) {
if ('encodeSourceMapAtPath' in newConfig) {
throw new Error(
`It looks like you're using options meant for '@sanity/preview-kit/client'. 'encodeSourceMapAtPath' is not supported in '@sanity/client'. Did you mean 'stega.filter'?`,
)
}
if (typeof newConfig.stega.enabled !== 'boolean') {
throw new Error(`stega.enabled must be a boolean, received ${newConfig.stega.enabled}`)
}
if (newConfig.stega.enabled && newConfig.stega.studioUrl === undefined) {
throw new Error(`stega.studioUrl must be defined when stega.enabled is true`)
}
if (
newConfig.stega.enabled &&
typeof newConfig.stega.studioUrl !== 'string' &&
typeof newConfig.stega.studioUrl !== 'function'
) {
throw new Error(
`It looks like you're using options meant for '@sanity/client/stega'. Make sure you're using the right import. Or set 'stega' in 'createClient' to 'false'.`,
`stega.studioUrl must be a string or a function, received ${newConfig.stega.studioUrl}`,
)
}

Expand Down
46 changes: 37 additions & 9 deletions src/data/dataMethods.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {type MonoTypeOperatorFunction, Observable} from 'rxjs'
import {filter, map} from 'rxjs/operators'
import {from, type MonoTypeOperatorFunction, Observable} from 'rxjs'
import {combineLatestWith, filter, map} from 'rxjs/operators'

import {validateApiPerspective} from '../config'
import {requestOptions} from '../http/requestOptions'
import type {ObservableSanityClient, SanityClient} from '../SanityClient'
import {vercelStegaCleanAll} from '../stega/vercelStegaCleanAll'
import type {
AllDocumentIdsMutationOptions,
AllDocumentsMutationOptions,
Expand All @@ -15,6 +16,7 @@ import type {
HttpRequest,
HttpRequestEvent,
IdentifiedSanityDocumentStub,
InitializedStegaConfig,
MultipleMutationResult,
Mutation,
MutationSelection,
Expand Down Expand Up @@ -65,29 +67,55 @@ const getQuerySizeLimit = 11264
export function _fetch<R, Q extends QueryParams>(
client: ObservableSanityClient | SanityClient,
httpRequest: HttpRequest,
_stega: InitializedStegaConfig,
query: string,
params?: Q,
_params?: Q,
options: FilteredResponseQueryOptions | UnfilteredResponseQueryOptions = {},
): Observable<RawQueryResponse<R> | R> {
if ('stega' in options && options['stega'] !== undefined && options['stega'] !== false) {
throw new Error(
`It looks like you're using options meant for '@sanity/client/stega'. Make sure you're using the right import. Or set 'stega' in 'fetch' to 'false'.`,
)
}
const stega =
'stega' in options
? {
...(_stega || {}),
...(typeof options.stega === 'boolean' ? {enabled: options.stega} : options.stega || {}),
}
: _stega
const params = stega.enabled ? vercelStegaCleanAll(_params) : _params
const mapResponse =
options.filterResponse === false ? (res: Any) => res : (res: Any) => res.result
const {cache, next, ...opts} = {
// Opt out of setting a `signal` on an internal `fetch` if one isn't provided.
// This is necessary in React Server Components to avoid opting out of Request Memoization.
useAbortSignal: typeof options.signal !== 'undefined',
// Set `resultSourceMap' when stega is enabled, as it's required for encoding.
resultSourceMap: stega.enabled ? 'withKeyArraySelector' : options.resultSourceMap,
...options,
}
const reqOpts =
typeof cache !== 'undefined' || typeof next !== 'undefined'
? {...opts, fetch: {cache, next}}
: opts

return _dataRequest(client, httpRequest, 'query', {query, params}, reqOpts).pipe(map(mapResponse))
const $request = _dataRequest(client, httpRequest, 'query', {query, params}, reqOpts)
return stega.enabled
? $request.pipe(
combineLatestWith(
from(
import('../stega/stegaEncodeSourceMap').then(
({stegaEncodeSourceMap}) => stegaEncodeSourceMap,
),
),
),
map(
([res, stegaEncodeSourceMap]: [
Any,
(typeof import('../stega/stegaEncodeSourceMap'))['stegaEncodeSourceMap'],
]) => {
const result = stegaEncodeSourceMap(res.result, res.resultSourceMap, stega)
return mapResponse({...res, result})
},
),
)
: $request.pipe(map(mapResponse))
}

/** @internal */
Expand Down
Loading

0 comments on commit a1abe4a

Please sign in to comment.