UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

623 lines (515 loc) • 19.5 kB
import { type AssetSource, isSearchStrategy, type SchemaTypeDefinition, searchStrategies, type SearchStrategy, } from '@sanity/types' import {type ErrorInfo, type ReactNode} from 'react' import {type LocaleConfigContext, type LocaleDefinition, type LocaleResourceBundle} from '../i18n' import {type Template, type TemplateItem} from '../templates' import {getPrintableType} from '../util/getPrintableType' import { type DocumentActionComponent, type DocumentBadgeComponent, type DocumentInspector, } from './document' import {flattenConfig} from './flattenConfig' import { type AsyncConfigPropertyReducer, type ConfigContext, type ConfigPropertyReducer, type DocumentActionsContext, type DocumentBadgesContext, type DocumentCommentsEnabledContext, type DocumentInspectorContext, type DocumentLanguageFilterComponent, type DocumentLanguageFilterContext, type NewDocumentOptionsContext, type PluginOptions, type ResolveProductionUrlContext, type Tool, } from './types' export const initialDocumentBadges: DocumentBadgeComponent[] = [] export const initialDocumentActions: DocumentActionComponent[] = [] export const initialLanguageFilter: DocumentLanguageFilterComponent[] = [] export const schemaTypesReducer: ConfigPropertyReducer< SchemaTypeDefinition[], Omit<ConfigContext, 'schema' | 'currentUser' | 'client' | 'getClient' | 'i18n'> > = (prev, {schema}, context) => { const schemaTypes = schema?.types if (!schemaTypes) return prev if (typeof schemaTypes === 'function') return schemaTypes(prev, context) if (Array.isArray(schemaTypes)) return [...prev, ...schemaTypes] throw new Error( `Expected \`schema.types\` to be an array or a function, but received ${getPrintableType( schemaTypes, )}`, ) } export const resolveProductionUrlReducer: AsyncConfigPropertyReducer< string | undefined, ResolveProductionUrlContext > = async (prev, {document}, context) => { const resolveProductionUrl = document?.productionUrl // the redundant await is useful for error logging because the error is caught // in this stack vs somewhere down stream // eslint-disable-next-line no-return-await if (resolveProductionUrl) return await resolveProductionUrl(prev, context) return prev } export const toolsReducer: ConfigPropertyReducer<Tool[], ConfigContext> = ( prev, {tools}, context, ) => { if (!tools) return prev if (typeof tools === 'function') return tools(prev, context) if (Array.isArray(tools)) return [...prev, ...tools] throw new Error( `Expected \`tools\` to be an array or a function, but received ${getPrintableType(tools)}`, ) } // we will need this when we ressurect user config for search /*export const searchFilterReducer: ConfigPropertyReducer< SearchFilterDefinition<string>[], ConfigContext > = (prev, {search}, context) => { const filters = search?.filters if (!filters) return prev if (typeof filters === 'function') return filters(prev, context) if (Array.isArray(filters)) return [...prev, ...filters] throw new Error( `Expected \`search.filters\` to be an array or a function, but received ${typeof filters}` ) } export const searchOperatorsReducer: ConfigPropertyReducer< SearchOperatorDefinition[], ConfigContext > = (prev, {search}, context) => { const operators = search?.operators if (!operators) return prev if (typeof operators === 'function') return operators(prev, context) if (Array.isArray(operators)) return [...prev, ...operators] throw new Error( `Expected \`operators\` to be be an array or a function, but received ${getPrintableType(operators)}` ) }*/ export const schemaTemplatesReducer: ConfigPropertyReducer<Template[], ConfigContext> = ( prev, {schema}, context, ) => { const schemaTemplates = schema?.templates if (!schemaTemplates) return prev if (typeof schemaTemplates === 'function') return schemaTemplates(prev, context) if (Array.isArray(schemaTemplates)) return [...prev, ...schemaTemplates] throw new Error( `Expected \`schema.templates\` to be an array or a function, but received ${getPrintableType( schemaTemplates, )}`, ) } export const localeDefReducer: ConfigPropertyReducer<LocaleDefinition[], LocaleConfigContext> = ( prev, {i18n}, context, ) => { const locales = i18n?.locales if (!locales) return prev if (typeof locales === 'function') return locales(prev, context) if (Array.isArray(locales)) return [...prev, ...locales] throw new Error( `Expected \`i18n.locales\` to be an array or a function, but received ${getPrintableType( locales, )}`, ) } export const localeBundlesReducer: ConfigPropertyReducer< LocaleResourceBundle[], LocaleConfigContext > = (prev, {i18n}, context) => { const bundles = i18n?.bundles if (!bundles) return prev if (Array.isArray(bundles)) return [...prev, ...bundles] if (typeof bundles === 'function') return bundles(prev, context) throw new Error( `Expected \`i18n.bundles\` to be an array or a function, but received ${typeof bundles}`, ) } export const documentBadgesReducer: ConfigPropertyReducer< DocumentBadgeComponent[], DocumentBadgesContext > = (prev, {document}, context) => { const documentBadges = document?.badges if (!documentBadges) return prev if (typeof documentBadges === 'function') return documentBadges(prev, context) if (Array.isArray(documentBadges)) return [...prev, ...documentBadges] throw new Error( `Expected \`document.badges\` to be an array or a function, but received ${getPrintableType( documentBadges, )}`, ) } export const documentActionsReducer: ConfigPropertyReducer< DocumentActionComponent[], DocumentActionsContext > = (prev, {document}, context) => { const documentActions = document?.actions if (!documentActions) return prev if (typeof documentActions === 'function') return documentActions(prev, context) if (Array.isArray(documentActions)) return [...prev, ...documentActions] throw new Error( `Expected \`document.actions\` to be an array or a function, but received ${getPrintableType( documentActions, )}`, ) } export const newDocumentOptionsResolver: ConfigPropertyReducer< TemplateItem[], NewDocumentOptionsContext > = (prev, {document}, context) => { const resolveNewDocumentOptions = document?.newDocumentOptions if (!resolveNewDocumentOptions) return prev if (typeof resolveNewDocumentOptions !== 'function') { throw new Error( `Expected \`document.resolveNewDocumentOptions\` to be a function, but received ${getPrintableType( resolveNewDocumentOptions, )}`, ) } return resolveNewDocumentOptions(prev, context) } export const fileAssetSourceResolver: ConfigPropertyReducer<AssetSource[], ConfigContext> = ( prev, {form}, context, ) => { const assetSources = form?.file?.assetSources if (!assetSources) return prev if (typeof assetSources === 'function') return assetSources(prev, context) if (Array.isArray(assetSources)) return [...prev, ...assetSources] throw new Error( `Expected \`form.file.assetSources\` to be an array or a function, but received ${getPrintableType( assetSources, )}`, ) } export const imageAssetSourceResolver: ConfigPropertyReducer<AssetSource[], ConfigContext> = ( prev, {form}, context, ) => { const assetSources = form?.image?.assetSources if (!assetSources) return prev if (typeof assetSources === 'function') return assetSources(prev, context) if (Array.isArray(assetSources)) return [...prev, ...assetSources] throw new Error( `Expected \`form.image.assetSources\` to be an array or a function, but received ${getPrintableType( assetSources, )}`, ) } /** * @internal */ export const documentLanguageFilterReducer: ConfigPropertyReducer< DocumentLanguageFilterComponent[], DocumentLanguageFilterContext > = (prev, {document}, context) => { const resolveDocumentLanguageFilter = document?.unstable_languageFilter if (!resolveDocumentLanguageFilter) return prev if (typeof resolveDocumentLanguageFilter === 'function') return resolveDocumentLanguageFilter(prev, context) if (Array.isArray(resolveDocumentLanguageFilter)) return [...prev, ...resolveDocumentLanguageFilter] throw new Error( `Expected \`document.unstable_languageFilter\` to be an array or a function, but received ${getPrintableType( resolveDocumentLanguageFilter, )}`, ) } export const documentInspectorsReducer: ConfigPropertyReducer< DocumentInspector[], DocumentInspectorContext > = (prev, {document}, context) => { const resolveInspectorsFilter = document?.inspectors if (!resolveInspectorsFilter) return prev if (typeof resolveInspectorsFilter === 'function') return resolveInspectorsFilter(prev, context) if (Array.isArray(resolveInspectorsFilter)) return [...prev, ...resolveInspectorsFilter] throw new Error( `Expected \`document.inspectors\` to be an array or a function, but received ${getPrintableType( resolveInspectorsFilter, )}`, ) } export const documentCommentsEnabledReducer = (opts: { config: PluginOptions context: DocumentCommentsEnabledContext initialValue: boolean }): boolean => { const {config, context, initialValue} = opts const flattenedConfig = flattenConfig(config, []) // There is no concept of 'previous value' in this API. We only care about the final value. // That is, if a plugin returns true, but the next plugin returns false, the result will be false. // The last plugin 'wins'. const result = flattenedConfig.reduce((acc, {config: innerConfig}) => { const resolver = innerConfig.document?.comments?.enabled ?? innerConfig.document?.unstable_comments?.enabled if (!resolver && typeof resolver !== 'boolean') return acc if (typeof resolver === 'function') return resolver(context) if (typeof resolver === 'boolean') return resolver throw new Error( `Expected \`document.comments.enabled\` to be a boolean or a function, but received ${getPrintableType( resolver, )}`, ) }, initialValue) return result } export const onUncaughtErrorResolver = (opts: { config: PluginOptions context: {error: Error; errorInfo: ErrorInfo} }) => { const {config, context} = opts const flattenedConfig = flattenConfig(config, []) flattenedConfig.forEach(({config: pluginConfig}) => { // There is no concept of 'previous value' in this API. We only care about the final value. // That is, if a plugin returns true, but the next plugin returns false, the result will be false. // The last plugin 'wins'. const resolver = pluginConfig.onUncaughtError if (typeof resolver === 'function') return resolver(context.error, context.errorInfo) if (!resolver) return undefined throw new Error( `Expected \`document.onUncaughtError\` to be a a function, but received ${getPrintableType( resolver, )}`, ) }) } export const internalTasksReducer = (opts: { config: PluginOptions }): {footerAction: ReactNode} | undefined => { const {config} = opts const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce( (acc: {footerAction: ReactNode} | undefined, {config: innerConfig}) => { const resolver = innerConfig.__internal_tasks if (!resolver) return acc if (typeof resolver === 'object' && resolver.footerAction) return resolver throw new Error( `Expected \`__internal__tasks\` to be an object with footerAction, but received ${getPrintableType( resolver, )}`, ) }, undefined, ) return result } export const eventsAPIReducer = (opts: { config: PluginOptions initialValue: boolean key: 'releases' | 'documents' }): boolean => { const {config, initialValue} = opts const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce((acc: boolean, {config: innerConfig}) => { // @ts-expect-error enabled is a legacy option we want to warn beta testers in case they have enabled it. if (innerConfig.beta?.eventsAPI?.enabled) { throw new Error( `The \`beta.eventsAPI.enabled\` option has been removed. Use \`beta.eventsAPI.${opts.key}\` instead.`, ) } const enabled = innerConfig.beta?.eventsAPI?.[opts.key] if (typeof enabled === 'undefined') return acc if (typeof enabled === 'boolean') return enabled throw new Error( `Expected \`beta.eventsAPI.${opts.key}\` to be a boolean, but received ${getPrintableType( enabled, )}`, ) }, initialValue) return result } export const mediaLibraryEnabledReducer = (opts: { config: PluginOptions initialValue: boolean }): boolean => { const {config, initialValue} = opts const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce((acc, {config: innerConfig}) => { const resolver = innerConfig.mediaLibrary?.enabled if (!resolver && typeof resolver !== 'boolean') return acc if (typeof resolver === 'boolean') return resolver throw new Error( `Expected \`mediaLibrary.enabled\` to be a boolean, but received ${getPrintableType( resolver, )}`, ) }, initialValue) return result } export const mediaLibraryLibraryIdReducer = (opts: { config: PluginOptions initialValue: string | undefined }): string | undefined => { const {config, initialValue} = opts const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce((acc, {config: innerConfig}) => { const resolver = innerConfig.mediaLibrary?.libraryId if (!resolver && typeof resolver !== 'string') return acc if (typeof resolver === 'string') return resolver throw new Error( `Expected \`mediaLibrary.libraryId\` to be a string, but received ${getPrintableType( resolver, )}`, ) }, initialValue) return result } export const serverDocumentActionsReducer = (opts: { config: PluginOptions initialValue: boolean | undefined }): boolean | undefined => { const {config, initialValue} = opts const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce((acc: boolean | undefined, {config: innerConfig}) => { const enabled = innerConfig.__internal_serverDocumentActions?.enabled if (typeof enabled === 'undefined') return acc if (typeof enabled === 'boolean') return enabled throw new Error( `Expected \`__internal_serverDocumentActions\` to be a boolean, but received ${getPrintableType( enabled, )}`, ) }, initialValue) return result } export const partialIndexingEnabledReducer = (opts: { config: PluginOptions initialValue: boolean }): boolean => { const {config, initialValue} = opts const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce((acc, {config: innerConfig}) => { const resolver = innerConfig.search?.unstable_partialIndexing?.enabled if (!resolver && typeof resolver !== 'boolean') return acc if (typeof resolver === 'boolean') return resolver throw new Error( `Expected \`search.unstable_partialIndexing.enabled\` to be a boolean, but received ${getPrintableType( resolver, )}`, ) }, initialValue) return result } export const legacySearchEnabledReducer: ConfigPropertyReducer<boolean, ConfigContext> = ( prev, {search}, ): boolean => { if (typeof search?.enableLegacySearch !== 'undefined') { return search.enableLegacySearch } return prev } /** * Some projects may already be using the `enableLegacySearch` option. In order to gracefully * migrate to the `strategy` option, this reducer produces a value that respects any existing * `enableLegacySearch` option. * * If the project currently enables the Text Search API search strategy by setting * `enableLegacySearch` to `false`, this is mapped to the `groq2024` strategy. * * Any explicitly defined `strategy` value will take precedence over the value inferred from * `enableLegacySearch`. */ export const searchStrategyReducer = ({ config, initialValue, }: { config: PluginOptions initialValue: SearchStrategy }): SearchStrategy => { const flattenedConfig = flattenConfig(config, []) type SearchStrategyReducerState = [ implicit: SearchStrategy | undefined, explicit: SearchStrategy | undefined, ] const [implicit, explicit] = flattenedConfig.reduce<SearchStrategyReducerState>( ([currentImplicit, currentExplicit], entry) => { const {enableLegacySearch, strategy} = entry.config.search ?? {} // The strategy has been explicitly defined. if (typeof strategy !== 'undefined') { if (!isSearchStrategy(strategy)) { const listFormatter = new Intl.ListFormat('en-US', {type: 'disjunction'}) const options = listFormatter.format(searchStrategies.map((value) => `"${value}"`)) const received = typeof strategy === 'string' ? `"${strategy}"` : getPrintableType(strategy) throw new Error(`Expected \`search.strategy\` to be ${options}, but received ${received}`) } return [currentImplicit, strategy] } // The strategy has been implicitly defined. if (typeof enableLegacySearch === 'boolean') { return [enableLegacySearch ? 'groqLegacy' : 'groq2024', currentExplicit] } return [currentImplicit, currentExplicit] }, [undefined, undefined], ) return explicit ?? implicit ?? initialValue } export const startInCreateEnabledReducer = (opts: { config: PluginOptions initialValue: boolean }): boolean => { const {config, initialValue} = opts const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce((acc, {config: innerConfig}) => { const resolver = innerConfig.beta?.create?.startInCreateEnabled if (!resolver && typeof resolver !== 'boolean') return acc if (typeof resolver === 'boolean') return resolver throw new Error( `Expected \`beta.create.startInCreateEnabled\` to be a boolean, but received ${getPrintableType( resolver, )}`, ) }, initialValue) return result } export const createFallbackOriginReducer = (config: PluginOptions): string | undefined => { const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce( (acc, {config: innerConfig}) => { const resolver = innerConfig.beta?.create?.fallbackStudioOrigin if (!resolver) return acc if (typeof resolver === 'string') return resolver throw new Error( `Expected \`beta.create.fallbackStudioOrigin\` to be a string, but received ${getPrintableType( resolver, )}`, ) }, undefined as string | undefined, ) return result } export const announcementsEnabledReducer = (opts: { config: PluginOptions initialValue: boolean }): boolean => { const {config, initialValue} = opts const flattenedConfig = flattenConfig(config, []) const result = flattenedConfig.reduce((acc, {config: innerConfig}) => { const resolver = innerConfig.announcements?.enabled if (!resolver && typeof resolver !== 'boolean') return acc if (typeof resolver === 'boolean') return resolver throw new Error( `Expected \`announcements.enabled\` to be a boolean, but received ${getPrintableType( resolver, )}`, ) }, initialValue) return result }