@jhb.software/payload-alt-text-plugin 0.9.0 → 0.10.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/README.md +128 -19
- package/dist/endpoints/bulkGenerateAltTexts.js +13 -4
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +13 -4
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/exports/server.d.ts +6 -0
- package/dist/exports/server.js +6 -1
- package/dist/exports/server.js.map +1 -1
- package/dist/hooks/revalidateAltTextHealth.js +31 -17
- package/dist/hooks/revalidateAltTextHealth.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +24 -3
- package/dist/plugin.js.map +1 -1
- package/dist/resolvers/mistral.d.ts +40 -0
- package/dist/resolvers/mistral.js +280 -0
- package/dist/resolvers/mistral.js.map +1 -0
- package/dist/resolvers/openAI.d.ts +10 -0
- package/dist/resolvers/openAI.js +7 -7
- package/dist/resolvers/openAI.js.map +1 -1
- package/dist/resolvers/types.d.ts +24 -1
- package/dist/resolvers/types.js.map +1 -1
- package/dist/translations/index.js.map +1 -1
- package/dist/types/AltTextPluginConfig.d.ts +40 -8
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/altTextHealth.js.map +1 -1
- package/dist/utilities/mimeTypes.d.ts +54 -1
- package/dist/utilities/mimeTypes.js +41 -2
- package/dist/utilities/mimeTypes.js.map +1 -1
- package/package.json +14 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/resolvers/types.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\n/**\n * Result of generating alt text for a single image.\n */\nexport type AltTextResult = {\n /** Concise descriptive alt text (1-2 sentences) */\n altText: string\n /** Keywords describing the image content */\n keywords: string[]\n}\n\n/**\n * Arguments passed to the resolver for single image generation.\n */\nexport type AltTextResolverArgs = {\n /** Optional filename for additional context */\n filename?: string\n /** URL of the image thumbnail (must be publicly accessible) */\n imageThumbnailUrl: string\n /** Target locale for the generated alt text */\n locale: string\n /** Payload request object for logging */\n req: PayloadRequest\n}\n\n/**\n * Arguments passed to the resolver for bulk/multi-locale generation.\n */\nexport type AltTextBulkResolverArgs = {\n /** Optional filename for additional context */\n filename?: string\n /** URL of the image thumbnail (must be publicly accessible) */\n imageThumbnailUrl: string\n /** Target locales for the generated alt texts */\n locales: string[]\n /** Payload request object for logging */\n req: PayloadRequest\n}\n\n/**\n * Response from single image alt text generation.\n */\nexport type AltTextResolverResponse =\n
|
|
1
|
+
{"version":3,"sources":["../../src/resolvers/types.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\n/**\n * Result of generating alt text for a single image.\n */\nexport type AltTextResult = {\n /** Concise descriptive alt text (1-2 sentences) */\n altText: string\n /** Keywords describing the image content */\n keywords: string[]\n}\n\n/**\n * Arguments passed to the resolver for single image generation.\n */\nexport type AltTextResolverArgs = {\n /** Optional filename for additional context */\n filename?: string\n /**\n * The format served at `imageThumbnailUrl`, when the collection declares one\n * via the plugin's `imageThumbnailMimeType` option. Undefined otherwise.\n *\n * Resolvers that hand the URL to the provider can ignore this. Resolvers that\n * inline the bytes need it — Anthropic image blocks require `media_type`,\n * Gemini's `inline_data` requires `mime_type`, and neither can be sniffed from\n * a URL.\n */\n imageThumbnailMimeType?: string\n /** URL of the image thumbnail (must be publicly accessible) */\n imageThumbnailUrl: string\n /** Target locale for the generated alt text */\n locale: string\n /** Payload request object for logging */\n req: PayloadRequest\n}\n\n/**\n * Arguments passed to the resolver for bulk/multi-locale generation.\n */\nexport type AltTextBulkResolverArgs = {\n /** Optional filename for additional context */\n filename?: string\n /**\n * The format served at `imageThumbnailUrl`, when the collection declares one\n * via `imageThumbnailMimeType`. See {@link AltTextResolverArgs.imageThumbnailMimeType}.\n */\n imageThumbnailMimeType?: string\n /** URL of the image thumbnail (must be publicly accessible) */\n imageThumbnailUrl: string\n /** Target locales for the generated alt texts */\n locales: string[]\n /** Payload request object for logging */\n req: PayloadRequest\n}\n\n/**\n * Response from single image alt text generation.\n */\nexport type AltTextResolverResponse =\n { error?: string; success: false } | { result: AltTextResult; success: true }\n\n/**\n * Response from bulk/multi-locale alt text generation.\n */\nexport type AltTextBulkResolverResponse =\n { error?: string; success: false } | { results: Record<string, AltTextResult>; success: true }\n\n/**\n * Alt text resolver interface.\n * Implement this to create custom resolvers for different providers.\n */\nexport type AltTextResolver = {\n /** Unique key identifying this resolver */\n key: string\n /** Generate alt text for a single image in one locale */\n resolve: (args: AltTextResolverArgs) => Promise<AltTextResolverResponse>\n /** Generate alt text for a single image in multiple locales (bulk operation) */\n resolveBulk: (args: AltTextBulkResolverArgs) => Promise<AltTextBulkResolverResponse>\n /**\n * Formats the provider accepts for the bytes served at `imageThumbnailUrl`.\n *\n * When set, the endpoints reject documents whose stored `mimeType` is not in\n * this list — a conservative proxy, since the resolver never sees the stored\n * file. A project whose `getImageThumbnail` transcodes should declare the\n * delivered format via the plugin's `imageThumbnailMimeType` option, which\n * replaces the proxy with a one-time check against this list at config load.\n */\n supportedMimeTypes?: string[]\n}\n"],"names":[],"mappings":"AAmEA;;;CAGC,GACD,WAiBC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import { de } from './de.js'\nimport { en } from './en.js'\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type GenericTranslationsObject = {\n [key: string]: GenericTranslationsObject | string\n}\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type NestedKeysStripped<T> = T extends object\n ? {\n [K in keyof T]-?: K extends string\n ? T[K] extends object\n ? `${K}:${NestedKeysStripped<T[K]>}`\n : `${StripCountVariants<K>}`\n : never\n }[keyof T]\n : ''\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type StripCountVariants<TKey> = TKey extends\n
|
|
1
|
+
{"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import { de } from './de.js'\nimport { en } from './en.js'\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type GenericTranslationsObject = {\n [key: string]: GenericTranslationsObject | string\n}\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type NestedKeysStripped<T> = T extends object\n ? {\n [K in keyof T]-?: K extends string\n ? T[K] extends object\n ? `${K}:${NestedKeysStripped<T[K]>}`\n : `${StripCountVariants<K>}`\n : never\n }[keyof T]\n : ''\n\n// copied from https://github.com/payloadcms/payload/blob/main/packages/translations/src/types.ts\nexport type StripCountVariants<TKey> = TKey extends\n `${infer Base}_many` | `${infer Base}_one` | `${infer Base}_other`\n ? Base\n : TKey\n\nexport const translations = {\n de,\n en,\n}\n\nexport type PluginAltTextTranslations = GenericTranslationsObject\n\nexport type PluginAltTextTranslationKeys = NestedKeysStripped<PluginAltTextTranslations>\n"],"names":["de","en","translations"],"mappings":"AAAA,SAASA,EAAE,QAAQ,UAAS;AAC5B,SAASC,EAAE,QAAQ,UAAS;AAwB5B,OAAO,MAAMC,eAAe;IAC1BF;IACAC;AACF,EAAC"}
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import type { Field, PayloadRequest } from 'payload';
|
|
1
|
+
import type { CollectionSlug, Field, PayloadRequest } from 'payload';
|
|
2
2
|
import type { AltTextResolver } from '../resolvers/types.js';
|
|
3
3
|
import type { AltTextCollectionConfig, IncomingCollectionsConfig, NormalizedAltTextCollectionConfig } from '../utilities/mimeTypes.js';
|
|
4
4
|
export type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig };
|
|
5
|
+
/**
|
|
6
|
+
* Builds the thumbnail URL the resolver fetches. Must be publicly reachable.
|
|
7
|
+
*
|
|
8
|
+
* May be async, so the URL can be signed on demand (S3 presigning, short-lived
|
|
9
|
+
* CDN tokens).
|
|
10
|
+
*
|
|
11
|
+
* @param doc The upload document to build the URL for.
|
|
12
|
+
* @param args.collection The slug of the collection `doc` belongs to — use it to
|
|
13
|
+
* build different URLs per collection (e.g. a Cloudinary transformation for one
|
|
14
|
+
* collection and a plain S3 URL for another).
|
|
15
|
+
* @param args.req The request the generation runs under.
|
|
16
|
+
*/
|
|
17
|
+
export type GetImageThumbnail = (doc: Record<string, unknown>, args: {
|
|
18
|
+
collection: CollectionSlug;
|
|
19
|
+
req: PayloadRequest;
|
|
20
|
+
}) => Promise<string> | string;
|
|
5
21
|
/** Configuration options for the alt text plugin. */
|
|
6
22
|
export type IncomingAltTextPluginConfig = {
|
|
7
23
|
/**
|
|
@@ -36,14 +52,15 @@ export type IncomingAltTextPluginConfig = {
|
|
|
36
52
|
defaultFields: Field[];
|
|
37
53
|
}) => Field[];
|
|
38
54
|
/**
|
|
39
|
-
*
|
|
40
|
-
* This URL will be sent to the LLM for analysis.
|
|
55
|
+
* Builds the image URL sent to the resolver. See {@link GetImageThumbnail}.
|
|
41
56
|
*
|
|
42
57
|
* @remarks
|
|
43
|
-
* -
|
|
44
|
-
* -
|
|
58
|
+
* - Prefer a thumbnail/preview size over the original (e.g. from the sizes field)
|
|
59
|
+
* - When the URL transcodes, declare the delivered format via
|
|
60
|
+
* `imageThumbnailMimeType` so source formats the resolver does not accept are
|
|
61
|
+
* not rejected
|
|
45
62
|
*/
|
|
46
|
-
getImageThumbnail:
|
|
63
|
+
getImageThumbnail: GetImageThumbnail;
|
|
47
64
|
/**
|
|
48
65
|
* Controls the alt text health feature (REST endpoint, cache revalidation hooks, and dashboard widget).
|
|
49
66
|
*
|
|
@@ -61,6 +78,17 @@ export type IncomingAltTextPluginConfig = {
|
|
|
61
78
|
healthCheck?: ((args: {
|
|
62
79
|
req: PayloadRequest;
|
|
63
80
|
}) => boolean | Promise<boolean>) | boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The MIME type `getImageThumbnail` delivers, for every configured collection.
|
|
83
|
+
*
|
|
84
|
+
* Declaring it takes a document's stored `mimeType` out of the decision of
|
|
85
|
+
* whether alt text can be generated. Collections may override it or opt out with
|
|
86
|
+
* `null` — see {@link AltTextCollectionConfig.imageThumbnailMimeType} for the
|
|
87
|
+
* rationale and the `f_auto` caveat.
|
|
88
|
+
*
|
|
89
|
+
* @example 'image/webp'
|
|
90
|
+
*/
|
|
91
|
+
imageThumbnailMimeType?: string;
|
|
64
92
|
/**
|
|
65
93
|
* The locale to generate alt texts in when localization is disabled.
|
|
66
94
|
*
|
|
@@ -93,7 +121,11 @@ export type AltTextPluginConfig = {
|
|
|
93
121
|
access: (args: {
|
|
94
122
|
req: PayloadRequest;
|
|
95
123
|
}) => boolean | Promise<boolean>;
|
|
96
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Collections with resolved MIME type filters and resolved delivered thumbnail
|
|
126
|
+
* MIME types. The plugin-level `imageThumbnailMimeType` is folded into these
|
|
127
|
+
* entries during normalization, so this is the only place to read it from.
|
|
128
|
+
*/
|
|
97
129
|
collections: NormalizedAltTextCollectionConfig[];
|
|
98
130
|
/** Whether the plugin is enabled. */
|
|
99
131
|
enabled: boolean;
|
|
@@ -102,7 +134,7 @@ export type AltTextPluginConfig = {
|
|
|
102
134
|
defaultFields: Field[];
|
|
103
135
|
}) => Field[];
|
|
104
136
|
/** Function to get the thumbnail URL of an image document. */
|
|
105
|
-
getImageThumbnail:
|
|
137
|
+
getImageThumbnail: GetImageThumbnail;
|
|
106
138
|
/** Whether alt text health tracking is enabled. */
|
|
107
139
|
healthCheck: boolean;
|
|
108
140
|
/** Access control for the health endpoint. Defaults to `access`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import type { Field, PayloadRequest } from 'payload'\n\nimport type { AltTextResolver } from '../resolvers/types.js'\nimport type {\n AltTextCollectionConfig,\n IncomingCollectionsConfig,\n NormalizedAltTextCollectionConfig,\n} from '../utilities/mimeTypes.js'\n\nexport type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig }\n\n/** Configuration options for the alt text plugin. */\nexport type IncomingAltTextPluginConfig = {\n /**\n * Custom access control for plugin endpoints.\n * Return `true` to allow access, `false` to deny.\n *\n * @default ({ req }) => !!req.user — requires authentication\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections to enable the plugin for.\n *\n * Each entry may be a bare collection slug or an object with a `slug` and an\n * optional `mimeTypes` array restricting which MIME types are tracked,\n * validated, and generated. Bare slugs default to `['image/*']`.\n *\n * @example\n * ```typescript\n * collections: [\n * 'images', // shorthand — defaults to ['image/*']\n * { slug: 'media', mimeTypes: ['image/*', 'application/pdf'] },\n * ]\n * ```\n */\n collections: IncomingCollectionsConfig\n\n /** Whether the plugin is enabled. */\n enabled?: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /**\n *
|
|
1
|
+
{"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import type { CollectionSlug, Field, PayloadRequest } from 'payload'\n\nimport type { AltTextResolver } from '../resolvers/types.js'\nimport type {\n AltTextCollectionConfig,\n IncomingCollectionsConfig,\n NormalizedAltTextCollectionConfig,\n} from '../utilities/mimeTypes.js'\n\nexport type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig }\n\n/**\n * Builds the thumbnail URL the resolver fetches. Must be publicly reachable.\n *\n * May be async, so the URL can be signed on demand (S3 presigning, short-lived\n * CDN tokens).\n *\n * @param doc The upload document to build the URL for.\n * @param args.collection The slug of the collection `doc` belongs to — use it to\n * build different URLs per collection (e.g. a Cloudinary transformation for one\n * collection and a plain S3 URL for another).\n * @param args.req The request the generation runs under.\n */\nexport type GetImageThumbnail = (\n doc: Record<string, unknown>,\n args: { collection: CollectionSlug; req: PayloadRequest },\n) => Promise<string> | string\n\n/** Configuration options for the alt text plugin. */\nexport type IncomingAltTextPluginConfig = {\n /**\n * Custom access control for plugin endpoints.\n * Return `true` to allow access, `false` to deny.\n *\n * @default ({ req }) => !!req.user — requires authentication\n */\n access?: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections to enable the plugin for.\n *\n * Each entry may be a bare collection slug or an object with a `slug` and an\n * optional `mimeTypes` array restricting which MIME types are tracked,\n * validated, and generated. Bare slugs default to `['image/*']`.\n *\n * @example\n * ```typescript\n * collections: [\n * 'images', // shorthand — defaults to ['image/*']\n * { slug: 'media', mimeTypes: ['image/*', 'application/pdf'] },\n * ]\n * ```\n */\n collections: IncomingCollectionsConfig\n\n /** Whether the plugin is enabled. */\n enabled?: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /**\n * Builds the image URL sent to the resolver. See {@link GetImageThumbnail}.\n *\n * @remarks\n * - Prefer a thumbnail/preview size over the original (e.g. from the sizes field)\n * - When the URL transcodes, declare the delivered format via\n * `imageThumbnailMimeType` so source formats the resolver does not accept are\n * not rejected\n */\n getImageThumbnail: GetImageThumbnail\n\n /**\n * Controls the alt text health feature (REST endpoint, cache revalidation hooks, and dashboard widget).\n *\n * - `false` disables the entire feature.\n * - `true` enables it, gated by `access`.\n * - A function enables it and gates both the endpoint and the dashboard widget\n * with that access check — use this to restrict the collection-wide report\n * more strictly than the per-document generate endpoints (e.g. to admins).\n *\n * Regardless of the gate, the report is always filtered to the collections the\n * requesting user can read.\n *\n * @default true\n */\n healthCheck?: ((args: { req: PayloadRequest }) => boolean | Promise<boolean>) | boolean\n\n /**\n * The MIME type `getImageThumbnail` delivers, for every configured collection.\n *\n * Declaring it takes a document's stored `mimeType` out of the decision of\n * whether alt text can be generated. Collections may override it or opt out with\n * `null` — see {@link AltTextCollectionConfig.imageThumbnailMimeType} for the\n * rationale and the `f_auto` caveat.\n *\n * @example 'image/webp'\n */\n imageThumbnailMimeType?: string\n\n /**\n * The locale to generate alt texts in when localization is disabled.\n *\n * Required when localization is disabled, ignored when localization is enabled.\n * @example 'en'\n */\n locale?: string\n\n /**\n * Maximum number of concurrent API requests for bulk generate operations.\n *\n * @default 16\n */\n maxBulkGenerateConcurrency?: number\n\n /**\n * Maximum number of image IDs accepted in a single bulk generate request.\n * Requests exceeding this are rejected with `400`. Duplicate IDs are collapsed\n * before the limit is applied, so each image counts once.\n *\n * Raise it for large libraries that need to process more images per request.\n *\n * @default 100\n */\n maxBulkGenerateIds?: number\n\n /** The resolver to use for generating alt text (e.g., openAIResolver) */\n resolver: AltTextResolver\n}\n\n/** Configuration of the alt text plugin after defaults have been applied. */\nexport type AltTextPluginConfig = {\n /** Access control for plugin endpoints. */\n access: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /**\n * Collections with resolved MIME type filters and resolved delivered thumbnail\n * MIME types. The plugin-level `imageThumbnailMimeType` is folded into these\n * entries during normalization, so this is the only place to read it from.\n */\n collections: NormalizedAltTextCollectionConfig[]\n\n /** Whether the plugin is enabled. */\n enabled: boolean\n\n /** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */\n fieldsOverride?: (args: { defaultFields: Field[] }) => Field[]\n\n /** Function to get the thumbnail URL of an image document. */\n getImageThumbnail: GetImageThumbnail\n\n /** Whether alt text health tracking is enabled. */\n healthCheck: boolean\n\n /** Access control for the health endpoint. Defaults to `access`. */\n healthCheckAccess: (args: { req: PayloadRequest }) => boolean | Promise<boolean>\n\n /** The locale to generate alt texts in when localization is disabled. */\n locale?: string\n\n /** The locales to generate alt texts for. */\n locales: string[]\n\n /** Maximum number of concurrent API requests for bulk generate operations. */\n maxBulkGenerateConcurrency: number\n\n /** Maximum number of image IDs accepted per bulk generate request. */\n maxBulkGenerateIds: number\n\n /** The resolver to use for generating alt text */\n resolver: AltTextResolver\n}\n"],"names":[],"mappings":"AAkIA,2EAA2E,GAC3E,WAwCC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/altTextHealth.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport { unstable_cache } from 'next/cache.js'\n\nimport type {\n AltTextPluginConfig,\n NormalizedAltTextCollectionConfig,\n} from '../types/AltTextPluginConfig.js'\n\nimport { createCachedAltTextHealthScan } from './altTextHealthCache.js'\nimport { localesFromConfig } from './localesFromConfig.js'\nimport { buildMimeTypeWhere } from './mimeTypes.js'\nimport { summarizeCollection } from './summarizeCollection.js'\n\nexport const ALT_TEXT_HEALTH_PLUGIN_SLUG = 'alt-text'\nexport const ALT_TEXT_HEALTH_CACHE_TTL = 3600\nexport const ALT_TEXT_HEALTH_GLOBAL_TAG = 'alt-text-health'\n\nexport type AltTextHealthErrorCode =\n | 'ALT_TEXT_COLLECTION_READ_FAILED'\n | 'ALT_TEXT_PLUGIN_CONFIG_MISSING'\n\nexport type AltTextHealthError = {\n code: AltTextHealthErrorCode\n collection?: string\n message: string\n operation?: 'find'\n}\n\nexport type AltTextHealthScanCollection = {\n collection: string\n completeDocs: number\n error?: AltTextHealthError\n invalidDocIds: (number | string)[] | undefined\n missingDocs: number\n partialDocs: number\n totalDocs: number\n}\n\nexport type AltTextHealthScan = {\n checkedAt: string\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCodes: string[]\n}\n\nexport type AltTextHealthWidgetData = {\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCount: number\n totalDocs: number\n}\n\ntype AltTextHealthComputationArgs = {\n collections: NormalizedAltTextCollectionConfig[]\n isLocalized: boolean\n localeCodes: string[]\n payload: Payload\n}\n\nconst createUnknownScan = ({\n error,\n isLocalized,\n localeCodes,\n}: {\n error: AltTextHealthScan['errors'][number]\n isLocalized: boolean\n localeCodes: string[]\n}): AltTextHealthScan => ({\n checkedAt: new Date().toISOString(),\n collections: [],\n errors: [error],\n isLocalized,\n localeCodes,\n})\n\nconst createCollectionReadError = (collection: string, message: string) => ({\n code: 'ALT_TEXT_COLLECTION_READ_FAILED' as const,\n collection,\n message,\n operation: 'find' as const,\n})\n\nconst PAGE_SIZE = 500\n\nasync function fetchAllDocs(\n payload: Payload,\n collection: string,\n isLocalized: boolean,\n mimeTypes: readonly string[],\n): Promise<{ alt: unknown; id: number | string }[]> {\n const where = buildMimeTypeWhere(mimeTypes)\n if (!where) {\n return []\n }\n\n const docs: { alt: unknown; id: number | string }[] = []\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await payload.find({\n collection,\n depth: 0,\n fallbackLocale: isLocalized ? false : undefined,\n limit: PAGE_SIZE,\n locale: isLocalized ? 'all' : undefined,\n overrideAccess: true,\n page,\n select: {\n alt: true,\n },\n where,\n })\n\n for (const doc of result.docs) {\n docs.push({\n id: doc.id,\n alt: 'alt' in doc ? doc.alt : undefined,\n })\n }\n\n hasMore = result.hasNextPage\n page++\n }\n\n return docs\n}\n\nasync function computeAltTextHealthScan({\n collections,\n isLocalized,\n localeCodes,\n payload,\n}: AltTextHealthComputationArgs): Promise<AltTextHealthScan> {\n const collectionSummaries = await Promise.all(\n collections.map(async ({ slug, mimeTypes }): Promise<AltTextHealthScanCollection> => {\n try {\n const docs = await fetchAllDocs(payload, slug, isLocalized, mimeTypes)\n\n return summarizeCollection({\n collection: slug,\n docs,\n isLocalized,\n localeCodes,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n const collectionError = createCollectionReadError(slug, message)\n\n payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while reading a collection.',\n operation: 'find',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n collection: slug,\n completeDocs: 0,\n error: collectionError,\n invalidDocIds: undefined,\n missingDocs: 0,\n partialDocs: 0,\n totalDocs: 0,\n }\n }\n }),\n )\n\n const errors = collectionSummaries\n .filter((summary) => summary.error)\n .map((summary) => summary.error!)\n\n return {\n checkedAt: new Date().toISOString(),\n collections: collectionSummaries,\n errors,\n isLocalized,\n localeCodes,\n }\n}\n\nexport const getAltTextHealthCollectionTag = (collectionSlug: string): string =>\n `${ALT_TEXT_HEALTH_GLOBAL_TAG}:${collectionSlug}`\n\nasync function getAltTextHealthScan(req: PayloadRequest): Promise<AltTextHealthScan> {\n const { payload } = req\n const pluginConfig = payload.config.custom?.altTextPluginConfig as AltTextPluginConfig | undefined\n const localeCodes =\n localesFromConfig(payload.config) ?? (pluginConfig?.locale ? [pluginConfig.locale] : [])\n const isLocalized = Boolean(payload.config.localization)\n\n if (!pluginConfig) {\n return createUnknownScan({\n error: {\n code: 'ALT_TEXT_PLUGIN_CONFIG_MISSING',\n message: 'Alt text plugin config not found',\n },\n isLocalized,\n localeCodes,\n })\n }\n\n const collections = pluginConfig.collections\n\n const cacheKeyParts = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n [...collections]\n .map(({ slug, mimeTypes }) => `${slug}:${[...mimeTypes].sort().join('|')}`)\n .sort()\n .join(','),\n localeCodes.join(','),\n ]\n\n const tags = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n ...new Set(collections.map(({ slug }) => getAltTextHealthCollectionTag(slug))),\n ]\n\n const getCachedHealthScan = createCachedAltTextHealthScan({\n cacheFactory: unstable_cache,\n cacheKeyParts,\n compute: async () =>\n computeAltTextHealthScan({\n collections,\n isLocalized,\n localeCodes,\n payload,\n }),\n revalidate: ALT_TEXT_HEALTH_CACHE_TTL,\n tags,\n })\n\n return getCachedHealthScan()\n}\n\n/**\n * Whether `req.user` is allowed to read the given collection at the collection\n * level. The collection's `read` access is evaluated with the request; `false`\n * denies, while `true` or a scoped `Where` constraint grants visibility of the\n * collection's health aggregate. A thrown access function (e.g. `Forbidden`)\n * counts as denied so a restricted collection never leaks.\n */\nasync function userCanReadCollection(req: PayloadRequest, slug: string): Promise<boolean> {\n const readAccess = req.payload.collections?.[slug]?.config.access?.read\n\n if (typeof readAccess !== 'function') {\n return true\n }\n\n try {\n return (await readAccess({ req })) !== false\n } catch {\n return false\n }\n}\n\n/**\n * Filters a shared, elevated-access health scan down to the collections the\n * requesting user may read. The scan is computed once with `overrideAccess: true`\n * so it stays complete and cacheable; access is applied per request at\n * collection granularity, matching the aggregate's altitude.\n */\nexport async function filterScanByReadAccess(\n req: PayloadRequest,\n scan: AltTextHealthScan,\n): Promise<AltTextHealthScan> {\n const visibility = await Promise.all(\n scan.collections.map((collection) => userCanReadCollection(req, collection.collection)),\n )\n\n const collections = scan.collections.filter((_, index) => visibility[index])\n const allowedSlugs = new Set(collections.map((collection) => collection.collection))\n const errors = scan.errors.filter(\n (error) => !error.collection || allowedSlugs.has(error.collection),\n )\n\n return { ...scan, collections, errors }\n}\n\n/**\n * Whether the requesting user may view the health report at all, per the\n * configured `healthCheck` access gate. The dashboard widget uses this to hide\n * itself, mirroring the gate enforced on the health endpoint.\n */\nexport async function canViewHealthReport(req: PayloadRequest): Promise<boolean> {\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return false\n }\n\n return pluginConfig.healthCheckAccess({ req })\n}\n\nexport function toWidgetData(scan: AltTextHealthScan): AltTextHealthWidgetData {\n return {\n collections: scan.collections,\n errors: scan.errors,\n isLocalized: scan.isLocalized,\n localeCount: scan.localeCodes.length,\n totalDocs: scan.collections.reduce((total, c) => total + c.totalDocs, 0),\n }\n}\n\nexport async function getAltTextHealth(req: PayloadRequest): Promise<AltTextHealthScan> {\n return filterScanByReadAccess(req, await getAltTextHealthScan(req))\n}\n\nexport async function getAltTextHealthWidgetData(\n req: PayloadRequest,\n): Promise<AltTextHealthWidgetData> {\n return toWidgetData(await filterScanByReadAccess(req, await getAltTextHealthScan(req)))\n}\n"],"names":["unstable_cache","createCachedAltTextHealthScan","localesFromConfig","buildMimeTypeWhere","summarizeCollection","ALT_TEXT_HEALTH_PLUGIN_SLUG","ALT_TEXT_HEALTH_CACHE_TTL","ALT_TEXT_HEALTH_GLOBAL_TAG","createUnknownScan","error","isLocalized","localeCodes","checkedAt","Date","toISOString","collections","errors","createCollectionReadError","collection","message","code","operation","PAGE_SIZE","fetchAllDocs","payload","mimeTypes","where","docs","page","hasMore","result","find","depth","fallbackLocale","undefined","limit","locale","overrideAccess","select","alt","doc","push","id","hasNextPage","computeAltTextHealthScan","collectionSummaries","Promise","all","map","slug","Error","collectionError","logger","err","msg","plugin","completeDocs","invalidDocIds","missingDocs","partialDocs","totalDocs","filter","summary","getAltTextHealthCollectionTag","collectionSlug","getAltTextHealthScan","req","pluginConfig","config","custom","altTextPluginConfig","Boolean","localization","cacheKeyParts","sort","join","tags","Set","getCachedHealthScan","cacheFactory","compute","revalidate","userCanReadCollection","readAccess","access","read","filterScanByReadAccess","scan","visibility","_","index","allowedSlugs","has","canViewHealthReport","healthCheckAccess","toWidgetData","localeCount","length","reduce","total","c","getAltTextHealth","getAltTextHealthWidgetData"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/altTextHealth.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport { unstable_cache } from 'next/cache.js'\n\nimport type {\n AltTextPluginConfig,\n NormalizedAltTextCollectionConfig,\n} from '../types/AltTextPluginConfig.js'\n\nimport { createCachedAltTextHealthScan } from './altTextHealthCache.js'\nimport { localesFromConfig } from './localesFromConfig.js'\nimport { buildMimeTypeWhere } from './mimeTypes.js'\nimport { summarizeCollection } from './summarizeCollection.js'\n\nexport const ALT_TEXT_HEALTH_PLUGIN_SLUG = 'alt-text'\nexport const ALT_TEXT_HEALTH_CACHE_TTL = 3600\nexport const ALT_TEXT_HEALTH_GLOBAL_TAG = 'alt-text-health'\n\nexport type AltTextHealthErrorCode =\n 'ALT_TEXT_COLLECTION_READ_FAILED' | 'ALT_TEXT_PLUGIN_CONFIG_MISSING'\n\nexport type AltTextHealthError = {\n code: AltTextHealthErrorCode\n collection?: string\n message: string\n operation?: 'find'\n}\n\nexport type AltTextHealthScanCollection = {\n collection: string\n completeDocs: number\n error?: AltTextHealthError\n invalidDocIds: (number | string)[] | undefined\n missingDocs: number\n partialDocs: number\n totalDocs: number\n}\n\nexport type AltTextHealthScan = {\n checkedAt: string\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCodes: string[]\n}\n\nexport type AltTextHealthWidgetData = {\n collections: AltTextHealthScanCollection[]\n errors: AltTextHealthError[]\n isLocalized: boolean\n localeCount: number\n totalDocs: number\n}\n\ntype AltTextHealthComputationArgs = {\n collections: NormalizedAltTextCollectionConfig[]\n isLocalized: boolean\n localeCodes: string[]\n payload: Payload\n}\n\nconst createUnknownScan = ({\n error,\n isLocalized,\n localeCodes,\n}: {\n error: AltTextHealthScan['errors'][number]\n isLocalized: boolean\n localeCodes: string[]\n}): AltTextHealthScan => ({\n checkedAt: new Date().toISOString(),\n collections: [],\n errors: [error],\n isLocalized,\n localeCodes,\n})\n\nconst createCollectionReadError = (collection: string, message: string) => ({\n code: 'ALT_TEXT_COLLECTION_READ_FAILED' as const,\n collection,\n message,\n operation: 'find' as const,\n})\n\nconst PAGE_SIZE = 500\n\nasync function fetchAllDocs(\n payload: Payload,\n collection: string,\n isLocalized: boolean,\n mimeTypes: readonly string[],\n): Promise<{ alt: unknown; id: number | string }[]> {\n const where = buildMimeTypeWhere(mimeTypes)\n if (!where) {\n return []\n }\n\n const docs: { alt: unknown; id: number | string }[] = []\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await payload.find({\n collection,\n depth: 0,\n fallbackLocale: isLocalized ? false : undefined,\n limit: PAGE_SIZE,\n locale: isLocalized ? 'all' : undefined,\n overrideAccess: true,\n page,\n select: {\n alt: true,\n },\n where,\n })\n\n for (const doc of result.docs) {\n docs.push({\n id: doc.id,\n alt: 'alt' in doc ? doc.alt : undefined,\n })\n }\n\n hasMore = result.hasNextPage\n page++\n }\n\n return docs\n}\n\nasync function computeAltTextHealthScan({\n collections,\n isLocalized,\n localeCodes,\n payload,\n}: AltTextHealthComputationArgs): Promise<AltTextHealthScan> {\n const collectionSummaries = await Promise.all(\n collections.map(async ({ slug, mimeTypes }): Promise<AltTextHealthScanCollection> => {\n try {\n const docs = await fetchAllDocs(payload, slug, isLocalized, mimeTypes)\n\n return summarizeCollection({\n collection: slug,\n docs,\n isLocalized,\n localeCodes,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n const collectionError = createCollectionReadError(slug, message)\n\n payload.logger.error({\n collection: slug,\n err: error,\n msg: 'Alt text health check failed while reading a collection.',\n operation: 'find',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return {\n collection: slug,\n completeDocs: 0,\n error: collectionError,\n invalidDocIds: undefined,\n missingDocs: 0,\n partialDocs: 0,\n totalDocs: 0,\n }\n }\n }),\n )\n\n const errors = collectionSummaries\n .filter((summary) => summary.error)\n .map((summary) => summary.error!)\n\n return {\n checkedAt: new Date().toISOString(),\n collections: collectionSummaries,\n errors,\n isLocalized,\n localeCodes,\n }\n}\n\nexport const getAltTextHealthCollectionTag = (collectionSlug: string): string =>\n `${ALT_TEXT_HEALTH_GLOBAL_TAG}:${collectionSlug}`\n\nasync function getAltTextHealthScan(req: PayloadRequest): Promise<AltTextHealthScan> {\n const { payload } = req\n const pluginConfig = payload.config.custom?.altTextPluginConfig as AltTextPluginConfig | undefined\n const localeCodes =\n localesFromConfig(payload.config) ?? (pluginConfig?.locale ? [pluginConfig.locale] : [])\n const isLocalized = Boolean(payload.config.localization)\n\n if (!pluginConfig) {\n return createUnknownScan({\n error: {\n code: 'ALT_TEXT_PLUGIN_CONFIG_MISSING',\n message: 'Alt text plugin config not found',\n },\n isLocalized,\n localeCodes,\n })\n }\n\n const collections = pluginConfig.collections\n\n const cacheKeyParts = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n [...collections]\n .map(({ slug, mimeTypes }) => `${slug}:${[...mimeTypes].sort().join('|')}`)\n .sort()\n .join(','),\n localeCodes.join(','),\n ]\n\n const tags = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n ...new Set(collections.map(({ slug }) => getAltTextHealthCollectionTag(slug))),\n ]\n\n const getCachedHealthScan = createCachedAltTextHealthScan({\n cacheFactory: unstable_cache,\n cacheKeyParts,\n compute: async () =>\n computeAltTextHealthScan({\n collections,\n isLocalized,\n localeCodes,\n payload,\n }),\n revalidate: ALT_TEXT_HEALTH_CACHE_TTL,\n tags,\n })\n\n return getCachedHealthScan()\n}\n\n/**\n * Whether `req.user` is allowed to read the given collection at the collection\n * level. The collection's `read` access is evaluated with the request; `false`\n * denies, while `true` or a scoped `Where` constraint grants visibility of the\n * collection's health aggregate. A thrown access function (e.g. `Forbidden`)\n * counts as denied so a restricted collection never leaks.\n */\nasync function userCanReadCollection(req: PayloadRequest, slug: string): Promise<boolean> {\n const readAccess = req.payload.collections?.[slug]?.config.access?.read\n\n if (typeof readAccess !== 'function') {\n return true\n }\n\n try {\n return (await readAccess({ req })) !== false\n } catch {\n return false\n }\n}\n\n/**\n * Filters a shared, elevated-access health scan down to the collections the\n * requesting user may read. The scan is computed once with `overrideAccess: true`\n * so it stays complete and cacheable; access is applied per request at\n * collection granularity, matching the aggregate's altitude.\n */\nexport async function filterScanByReadAccess(\n req: PayloadRequest,\n scan: AltTextHealthScan,\n): Promise<AltTextHealthScan> {\n const visibility = await Promise.all(\n scan.collections.map((collection) => userCanReadCollection(req, collection.collection)),\n )\n\n const collections = scan.collections.filter((_, index) => visibility[index])\n const allowedSlugs = new Set(collections.map((collection) => collection.collection))\n const errors = scan.errors.filter(\n (error) => !error.collection || allowedSlugs.has(error.collection),\n )\n\n return { ...scan, collections, errors }\n}\n\n/**\n * Whether the requesting user may view the health report at all, per the\n * configured `healthCheck` access gate. The dashboard widget uses this to hide\n * itself, mirroring the gate enforced on the health endpoint.\n */\nexport async function canViewHealthReport(req: PayloadRequest): Promise<boolean> {\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n AltTextPluginConfig | undefined\n\n if (!pluginConfig) {\n return false\n }\n\n return pluginConfig.healthCheckAccess({ req })\n}\n\nexport function toWidgetData(scan: AltTextHealthScan): AltTextHealthWidgetData {\n return {\n collections: scan.collections,\n errors: scan.errors,\n isLocalized: scan.isLocalized,\n localeCount: scan.localeCodes.length,\n totalDocs: scan.collections.reduce((total, c) => total + c.totalDocs, 0),\n }\n}\n\nexport async function getAltTextHealth(req: PayloadRequest): Promise<AltTextHealthScan> {\n return filterScanByReadAccess(req, await getAltTextHealthScan(req))\n}\n\nexport async function getAltTextHealthWidgetData(\n req: PayloadRequest,\n): Promise<AltTextHealthWidgetData> {\n return toWidgetData(await filterScanByReadAccess(req, await getAltTextHealthScan(req)))\n}\n"],"names":["unstable_cache","createCachedAltTextHealthScan","localesFromConfig","buildMimeTypeWhere","summarizeCollection","ALT_TEXT_HEALTH_PLUGIN_SLUG","ALT_TEXT_HEALTH_CACHE_TTL","ALT_TEXT_HEALTH_GLOBAL_TAG","createUnknownScan","error","isLocalized","localeCodes","checkedAt","Date","toISOString","collections","errors","createCollectionReadError","collection","message","code","operation","PAGE_SIZE","fetchAllDocs","payload","mimeTypes","where","docs","page","hasMore","result","find","depth","fallbackLocale","undefined","limit","locale","overrideAccess","select","alt","doc","push","id","hasNextPage","computeAltTextHealthScan","collectionSummaries","Promise","all","map","slug","Error","collectionError","logger","err","msg","plugin","completeDocs","invalidDocIds","missingDocs","partialDocs","totalDocs","filter","summary","getAltTextHealthCollectionTag","collectionSlug","getAltTextHealthScan","req","pluginConfig","config","custom","altTextPluginConfig","Boolean","localization","cacheKeyParts","sort","join","tags","Set","getCachedHealthScan","cacheFactory","compute","revalidate","userCanReadCollection","readAccess","access","read","filterScanByReadAccess","scan","visibility","_","index","allowedSlugs","has","canViewHealthReport","healthCheckAccess","toWidgetData","localeCount","length","reduce","total","c","getAltTextHealth","getAltTextHealthWidgetData"],"mappings":"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"}
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import type { CollectionSlug, TextareaFieldValidation, Where } from 'payload';
|
|
2
2
|
export declare const DEFAULT_TRACKED_MIME_TYPES: readonly string[];
|
|
3
3
|
export type AltTextCollectionConfig = {
|
|
4
|
+
/**
|
|
5
|
+
* The MIME type `getImageThumbnail` delivers for documents in this collection.
|
|
6
|
+
*
|
|
7
|
+
* The resolver only receives the bytes at that URL, never the stored file. When
|
|
8
|
+
* the URL transcodes (e.g. a Cloudinary `f_webp` transformation), the stored
|
|
9
|
+
* `mimeType` says nothing about what arrives, so AVIF and HEIC uploads are
|
|
10
|
+
* rejected on their source format even though the delivered bytes are fine.
|
|
11
|
+
*
|
|
12
|
+
* Declaring the delivered format replaces that per-document source check, and
|
|
13
|
+
* passes the type to the resolver as `imageThumbnailMimeType`. Which source
|
|
14
|
+
* formats get alt text at all is still governed by `mimeTypes`.
|
|
15
|
+
*
|
|
16
|
+
* Only declare a format the transform *always* produces. An `f_auto`-style
|
|
17
|
+
* transformation negotiates via the client's `Accept` header and may return the
|
|
18
|
+
* source format, so leave this unset there.
|
|
19
|
+
*
|
|
20
|
+
* Validated at config load — for syntax, and against the resolver's
|
|
21
|
+
* `supportedMimeTypes` when it declares any.
|
|
22
|
+
*
|
|
23
|
+
* Falls back to the plugin-level option of the same name; `null` opts out of it.
|
|
24
|
+
*
|
|
25
|
+
* @example 'image/webp'
|
|
26
|
+
*/
|
|
27
|
+
imageThumbnailMimeType?: null | string;
|
|
4
28
|
/**
|
|
5
29
|
* MIME types for which alt text is tracked, validated, and generated in this collection.
|
|
6
30
|
*
|
|
@@ -40,12 +64,41 @@ export type AltTextCollectionConfig = {
|
|
|
40
64
|
validate?: TextareaFieldValidation;
|
|
41
65
|
};
|
|
42
66
|
export type NormalizedAltTextCollectionConfig = {
|
|
67
|
+
/**
|
|
68
|
+
* The delivered thumbnail MIME type in effect for this collection, after the
|
|
69
|
+
* plugin-level default has been applied. Absent when the collection opted out
|
|
70
|
+
* (`null`) or neither level declared one.
|
|
71
|
+
*/
|
|
72
|
+
imageThumbnailMimeType?: string;
|
|
43
73
|
mimeTypes: string[];
|
|
44
74
|
slug: CollectionSlug;
|
|
45
75
|
validate?: TextareaFieldValidation;
|
|
46
76
|
};
|
|
47
77
|
export type IncomingCollectionsConfig = (AltTextCollectionConfig | CollectionSlug)[];
|
|
48
|
-
export declare function normalizeCollectionsConfig(incoming: IncomingCollectionsConfig
|
|
78
|
+
export declare function normalizeCollectionsConfig(incoming: IncomingCollectionsConfig, defaults?: {
|
|
79
|
+
imageThumbnailMimeType?: string;
|
|
80
|
+
}): NormalizedAltTextCollectionConfig[];
|
|
81
|
+
/**
|
|
82
|
+
* Whether a declared `imageThumbnailMimeType` is syntactically a MIME type.
|
|
83
|
+
*
|
|
84
|
+
* Checked independently of the resolver's `supportedMimeTypes`: declaring a type
|
|
85
|
+
* switches off the per-document source check, so a typo that booted cleanly would
|
|
86
|
+
* remove the guard while declaring nothing.
|
|
87
|
+
*/
|
|
88
|
+
export declare function isValidMimeType(value: string): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Whether a document's stored `mimeType` blocks alt text generation. Returns the
|
|
91
|
+
* error message when it does, `null` when generation may proceed.
|
|
92
|
+
*
|
|
93
|
+
* The stored type is only a proxy — the resolver receives the bytes at the
|
|
94
|
+
* thumbnail URL, not the stored file. A collection that declares what its URL
|
|
95
|
+
* delivers settles support at config load, making the source format irrelevant.
|
|
96
|
+
*/
|
|
97
|
+
export declare function getUnsupportedSourceMimeTypeError({ declaredThumbnailMimeType, mimeType, supportedMimeTypes, }: {
|
|
98
|
+
declaredThumbnailMimeType?: string;
|
|
99
|
+
mimeType?: string;
|
|
100
|
+
supportedMimeTypes?: readonly string[];
|
|
101
|
+
}): null | string;
|
|
49
102
|
export declare function matchesMimeType(mimeType: string, patterns: readonly string[]): boolean;
|
|
50
103
|
/**
|
|
51
104
|
* Builds a Payload `where` clause that matches documents whose `mimeType`
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
export const DEFAULT_TRACKED_MIME_TYPES = [
|
|
2
2
|
'image/*'
|
|
3
3
|
];
|
|
4
|
-
export function normalizeCollectionsConfig(incoming) {
|
|
4
|
+
export function normalizeCollectionsConfig(incoming, defaults) {
|
|
5
|
+
const defaultThumbnailMimeType = defaults?.imageThumbnailMimeType;
|
|
5
6
|
return incoming.map((entry)=>{
|
|
6
7
|
if (typeof entry === 'string') {
|
|
7
8
|
return {
|
|
8
9
|
slug: entry,
|
|
9
10
|
mimeTypes: [
|
|
10
11
|
...DEFAULT_TRACKED_MIME_TYPES
|
|
11
|
-
]
|
|
12
|
+
],
|
|
13
|
+
...typeof defaultThumbnailMimeType === 'string' && {
|
|
14
|
+
imageThumbnailMimeType: defaultThumbnailMimeType
|
|
15
|
+
}
|
|
12
16
|
};
|
|
13
17
|
}
|
|
14
18
|
const normalized = {
|
|
@@ -19,12 +23,47 @@ export function normalizeCollectionsConfig(incoming) {
|
|
|
19
23
|
...DEFAULT_TRACKED_MIME_TYPES
|
|
20
24
|
]
|
|
21
25
|
};
|
|
26
|
+
// `undefined` inherits the plugin-level default, `null` explicitly opts out of
|
|
27
|
+
// it. Resolving with `??` would silently collapse those two into "inherit".
|
|
28
|
+
const thumbnailMimeType = entry.imageThumbnailMimeType !== undefined ? entry.imageThumbnailMimeType : defaultThumbnailMimeType;
|
|
29
|
+
// Only `null` (opt-out) and an absent declaration drop the key. An empty
|
|
30
|
+
// string is kept so config-load validation rejects it, rather than it
|
|
31
|
+
// silently degrading into "nothing declared".
|
|
32
|
+
if (typeof thumbnailMimeType === 'string') {
|
|
33
|
+
normalized.imageThumbnailMimeType = thumbnailMimeType;
|
|
34
|
+
}
|
|
22
35
|
if (entry.validate) {
|
|
23
36
|
normalized.validate = entry.validate;
|
|
24
37
|
}
|
|
25
38
|
return normalized;
|
|
26
39
|
});
|
|
27
40
|
}
|
|
41
|
+
/** `type/subtype`, per RFC 6838's restricted-name grammar, lowercase. */ const MIME_TYPE_PATTERN = /^[a-z]+\/[a-z0-9][a-z0-9!#$&^_.+-]*$/;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a declared `imageThumbnailMimeType` is syntactically a MIME type.
|
|
44
|
+
*
|
|
45
|
+
* Checked independently of the resolver's `supportedMimeTypes`: declaring a type
|
|
46
|
+
* switches off the per-document source check, so a typo that booted cleanly would
|
|
47
|
+
* remove the guard while declaring nothing.
|
|
48
|
+
*/ export function isValidMimeType(value) {
|
|
49
|
+
return MIME_TYPE_PATTERN.test(value);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether a document's stored `mimeType` blocks alt text generation. Returns the
|
|
53
|
+
* error message when it does, `null` when generation may proceed.
|
|
54
|
+
*
|
|
55
|
+
* The stored type is only a proxy — the resolver receives the bytes at the
|
|
56
|
+
* thumbnail URL, not the stored file. A collection that declares what its URL
|
|
57
|
+
* delivers settles support at config load, making the source format irrelevant.
|
|
58
|
+
*/ export function getUnsupportedSourceMimeTypeError({ declaredThumbnailMimeType, mimeType, supportedMimeTypes }) {
|
|
59
|
+
if (declaredThumbnailMimeType || !mimeType || !supportedMimeTypes) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (supportedMimeTypes.includes(mimeType)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return `Alt text generation is not supported for files of type "${mimeType}". Supported types: ${supportedMimeTypes.join(', ')}.`;
|
|
66
|
+
}
|
|
28
67
|
// Payload stores upload mimeType values as the lowercase MIME string (e.g. `image/png`).
|
|
29
68
|
// Pattern comparisons here are case-sensitive; callers should pass lowercase patterns.
|
|
30
69
|
export function matchesMimeType(mimeType, patterns) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/mimeTypes.ts"],"sourcesContent":["import type { CollectionSlug, TextareaFieldValidation, Where } from 'payload'\n\nexport const DEFAULT_TRACKED_MIME_TYPES: readonly string[] = ['image/*']\n\nexport type AltTextCollectionConfig = {\n /**\n * MIME types for which alt text is tracked, validated, and generated in this collection.\n *\n * Accepts exact MIME types (e.g. `image/png`) or wildcards (e.g. `image/*`).\n * For documents whose mime type does not match, the alt text field is hidden,\n * its validation is skipped, and the document is excluded from the health widget.\n *\n * @default ['image/*']\n */\n mimeTypes?: string[]\n /** Collection slug to enable the plugin for. */\n slug: CollectionSlug\n /**\n * Custom validate function for the alt text field on this collection.\n * When provided, it fully replaces the default validator (`validateAltText`).\n *\n * Use this to relax or extend the default — for example, to skip the\n * required-alt check when the request body does not touch `alt`\n * (folder moves, partial API updates).\n *\n * @example\n * ```typescript\n * import { validateAltText } from '@jhb.software/payload-alt-text-plugin'\n *\n * collections: [\n * {\n * slug: 'media',\n * validate: (value, args) => {\n * const { req } = args\n * if (!req.data || !('alt' in req.data)) return true\n * return validateAltText(value, args)\n * },\n * },\n * ]\n * ```\n */\n validate?: TextareaFieldValidation\n}\n\nexport type NormalizedAltTextCollectionConfig = {\n mimeTypes: string[]\n slug: CollectionSlug\n validate?: TextareaFieldValidation\n}\n\nexport type IncomingCollectionsConfig = (AltTextCollectionConfig | CollectionSlug)[]\n\nexport function normalizeCollectionsConfig(\n incoming: IncomingCollectionsConfig,\n): NormalizedAltTextCollectionConfig[] {\n return incoming.map((entry) => {\n if (typeof entry === 'string') {\n return { slug: entry, mimeTypes: [...DEFAULT_TRACKED_MIME_TYPES] }\n }\n\n const normalized: NormalizedAltTextCollectionConfig = {\n slug: entry.slug,\n mimeTypes: entry.mimeTypes ? [...entry.mimeTypes] : [...DEFAULT_TRACKED_MIME_TYPES],\n }\n if (entry.validate) {\n normalized.validate = entry.validate\n }\n return normalized\n })\n}\n\n// Payload stores upload mimeType values as the lowercase MIME string (e.g. `image/png`).\n// Pattern comparisons here are case-sensitive; callers should pass lowercase patterns.\nexport function matchesMimeType(mimeType: string, patterns: readonly string[]): boolean {\n return patterns.some((pattern) => {\n if (pattern === mimeType) {\n return true\n }\n if (pattern.endsWith('/*')) {\n const prefix = pattern.slice(0, -1)\n return mimeType.startsWith(prefix)\n }\n return false\n })\n}\n\n/**\n * Builds a Payload `where` clause that matches documents whose `mimeType`\n * is in the given list of patterns. Returns `null` when nothing should match\n * (empty patterns), so callers can short-circuit the query.\n *\n * Wildcards like `image/*` are translated to a `like` (case-insensitive\n * substring) match on the prefix (`image/`). For valid MIME types this is\n * equivalent to a prefix match.\n */\nexport function buildMimeTypeWhere(patterns: readonly string[]): null | Where {\n if (patterns.length === 0) {\n return null\n }\n\n const exacts: string[] = []\n const wildcardPrefixes: string[] = []\n\n for (const pattern of patterns) {\n if (pattern.endsWith('/*')) {\n wildcardPrefixes.push(pattern.slice(0, -1))\n } else {\n exacts.push(pattern)\n }\n }\n\n const clauses: Where[] = []\n if (exacts.length > 0) {\n clauses.push({ mimeType: { in: exacts } })\n }\n for (const prefix of wildcardPrefixes) {\n clauses.push({ mimeType: { like: prefix } })\n }\n\n return clauses.length === 1 ? clauses[0] : { or: clauses }\n}\n\n/**\n * Default validation logic for the alt text field.\n *\n * - Allows an empty value during the initial upload (no regular update has occurred yet).\n * - Allows an empty value when the document's mime type is not tracked for alt text.\n * - Otherwise requires a non-empty value.\n *\n * Projects with stricter or looser requirements can pass a custom function to\n * a collection's `validate` option instead.\n */\nexport function validateAltText(\n value: Parameters<TextareaFieldValidation>[0],\n args: Parameters<TextareaFieldValidation>[1],\n trackedMimeTypes?: readonly string[],\n): string | true {\n const data = (args.data ?? {}) as Record<string, unknown>\n const { operation, req } = args\n\n // Since https://github.com/payloadcms/payload/pull/14988, when using external storage (e.g., S3),\n // it is no longer possible to detect whether this validation runs during the initial upload\n // or a regular update by checking the existence of the ID.\n // Instead, compare the timestamps of the createdAt and updatedAt fields.\n const isInitialUpload =\n operation === 'create' ||\n ('createdAt' in data && 'updatedAt' in data && data.createdAt === data.updatedAt)\n\n if (isInitialUpload) {\n return true\n }\n\n if (trackedMimeTypes && trackedMimeTypes.length > 0) {\n const mimeType = typeof data.mimeType === 'string' ? data.mimeType : undefined\n if (!mimeType || !matchesMimeType(mimeType, trackedMimeTypes)) {\n return true\n }\n }\n\n if (typeof value !== 'string' || value.trim().length === 0) {\n // @ts-expect-error - the translation key type does not include the custom key\n return req.t('@jhb.software/payload-alt-text-plugin:theAlternateTextIsRequired')\n }\n\n return true\n}\n"],"names":["DEFAULT_TRACKED_MIME_TYPES","normalizeCollectionsConfig","incoming","map","entry","slug","mimeTypes","normalized","validate","matchesMimeType","mimeType","patterns","some","pattern","endsWith","prefix","slice","startsWith","buildMimeTypeWhere","length","exacts","wildcardPrefixes","push","clauses","in","like","or","validateAltText","value","args","trackedMimeTypes","data","operation","req","isInitialUpload","createdAt","updatedAt","undefined","trim","t"],"mappings":"AAEA,OAAO,MAAMA,6BAAgD;IAAC;CAAU,CAAA;AAkDxE,OAAO,SAASC,2BACdC,QAAmC;IAEnC,OAAOA,SAASC,GAAG,CAAC,CAACC;QACnB,IAAI,OAAOA,UAAU,UAAU;YAC7B,OAAO;gBAAEC,MAAMD;gBAAOE,WAAW;uBAAIN;iBAA2B;YAAC;QACnE;QAEA,MAAMO,aAAgD;YACpDF,MAAMD,MAAMC,IAAI;YAChBC,WAAWF,MAAME,SAAS,GAAG;mBAAIF,MAAME,SAAS;aAAC,GAAG;mBAAIN;aAA2B;QACrF;QACA,IAAII,MAAMI,QAAQ,EAAE;YAClBD,WAAWC,QAAQ,GAAGJ,MAAMI,QAAQ;QACtC;QACA,OAAOD;IACT;AACF;AAEA,yFAAyF;AACzF,uFAAuF;AACvF,OAAO,SAASE,gBAAgBC,QAAgB,EAAEC,QAA2B;IAC3E,OAAOA,SAASC,IAAI,CAAC,CAACC;QACpB,IAAIA,YAAYH,UAAU;YACxB,OAAO;QACT;QACA,IAAIG,QAAQC,QAAQ,CAAC,OAAO;YAC1B,MAAMC,SAASF,QAAQG,KAAK,CAAC,GAAG,CAAC;YACjC,OAAON,SAASO,UAAU,CAACF;QAC7B;QACA,OAAO;IACT;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASG,mBAAmBP,QAA2B;IAC5D,IAAIA,SAASQ,MAAM,KAAK,GAAG;QACzB,OAAO;IACT;IAEA,MAAMC,SAAmB,EAAE;IAC3B,MAAMC,mBAA6B,EAAE;IAErC,KAAK,MAAMR,WAAWF,SAAU;QAC9B,IAAIE,QAAQC,QAAQ,CAAC,OAAO;YAC1BO,iBAAiBC,IAAI,CAACT,QAAQG,KAAK,CAAC,GAAG,CAAC;QAC1C,OAAO;YACLI,OAAOE,IAAI,CAACT;QACd;IACF;IAEA,MAAMU,UAAmB,EAAE;IAC3B,IAAIH,OAAOD,MAAM,GAAG,GAAG;QACrBI,QAAQD,IAAI,CAAC;YAAEZ,UAAU;gBAAEc,IAAIJ;YAAO;QAAE;IAC1C;IACA,KAAK,MAAML,UAAUM,iBAAkB;QACrCE,QAAQD,IAAI,CAAC;YAAEZ,UAAU;gBAAEe,MAAMV;YAAO;QAAE;IAC5C;IAEA,OAAOQ,QAAQJ,MAAM,KAAK,IAAII,OAAO,CAAC,EAAE,GAAG;QAAEG,IAAIH;IAAQ;AAC3D;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASI,gBACdC,KAA6C,EAC7CC,IAA4C,EAC5CC,gBAAoC;IAEpC,MAAMC,OAAQF,KAAKE,IAAI,IAAI,CAAC;IAC5B,MAAM,EAAEC,SAAS,EAAEC,GAAG,EAAE,GAAGJ;IAE3B,kGAAkG;IAClG,4FAA4F;IAC5F,2DAA2D;IAC3D,yEAAyE;IACzE,MAAMK,kBACJF,cAAc,YACb,eAAeD,QAAQ,eAAeA,QAAQA,KAAKI,SAAS,KAAKJ,KAAKK,SAAS;IAElF,IAAIF,iBAAiB;QACnB,OAAO;IACT;IAEA,IAAIJ,oBAAoBA,iBAAiBX,MAAM,GAAG,GAAG;QACnD,MAAMT,WAAW,OAAOqB,KAAKrB,QAAQ,KAAK,WAAWqB,KAAKrB,QAAQ,GAAG2B;QACrE,IAAI,CAAC3B,YAAY,CAACD,gBAAgBC,UAAUoB,mBAAmB;YAC7D,OAAO;QACT;IACF;IAEA,IAAI,OAAOF,UAAU,YAAYA,MAAMU,IAAI,GAAGnB,MAAM,KAAK,GAAG;QAC1D,8EAA8E;QAC9E,OAAOc,IAAIM,CAAC,CAAC;IACf;IAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/mimeTypes.ts"],"sourcesContent":["import type { CollectionSlug, TextareaFieldValidation, Where } from 'payload'\n\nexport const DEFAULT_TRACKED_MIME_TYPES: readonly string[] = ['image/*']\n\nexport type AltTextCollectionConfig = {\n /**\n * The MIME type `getImageThumbnail` delivers for documents in this collection.\n *\n * The resolver only receives the bytes at that URL, never the stored file. When\n * the URL transcodes (e.g. a Cloudinary `f_webp` transformation), the stored\n * `mimeType` says nothing about what arrives, so AVIF and HEIC uploads are\n * rejected on their source format even though the delivered bytes are fine.\n *\n * Declaring the delivered format replaces that per-document source check, and\n * passes the type to the resolver as `imageThumbnailMimeType`. Which source\n * formats get alt text at all is still governed by `mimeTypes`.\n *\n * Only declare a format the transform *always* produces. An `f_auto`-style\n * transformation negotiates via the client's `Accept` header and may return the\n * source format, so leave this unset there.\n *\n * Validated at config load — for syntax, and against the resolver's\n * `supportedMimeTypes` when it declares any.\n *\n * Falls back to the plugin-level option of the same name; `null` opts out of it.\n *\n * @example 'image/webp'\n */\n imageThumbnailMimeType?: null | string\n /**\n * MIME types for which alt text is tracked, validated, and generated in this collection.\n *\n * Accepts exact MIME types (e.g. `image/png`) or wildcards (e.g. `image/*`).\n * For documents whose mime type does not match, the alt text field is hidden,\n * its validation is skipped, and the document is excluded from the health widget.\n *\n * @default ['image/*']\n */\n mimeTypes?: string[]\n /** Collection slug to enable the plugin for. */\n slug: CollectionSlug\n /**\n * Custom validate function for the alt text field on this collection.\n * When provided, it fully replaces the default validator (`validateAltText`).\n *\n * Use this to relax or extend the default — for example, to skip the\n * required-alt check when the request body does not touch `alt`\n * (folder moves, partial API updates).\n *\n * @example\n * ```typescript\n * import { validateAltText } from '@jhb.software/payload-alt-text-plugin'\n *\n * collections: [\n * {\n * slug: 'media',\n * validate: (value, args) => {\n * const { req } = args\n * if (!req.data || !('alt' in req.data)) return true\n * return validateAltText(value, args)\n * },\n * },\n * ]\n * ```\n */\n validate?: TextareaFieldValidation\n}\n\nexport type NormalizedAltTextCollectionConfig = {\n /**\n * The delivered thumbnail MIME type in effect for this collection, after the\n * plugin-level default has been applied. Absent when the collection opted out\n * (`null`) or neither level declared one.\n */\n imageThumbnailMimeType?: string\n mimeTypes: string[]\n slug: CollectionSlug\n validate?: TextareaFieldValidation\n}\n\nexport type IncomingCollectionsConfig = (AltTextCollectionConfig | CollectionSlug)[]\n\nexport function normalizeCollectionsConfig(\n incoming: IncomingCollectionsConfig,\n defaults?: { imageThumbnailMimeType?: string },\n): NormalizedAltTextCollectionConfig[] {\n const defaultThumbnailMimeType = defaults?.imageThumbnailMimeType\n\n return incoming.map((entry) => {\n if (typeof entry === 'string') {\n return {\n slug: entry,\n mimeTypes: [...DEFAULT_TRACKED_MIME_TYPES],\n ...(typeof defaultThumbnailMimeType === 'string' && {\n imageThumbnailMimeType: defaultThumbnailMimeType,\n }),\n }\n }\n\n const normalized: NormalizedAltTextCollectionConfig = {\n slug: entry.slug,\n mimeTypes: entry.mimeTypes ? [...entry.mimeTypes] : [...DEFAULT_TRACKED_MIME_TYPES],\n }\n\n // `undefined` inherits the plugin-level default, `null` explicitly opts out of\n // it. Resolving with `??` would silently collapse those two into \"inherit\".\n const thumbnailMimeType =\n entry.imageThumbnailMimeType !== undefined\n ? entry.imageThumbnailMimeType\n : defaultThumbnailMimeType\n // Only `null` (opt-out) and an absent declaration drop the key. An empty\n // string is kept so config-load validation rejects it, rather than it\n // silently degrading into \"nothing declared\".\n if (typeof thumbnailMimeType === 'string') {\n normalized.imageThumbnailMimeType = thumbnailMimeType\n }\n\n if (entry.validate) {\n normalized.validate = entry.validate\n }\n return normalized\n })\n}\n\n/** `type/subtype`, per RFC 6838's restricted-name grammar, lowercase. */\nconst MIME_TYPE_PATTERN = /^[a-z]+\\/[a-z0-9][a-z0-9!#$&^_.+-]*$/\n\n/**\n * Whether a declared `imageThumbnailMimeType` is syntactically a MIME type.\n *\n * Checked independently of the resolver's `supportedMimeTypes`: declaring a type\n * switches off the per-document source check, so a typo that booted cleanly would\n * remove the guard while declaring nothing.\n */\nexport function isValidMimeType(value: string): boolean {\n return MIME_TYPE_PATTERN.test(value)\n}\n\n/**\n * Whether a document's stored `mimeType` blocks alt text generation. Returns the\n * error message when it does, `null` when generation may proceed.\n *\n * The stored type is only a proxy — the resolver receives the bytes at the\n * thumbnail URL, not the stored file. A collection that declares what its URL\n * delivers settles support at config load, making the source format irrelevant.\n */\nexport function getUnsupportedSourceMimeTypeError({\n declaredThumbnailMimeType,\n mimeType,\n supportedMimeTypes,\n}: {\n declaredThumbnailMimeType?: string\n mimeType?: string\n supportedMimeTypes?: readonly string[]\n}): null | string {\n if (declaredThumbnailMimeType || !mimeType || !supportedMimeTypes) {\n return null\n }\n\n if (supportedMimeTypes.includes(mimeType)) {\n return null\n }\n\n return `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${supportedMimeTypes.join(', ')}.`\n}\n\n// Payload stores upload mimeType values as the lowercase MIME string (e.g. `image/png`).\n// Pattern comparisons here are case-sensitive; callers should pass lowercase patterns.\nexport function matchesMimeType(mimeType: string, patterns: readonly string[]): boolean {\n return patterns.some((pattern) => {\n if (pattern === mimeType) {\n return true\n }\n if (pattern.endsWith('/*')) {\n const prefix = pattern.slice(0, -1)\n return mimeType.startsWith(prefix)\n }\n return false\n })\n}\n\n/**\n * Builds a Payload `where` clause that matches documents whose `mimeType`\n * is in the given list of patterns. Returns `null` when nothing should match\n * (empty patterns), so callers can short-circuit the query.\n *\n * Wildcards like `image/*` are translated to a `like` (case-insensitive\n * substring) match on the prefix (`image/`). For valid MIME types this is\n * equivalent to a prefix match.\n */\nexport function buildMimeTypeWhere(patterns: readonly string[]): null | Where {\n if (patterns.length === 0) {\n return null\n }\n\n const exacts: string[] = []\n const wildcardPrefixes: string[] = []\n\n for (const pattern of patterns) {\n if (pattern.endsWith('/*')) {\n wildcardPrefixes.push(pattern.slice(0, -1))\n } else {\n exacts.push(pattern)\n }\n }\n\n const clauses: Where[] = []\n if (exacts.length > 0) {\n clauses.push({ mimeType: { in: exacts } })\n }\n for (const prefix of wildcardPrefixes) {\n clauses.push({ mimeType: { like: prefix } })\n }\n\n return clauses.length === 1 ? clauses[0] : { or: clauses }\n}\n\n/**\n * Default validation logic for the alt text field.\n *\n * - Allows an empty value during the initial upload (no regular update has occurred yet).\n * - Allows an empty value when the document's mime type is not tracked for alt text.\n * - Otherwise requires a non-empty value.\n *\n * Projects with stricter or looser requirements can pass a custom function to\n * a collection's `validate` option instead.\n */\nexport function validateAltText(\n value: Parameters<TextareaFieldValidation>[0],\n args: Parameters<TextareaFieldValidation>[1],\n trackedMimeTypes?: readonly string[],\n): string | true {\n const data = (args.data ?? {}) as Record<string, unknown>\n const { operation, req } = args\n\n // Since https://github.com/payloadcms/payload/pull/14988, when using external storage (e.g., S3),\n // it is no longer possible to detect whether this validation runs during the initial upload\n // or a regular update by checking the existence of the ID.\n // Instead, compare the timestamps of the createdAt and updatedAt fields.\n const isInitialUpload =\n operation === 'create' ||\n ('createdAt' in data && 'updatedAt' in data && data.createdAt === data.updatedAt)\n\n if (isInitialUpload) {\n return true\n }\n\n if (trackedMimeTypes && trackedMimeTypes.length > 0) {\n const mimeType = typeof data.mimeType === 'string' ? data.mimeType : undefined\n if (!mimeType || !matchesMimeType(mimeType, trackedMimeTypes)) {\n return true\n }\n }\n\n if (typeof value !== 'string' || value.trim().length === 0) {\n // @ts-expect-error - the translation key type does not include the custom key\n return req.t('@jhb.software/payload-alt-text-plugin:theAlternateTextIsRequired')\n }\n\n return true\n}\n"],"names":["DEFAULT_TRACKED_MIME_TYPES","normalizeCollectionsConfig","incoming","defaults","defaultThumbnailMimeType","imageThumbnailMimeType","map","entry","slug","mimeTypes","normalized","thumbnailMimeType","undefined","validate","MIME_TYPE_PATTERN","isValidMimeType","value","test","getUnsupportedSourceMimeTypeError","declaredThumbnailMimeType","mimeType","supportedMimeTypes","includes","join","matchesMimeType","patterns","some","pattern","endsWith","prefix","slice","startsWith","buildMimeTypeWhere","length","exacts","wildcardPrefixes","push","clauses","in","like","or","validateAltText","args","trackedMimeTypes","data","operation","req","isInitialUpload","createdAt","updatedAt","trim","t"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhb.software/payload-alt-text-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A Payload CMS plugin that adds AI-powered alt text generation for images.",
|
|
5
5
|
"bugs": "https://github.com/jhb-software/payload-plugins/issues",
|
|
6
6
|
"repository": {
|
|
@@ -20,28 +20,28 @@
|
|
|
20
20
|
"main": "./dist/index.js",
|
|
21
21
|
"types": "./dist/index.d.ts",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"openai": "^
|
|
24
|
-
"p-map": "^7.0.
|
|
23
|
+
"openai": "^7.4.0",
|
|
24
|
+
"p-map": "^7.0.6",
|
|
25
25
|
"zod": "^4.4.3"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@payloadcms/translations": "^3.
|
|
29
|
-
"@payloadcms/ui": "^3.
|
|
30
|
-
"next": "^15.
|
|
31
|
-
"payload": "^3.
|
|
32
|
-
"react": "19.2.
|
|
33
|
-
"react-dom": "19.2.
|
|
28
|
+
"@payloadcms/translations": "^3.88.0",
|
|
29
|
+
"@payloadcms/ui": "^3.88.0",
|
|
30
|
+
"next": "^15.1.0 || ^16.0.0",
|
|
31
|
+
"payload": "^3.88.0",
|
|
32
|
+
"react": "19.2.8",
|
|
33
|
+
"react-dom": "19.2.8"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@payloadcms/eslint-config": "^3.28.0",
|
|
37
|
-
"@payloadcms/translations": "^3.
|
|
37
|
+
"@payloadcms/translations": "^3.88.0",
|
|
38
38
|
"@swc/cli": "^0.8.1",
|
|
39
|
-
"@swc/core": "^1.15.
|
|
40
|
-
"@types/react": "19.2.
|
|
41
|
-
"@types/react-dom": "19.2.
|
|
39
|
+
"@swc/core": "^1.15.47",
|
|
40
|
+
"@types/react": "19.2.18",
|
|
41
|
+
"@types/react-dom": "19.2.4",
|
|
42
42
|
"copyfiles": "2.4.1",
|
|
43
43
|
"eslint": "^9.39.4",
|
|
44
|
-
"prettier": "
|
|
44
|
+
"prettier": "3.9.6",
|
|
45
45
|
"rimraf": "6.1.3",
|
|
46
46
|
"typescript": "^6.0.3",
|
|
47
47
|
"vitest": "^4.1.10"
|