@prismicio/client
Version:
The official JavaScript + TypeScript client library for Prismic
289 lines (264 loc) • 7.76 kB
text/typescript
import type { Slice } from "../types/value/slice"
/**
* Convert a value to a lazyily loaded module. This is useful when using
* functions like `() => import("...")`.
*/
type LazyModule<T> = () => Promise<T | { default: T }>
/**
* Mark a type as potentially lazy-loaded via a module.
*/
type MaybeLazyModule<T> = T | LazyModule<T>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyFunction = (...args: any[]) => any
/**
* Returns the type of a `SliceLike` type.
*
* @typeParam Slice - The Slice from which the type will be extracted.
*/
type ExtractSliceType<TSlice extends SliceLike> = TSlice extends SliceLikeRestV2
? TSlice["slice_type"]
: TSlice extends SliceLikeGraphQL
? TSlice["type"]
: never
/**
* The minimum required properties to represent a Prismic slice from the Prismic
* Content API for the `mapSliceZone()` helper.
*
* @typeParam SliceType - Type name of the slice.
*/
type SliceLikeRestV2<TSliceType extends string = string> = Pick<
Slice<TSliceType>,
"id" | "slice_type"
>
/**
* The minimum required properties to represent a Prismic slice from the Prismic
* GraphQL API for the `mapSliceZone()` helper.
*
* @typeParam SliceType - Type name of the slice.
*/
type SliceLikeGraphQL<TSliceType extends string = string> = {
type: Slice<TSliceType>["slice_type"]
}
/**
* The minimum required properties to represent a Prismic slice for the
* `mapSliceZone()` helper.
*
* If using Prismic's Content API, use the `Slice` export from
* `@prismicio/client` for a full interface.
*
* @typeParam SliceType - Type name of the slice.
*/
type SliceLike<TSliceType extends string = string> =
| SliceLikeRestV2<TSliceType>
| SliceLikeGraphQL<TSliceType>
/**
* A looser version of the `SliceZone` type from `@prismicio/client` using
* `SliceLike`.
*
* If using Prismic's Content API, use the `SliceZone` export from
* `@prismicio/client` for the full type.
*
* @typeParam TSlice - The type(s) of a slice in the slice zone.
*/
type SliceZoneLike<TSlice extends SliceLike = SliceLike> = readonly TSlice[]
/**
* A set of properties that identify a Slice as having been mapped. Consumers of
* the mapped Slice Zone can use these properties to detect and specially handle
* mapped Slices.
*/
type MappedSliceLike = {
/**
* If `true`, this Slice has been modified from its original value using a
* mapper.
*
* @internal
*/
__mapped: true
}
/**
* Arguments for a function mapping content from a Prismic Slice using the
* `mapSliceZone()` helper.
*
* @typeParam TSlice - The Slice passed as a prop.
* @typeParam TContext - Arbitrary data passed to `mapSliceZone()` and made
* available to all Slice mappers.
*/
type SliceMapperArgs<
TSlice extends SliceLike = SliceLike,
TContext = unknown,
> = {
/**
* Slice data.
*/
slice: TSlice
/**
* The index of the Slice in the Slice Zone.
*/
index: number
/**
* All Slices from the Slice Zone to which the Slice belongs.
*/
// TODO: We have to keep this list of Slices general due to circular
// reference limtiations. If we had another generic to determine the full
// union of Slice types, it would include TSlice. This causes TypeScript to
// throw a compilation error.
slices: SliceZoneLike<
TSlice extends SliceLikeGraphQL ? SliceLikeGraphQL : SliceLikeRestV2
>
/**
* Arbitrary data passed to `mapSliceZone()` and made available to all Slice
* mappers.
*/
context: TContext
}
/**
* A record of mappers.
*/
type SliceMappers<TSlice extends SliceLike = SliceLike, TContext = unknown> = {
[P in ExtractSliceType<TSlice>]?: MaybeLazyModule<
SliceMapper<
Extract<TSlice, SliceLike<P>>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
TContext
>
>
}
/**
* A function that maps a Slice and its metadata to a modified version. The
* return value will replace the Slice in the Slice Zone.
*/
export type SliceMapper<
TSlice extends SliceLike = SliceLike,
TMappedSlice extends Record<string, unknown> | undefined | void =
| Record<string, unknown>
| undefined
| void,
TContext = unknown,
> = (
args: SliceMapperArgs<TSlice, TContext>,
) => TMappedSlice | Promise<TMappedSlice>
/**
* Unwraps a lazily loaded mapper module.
*/
type ResolveLazySliceMapperModule<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TSliceMapper extends SliceMapper<any, any> | LazyModule<SliceMapper>,
> =
TSliceMapper extends LazyModule<SliceMapper>
? Awaited<ReturnType<TSliceMapper>> extends {
default: unknown
}
? Awaited<ReturnType<TSliceMapper>>["default"]
: Awaited<ReturnType<TSliceMapper>>
: TSliceMapper
/**
* Transforms a Slice into its mapped version.
*/
type MapSliceLike<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TSliceLike extends SliceLike<any>,
TSliceMappers extends SliceMappers<
TSliceLike,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>,
> = TSliceLike extends Slice
? TSliceLike["slice_type"] extends keyof TSliceMappers
? TSliceMappers[TSliceLike["slice_type"]] extends AnyFunction
? SliceLikeRestV2<TSliceLike["slice_type"]> &
MappedSliceLike &
Awaited<
ReturnType<
ResolveLazySliceMapperModule<
TSliceMappers[TSliceLike["slice_type"]]
>
>
>
: TSliceLike
: TSliceLike
: TSliceLike extends SliceLikeGraphQL
? TSliceLike["type"] extends keyof TSliceMappers
? TSliceMappers[TSliceLike["type"]] extends AnyFunction
? SliceLikeGraphQL<TSliceLike["type"]> &
MappedSliceLike &
Awaited<
ReturnType<
ResolveLazySliceMapperModule<TSliceMappers[TSliceLike["type"]]>
>
>
: TSliceLike
: TSliceLike
: never
/**
* Transforms a Slice Zone using a set of mapping functions, one for each type
* of Slice. Mapping functions can be async.
*
* Whenever possible, use this function on the server to minimize client-side
* processing.
*
* @example
*
* ```typescript
* const mappedSliceZone = await mapSliceZone(page.data.slices, {
* code_block: ({ slice }) => ({
* codeHTML: await highlight(slice.primary.code),
* }),
* });
* ```
*/
export function mapSliceZone<
TSliceLike extends SliceLike,
TSliceMappers extends SliceMappers<TSliceLike, TContext>,
TContext = unknown,
>(
sliceZone: SliceZoneLike<TSliceLike>,
mappers: TSliceMappers,
context?: TContext,
): Promise<MapSliceLike<TSliceLike, TSliceMappers>[]> {
return Promise.all(
sliceZone.map(async (slice, index, slices) => {
const isRestSliceType = "slice_type" in slice
const sliceType = isRestSliceType ? slice.slice_type : slice.type
const mapper = mappers[sliceType as keyof typeof mappers]
if (!mapper) {
return slice
}
const mapperArgs = { slice, slices, index, context }
// `result` may be a mapper function OR a module
// containing a mapper function.
let result = await mapper(
// @ts-expect-error - I don't know how to fix this type
mapperArgs,
)
// `result` is a module containing a mapper function,
// we need to dig out the mapper function. `result`
// will be reassigned with the mapper function's value.
if (
// `mapper.length < 1` ensures the given
// function is something of the form:
// `() => import(...)`
mapper.length < 1 &&
(typeof result === "function" ||
(typeof result === "object" && "default" in result))
) {
result = "default" in result ? result.default : result
result = await result(mapperArgs)
}
if (isRestSliceType) {
return {
__mapped: true,
id: slice.id,
slice_type: sliceType,
...result,
}
} else {
return {
__mapped: true,
type: sliceType,
...result,
}
}
}),
)
}