@jhb.software/payload-alt-text-plugin 0.4.4 → 0.5.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 +62 -33
- package/dist/components/AltTextField.d.ts +1 -1
- package/dist/components/AltTextField.js +9 -0
- package/dist/components/AltTextField.js.map +1 -1
- package/dist/endpoints/bulkGenerateAltTexts.js +5 -0
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +9 -0
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/fields/altTextField.d.ts +7 -2
- package/dist/fields/altTextField.js +5 -19
- package/dist/fields/altTextField.js.map +1 -1
- package/dist/hooks/revalidateAltTextHealth.js +5 -0
- package/dist/hooks/revalidateAltTextHealth.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +23 -47
- package/dist/plugin.js.map +1 -1
- package/dist/types/AltTextPluginConfig.d.ts +21 -5
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/altTextHealth.js +18 -10
- package/dist/utilities/altTextHealth.js.map +1 -1
- package/dist/utilities/mimeTypes.d.ts +70 -0
- package/dist/utilities/mimeTypes.js +115 -0
- package/dist/utilities/mimeTypes.js.map +1 -0
- package/package.json +14 -12
package/README.md
CHANGED
|
@@ -12,20 +12,17 @@ A [Payload CMS](https://payloadcms.com/) plugin that adds AI-powered alt text ge
|
|
|
12
12
|
- Full localization support
|
|
13
13
|
- Dashboard health widget with cached coverage insights across all configured upload collections
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
When the plugin is enabled for an upload collection, it will:
|
|
17
16
|
|
|
18
17
|
1. Add an alt text field to the collection
|
|
19
18
|
- A button to AI-generate the alt text
|
|
20
19
|
- This field will include a description of what the alt text should be
|
|
21
20
|
2. Add a keywords fields to the collection
|
|
22
|
-
- This field will be automatically filled when generating the alt text
|
|
21
|
+
- This field will be automatically filled when generating the alt text
|
|
23
22
|
- It will be used for improving the search of images in the admin panel
|
|
24
|
-
|
|
23
|
+
3. Add a bulk generate button to the collection list view
|
|
25
24
|
- This button will allow you to generate alt text for multiple images at once
|
|
26
|
-
|
|
27
|
-
- The widget is available in Payload's dashboard editor
|
|
28
|
-
- It is added to the default dashboard layout for first-time and reset layouts
|
|
25
|
+
4. Register an `Alt text health` dashboard widget
|
|
29
26
|
- Results are cached and revalidated when documents in the configured upload collections change
|
|
30
27
|
|
|
31
28
|
## Installation
|
|
@@ -39,10 +36,7 @@ pnpm add @jhb.software/payload-alt-text-plugin
|
|
|
39
36
|
Install the plugin and add it to your Payload config:
|
|
40
37
|
|
|
41
38
|
```ts
|
|
42
|
-
import {
|
|
43
|
-
payloadAltTextPlugin,
|
|
44
|
-
openAIResolver,
|
|
45
|
-
} from '@jhb.software/payload-alt-text-plugin'
|
|
39
|
+
import { payloadAltTextPlugin, openAIResolver } from '@jhb.software/payload-alt-text-plugin'
|
|
46
40
|
|
|
47
41
|
export default buildConfig({
|
|
48
42
|
plugins: [
|
|
@@ -60,25 +54,60 @@ export default buildConfig({
|
|
|
60
54
|
|
|
61
55
|
Note: When localization is disabled in your Payload config (default), you need to specify the locale to generate the alt texts in via the `locale` plugin option.
|
|
62
56
|
|
|
57
|
+
### Admin list search
|
|
58
|
+
|
|
59
|
+
By default, the plugin sets `admin.listSearchableFields` on the configured upload collections to `['filename', 'keywords', 'alt']` so the admin list-view search matches against these fields. To opt out, set `admin.listSearchableFields` on the collection yourself — any explicit value is preserved as-is:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
{
|
|
63
|
+
slug: 'media',
|
|
64
|
+
upload: true,
|
|
65
|
+
admin: {
|
|
66
|
+
listSearchableFields: ['filename', 'alt'],
|
|
67
|
+
},
|
|
68
|
+
// ...
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This is also the recommended escape hatch if you hit Payload's Postgres SQL-builder bug for `hasMany` localized text fields in `listSearchableFields` (see [#92](https://github.com/jhb-software/payload-plugins/issues/92)).
|
|
73
|
+
|
|
63
74
|
## Configuration
|
|
64
75
|
|
|
65
76
|
### Plugin Options
|
|
66
77
|
|
|
67
|
-
| Option | Type | Required | Description
|
|
68
|
-
| ---------------------------- | ------------------ | -------- |
|
|
69
|
-
| `collections` | `CollectionSlug[]` | Yes | Collections to enable alt text generation for
|
|
70
|
-
| `resolver` | `AltTextResolver` | Yes | Alt text resolver to use (e.g., `openAIResolver`)
|
|
71
|
-
| `getImageThumbnail` | `Function` | Yes | Function to get the thumbnail URL from an image document
|
|
72
|
-
| `enabled` | `boolean` | No | Whether to enable the plugin
|
|
73
|
-
| `locale` | `string` | No | Locale for alt text generation (required when localization is disabled)
|
|
74
|
-
| `maxBulkGenerateConcurrency` | `number` | No | Maximum concurrent API requests for bulk operations (default: 16)
|
|
75
|
-
| `fieldsOverride` | `Function` | No | Override the default fields inserted by the plugin
|
|
78
|
+
| Option | Type | Required | Description |
|
|
79
|
+
| ---------------------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `collections` | `CollectionSlug[]` | Yes | Collections to enable alt text generation for |
|
|
81
|
+
| `resolver` | `AltTextResolver` | Yes | Alt text resolver to use (e.g., `openAIResolver`) |
|
|
82
|
+
| `getImageThumbnail` | `Function` | Yes | Function to get the thumbnail URL from an image document |
|
|
83
|
+
| `enabled` | `boolean` | No | Whether to enable the plugin |
|
|
84
|
+
| `locale` | `string` | No | Locale for alt text generation (required when localization is disabled) |
|
|
85
|
+
| `maxBulkGenerateConcurrency` | `number` | No | Maximum concurrent API requests for bulk operations (default: 16) |
|
|
86
|
+
| `fieldsOverride` | `Function` | No | Override the default fields inserted by the plugin |
|
|
76
87
|
| `healthCheck` | `boolean` | No | Enable alt text health tracking: REST endpoint, cache revalidation hooks, and dashboard widget (default: `true`) |
|
|
77
88
|
|
|
78
89
|
## Dashboard Widget
|
|
79
90
|
|
|
80
91
|
The plugin registers an `Alt text health` dashboard widget that shows alt text coverage across all configured upload collections, with cached queries that revalidate on document changes. Collections with missing alt text show a clickable badge linking to the affected images.
|
|
81
92
|
|
|
93
|
+
<img width="696" height="246" alt="image" src="https://github.com/user-attachments/assets/75df7349-0307-4047-b1ac-6b2ee0814464" />
|
|
94
|
+
|
|
95
|
+
The widget is registered under `admin.dashboard.widgets` with the slug `alt-text-health`. To show it by default on the dashboard, add it to your `admin.dashboard.defaultLayout`:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
buildConfig({
|
|
99
|
+
admin: {
|
|
100
|
+
dashboard: {
|
|
101
|
+
defaultLayout: [
|
|
102
|
+
// ...other default widgets
|
|
103
|
+
{ widgetSlug: 'alt-text-health', width: 'full' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
// ...
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
82
111
|
Set `healthCheck: false` in the plugin config to disable the REST endpoint, cache revalidation hooks, and dashboard widget. If your project replaces the default dashboard via `admin.components.views.dashboard`, you need to integrate the widget into your custom dashboard yourself.
|
|
83
112
|
|
|
84
113
|
### Resolvers
|
|
@@ -117,12 +146,12 @@ export const customResolver = (): AltTextResolver => ({
|
|
|
117
146
|
}
|
|
118
147
|
},
|
|
119
148
|
resolveBulk: async ({ imageThumbnailUrl, filename, locales, req }) => {
|
|
120
|
-
|
|
149
|
+
// Your custom alt text generation logic here
|
|
121
150
|
const altTexts = await generateAltTextBulk(imageThumbnailUrl, filename, locales, req)
|
|
122
151
|
|
|
123
|
-
return {
|
|
124
|
-
success: true,
|
|
125
|
-
results: altTexts
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
results: altTexts,
|
|
126
155
|
}
|
|
127
156
|
},
|
|
128
157
|
})
|
|
@@ -138,12 +167,12 @@ Generates alt text for a single image. By default, returns the result without sa
|
|
|
138
167
|
|
|
139
168
|
**Request body:**
|
|
140
169
|
|
|
141
|
-
| Field | Type | Required | Description
|
|
142
|
-
| ------------ | ------------------ | -------- |
|
|
143
|
-
| `id` | `string \| number` | Yes | The document ID
|
|
144
|
-
| `collection` | `string` | Yes | The collection slug
|
|
145
|
-
| `locale` | `string \| null` | Yes | Target locale (use `null` for non-localized setups)
|
|
146
|
-
| `update` | `boolean` | No | When `true`, persists the result to the document (default: `false`)
|
|
170
|
+
| Field | Type | Required | Description |
|
|
171
|
+
| ------------ | ------------------ | -------- | ------------------------------------------------------------------- |
|
|
172
|
+
| `id` | `string \| number` | Yes | The document ID |
|
|
173
|
+
| `collection` | `string` | Yes | The collection slug |
|
|
174
|
+
| `locale` | `string \| null` | Yes | Target locale (use `null` for non-localized setups) |
|
|
175
|
+
| `update` | `boolean` | No | When `true`, persists the result to the document (default: `false`) |
|
|
147
176
|
|
|
148
177
|
**Response:**
|
|
149
178
|
|
|
@@ -162,10 +191,10 @@ Generates and persists alt text for multiple images across all configured locale
|
|
|
162
191
|
|
|
163
192
|
**Request body:**
|
|
164
193
|
|
|
165
|
-
| Field | Type
|
|
166
|
-
| ------------ |
|
|
167
|
-
| `collection` | `string`
|
|
168
|
-
| `ids` | `(string \| number)[]` | Yes
|
|
194
|
+
| Field | Type | Required | Description |
|
|
195
|
+
| ------------ | ---------------------- | -------- | -------------------------------- |
|
|
196
|
+
| `collection` | `string` | Yes | The collection slug |
|
|
197
|
+
| `ids` | `(string \| number)[]` | Yes | Array of document IDs to process |
|
|
169
198
|
|
|
170
199
|
**Response:**
|
|
171
200
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { TextareaFieldClientProps } from 'payload';
|
|
2
|
-
export declare const AltTextField: (clientProps: TextareaFieldClientProps) => import("react").JSX.Element;
|
|
2
|
+
export declare const AltTextField: (clientProps: TextareaFieldClientProps) => import("react").JSX.Element | null;
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { FieldLabel, TextareaInput, useDocumentInfo, useField } from '@payloadcms/ui';
|
|
4
|
+
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
4
5
|
import { GenerateAltTextButton } from './GenerateAltTextButton.js';
|
|
5
6
|
export const AltTextField = (clientProps)=>{
|
|
6
7
|
const { field, path } = clientProps;
|
|
7
8
|
const supportedMimeTypes = field.admin?.custom?.supportedMimeTypes;
|
|
9
|
+
const trackedMimeTypes = field.admin?.custom?.trackedMimeTypes;
|
|
8
10
|
const { setValue, value } = useField({
|
|
9
11
|
path
|
|
10
12
|
});
|
|
11
13
|
const { id } = useDocumentInfo();
|
|
14
|
+
const { value: mimeType } = useField({
|
|
15
|
+
path: 'mimeType'
|
|
16
|
+
});
|
|
17
|
+
const isTrackedMimeType = !trackedMimeTypes || trackedMimeTypes.length === 0 || !!mimeType && matchesMimeType(mimeType, trackedMimeTypes);
|
|
18
|
+
if (!isTrackedMimeType) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
12
21
|
// the field should be optional when the document is created
|
|
13
22
|
// (since the alt text generation can only be used once the document is created and the image uploaded)
|
|
14
23
|
const required = id ? field.required : false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/AltTextField.tsx"],"sourcesContent":["'use client'\n\nimport type { TextareaFieldClientProps } from 'payload'\n\nimport { FieldLabel, TextareaInput, useDocumentInfo, useField } from '@payloadcms/ui'\n\nimport { GenerateAltTextButton } from './GenerateAltTextButton.js'\n\nexport const AltTextField = (clientProps: TextareaFieldClientProps) => {\n const { field, path } = clientProps\n\n const supportedMimeTypes = field.admin?.custom?.supportedMimeTypes as string[] | undefined\n\n const { setValue, value } = useField<string>({ path })\n const { id } = useDocumentInfo()\n\n // the field should be optional when the document is created\n // (since the alt text generation can only be used once the document is created and the image uploaded)\n const required = id ? field.required : false\n\n return (\n <div className=\"field-type textarea\" style={{ flex: '1 1 auto' }}>\n <FieldLabel\n htmlFor={`field-${path}`}\n label={field.label}\n localized={field.localized}\n required={required}\n />\n\n <div className=\"field-type__wrap\">\n <TextareaInput\n AfterInput={<GenerateAltTextButton supportedMimeTypes={supportedMimeTypes} />}\n onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value)}\n path={path}\n required={required}\n value={value}\n />\n </div>\n </div>\n )\n}\n"],"names":["FieldLabel","TextareaInput","useDocumentInfo","useField","GenerateAltTextButton","AltTextField","clientProps","field","path","supportedMimeTypes","admin","custom","setValue","value","id","required","div","className","style","flex","htmlFor","label","localized","AfterInput","onChange","e","target"],"mappings":"AAAA;;AAIA,SAASA,UAAU,EAAEC,aAAa,EAAEC,eAAe,EAAEC,QAAQ,QAAQ,iBAAgB;AAErF,SAASC,qBAAqB,QAAQ,6BAA4B;AAElE,OAAO,MAAMC,eAAe,CAACC;IAC3B,MAAM,EAAEC,KAAK,EAAEC,IAAI,EAAE,GAAGF;IAExB,MAAMG,qBAAqBF,MAAMG,KAAK,EAAEC,QAAQF;
|
|
1
|
+
{"version":3,"sources":["../../src/components/AltTextField.tsx"],"sourcesContent":["'use client'\n\nimport type { TextareaFieldClientProps } from 'payload'\n\nimport { FieldLabel, TextareaInput, useDocumentInfo, useField } from '@payloadcms/ui'\n\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { GenerateAltTextButton } from './GenerateAltTextButton.js'\n\nexport const AltTextField = (clientProps: TextareaFieldClientProps) => {\n const { field, path } = clientProps\n\n const supportedMimeTypes = field.admin?.custom?.supportedMimeTypes as string[] | undefined\n const trackedMimeTypes = field.admin?.custom?.trackedMimeTypes as string[] | undefined\n\n const { setValue, value } = useField<string>({ path })\n const { id } = useDocumentInfo()\n const { value: mimeType } = useField<string>({ path: 'mimeType' })\n\n const isTrackedMimeType =\n !trackedMimeTypes ||\n trackedMimeTypes.length === 0 ||\n (!!mimeType && matchesMimeType(mimeType, trackedMimeTypes))\n\n if (!isTrackedMimeType) {\n return null\n }\n\n // the field should be optional when the document is created\n // (since the alt text generation can only be used once the document is created and the image uploaded)\n const required = id ? field.required : false\n\n return (\n <div className=\"field-type textarea\" style={{ flex: '1 1 auto' }}>\n <FieldLabel\n htmlFor={`field-${path}`}\n label={field.label}\n localized={field.localized}\n required={required}\n />\n\n <div className=\"field-type__wrap\">\n <TextareaInput\n AfterInput={<GenerateAltTextButton supportedMimeTypes={supportedMimeTypes} />}\n onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value)}\n path={path}\n required={required}\n value={value}\n />\n </div>\n </div>\n )\n}\n"],"names":["FieldLabel","TextareaInput","useDocumentInfo","useField","matchesMimeType","GenerateAltTextButton","AltTextField","clientProps","field","path","supportedMimeTypes","admin","custom","trackedMimeTypes","setValue","value","id","mimeType","isTrackedMimeType","length","required","div","className","style","flex","htmlFor","label","localized","AfterInput","onChange","e","target"],"mappings":"AAAA;;AAIA,SAASA,UAAU,EAAEC,aAAa,EAAEC,eAAe,EAAEC,QAAQ,QAAQ,iBAAgB;AAErF,SAASC,eAAe,QAAQ,4BAA2B;AAC3D,SAASC,qBAAqB,QAAQ,6BAA4B;AAElE,OAAO,MAAMC,eAAe,CAACC;IAC3B,MAAM,EAAEC,KAAK,EAAEC,IAAI,EAAE,GAAGF;IAExB,MAAMG,qBAAqBF,MAAMG,KAAK,EAAEC,QAAQF;IAChD,MAAMG,mBAAmBL,MAAMG,KAAK,EAAEC,QAAQC;IAE9C,MAAM,EAAEC,QAAQ,EAAEC,KAAK,EAAE,GAAGZ,SAAiB;QAAEM;IAAK;IACpD,MAAM,EAAEO,EAAE,EAAE,GAAGd;IACf,MAAM,EAAEa,OAAOE,QAAQ,EAAE,GAAGd,SAAiB;QAAEM,MAAM;IAAW;IAEhE,MAAMS,oBACJ,CAACL,oBACDA,iBAAiBM,MAAM,KAAK,KAC3B,CAAC,CAACF,YAAYb,gBAAgBa,UAAUJ;IAE3C,IAAI,CAACK,mBAAmB;QACtB,OAAO;IACT;IAEA,4DAA4D;IAC5D,uGAAuG;IACvG,MAAME,WAAWJ,KAAKR,MAAMY,QAAQ,GAAG;IAEvC,qBACE,MAACC;QAAIC,WAAU;QAAsBC,OAAO;YAAEC,MAAM;QAAW;;0BAC7D,KAACxB;gBACCyB,SAAS,CAAC,MAAM,EAAEhB,MAAM;gBACxBiB,OAAOlB,MAAMkB,KAAK;gBAClBC,WAAWnB,MAAMmB,SAAS;gBAC1BP,UAAUA;;0BAGZ,KAACC;gBAAIC,WAAU;0BACb,cAAA,KAACrB;oBACC2B,0BAAY,KAACvB;wBAAsBK,oBAAoBA;;oBACvDmB,UAAU,CAACC,IAA8ChB,SAASgB,EAAEC,MAAM,CAAChB,KAAK;oBAChFN,MAAMA;oBACNW,UAAUA;oBACVL,OAAOA;;;;;AAKjB,EAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pMap from 'p-map';
|
|
2
2
|
import { z, ZodError } from 'zod';
|
|
3
3
|
import { localesFromConfig } from '../utilities/localesFromConfig.js';
|
|
4
|
+
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
4
5
|
/**
|
|
5
6
|
* Generates and updates alt text for multiple images in all locales.
|
|
6
7
|
*/ export const bulkGenerateAltTextsEndpoint = (access)=>async (req)=>{
|
|
@@ -111,6 +112,10 @@ async function generateAndUpdateAltText({ id, collection, locales, payload, plug
|
|
|
111
112
|
throw new Error('Image not found');
|
|
112
113
|
}
|
|
113
114
|
const mimeType = 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined;
|
|
115
|
+
const collectionConfig = pluginConfig.collections.find((entry)=>entry.slug === collection);
|
|
116
|
+
if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {
|
|
117
|
+
throw new Error(`Alt text is not tracked for files of type "${mimeType}" in the "${collection}" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`);
|
|
118
|
+
}
|
|
114
119
|
if (mimeType && pluginConfig.resolver.supportedMimeTypes && !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)) {
|
|
115
120
|
throw new Error(`Alt text generation is not supported for files of type "${mimeType}". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`);
|
|
116
121
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const schema = z.object({\n collection: z.string(),\n ids: z.array(z.union([z.string(), z.number()])),\n })\n\n const { collection, ids } = schema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n ids,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${ids.length} updated (${Math.round((updatedDocs / ids.length) * 100)}%)`,\n )\n } catch (error) {\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: ids.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n throw new Error(\n `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n })\n }\n }\n}\n"],"names":["pMap","z","ZodError","localesFromConfig","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","schema","object","collection","string","ids","array","union","number","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","resolver","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","details","issues","map","e","message","path","Error","imageDoc","findByID","depth","mimeType","undefined","supportedMimeTypes","includes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const schema = z.object({\n collection: z.string(),\n ids: z.array(z.union([z.string(), z.number()])),\n })\n\n const { collection, ids } = schema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n ids,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${ids.length} updated (${Math.round((updatedDocs / ids.length) * 100)}%)`,\n )\n } catch (error) {\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: ids.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n throw new Error(\n `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n throw new Error(\n `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n })\n }\n }\n}\n"],"names":["pMap","z","ZodError","localesFromConfig","matchesMimeType","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","schema","object","collection","string","ids","array","union","number","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","resolver","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","details","issues","map","e","message","path","Error","imageDoc","findByID","depth","mimeType","undefined","collectionConfig","collections","find","entry","slug","mimeTypes","supportedMimeTypes","includes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z, ZodError } from 'zod';
|
|
2
|
+
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
2
3
|
/**
|
|
3
4
|
* Generates alt text for a single image using the configured resolver.
|
|
4
5
|
*
|
|
@@ -64,6 +65,14 @@ import { z, ZodError } from 'zod';
|
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
const mimeType = 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined;
|
|
68
|
+
const collectionConfig = pluginConfig.collections.find((entry)=>entry.slug === collection);
|
|
69
|
+
if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {
|
|
70
|
+
return Response.json({
|
|
71
|
+
error: `Alt text is not tracked for files of type "${mimeType}" in the "${collection}" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`
|
|
72
|
+
}, {
|
|
73
|
+
status: 400
|
|
74
|
+
});
|
|
75
|
+
}
|
|
67
76
|
if (mimeType && pluginConfig.resolver.supportedMimeTypes && !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)) {
|
|
68
77
|
return Response.json({
|
|
69
78
|
error: `Alt text generation is not supported for files of type "${mimeType}". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const requestSchema = z.object({\n id: z.union([z.string(), z.number()]),\n collection: z.string(),\n locale: z.string().nullable(),\n update: z.boolean().optional().default(false),\n })\n\n const { id, collection, locale, update } = requestSchema.parse(data)\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n return Response.json(\n {\n error: `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\n }\n console.error('Error generating alt text:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n"],"names":["z","ZodError","generateAltTextEndpoint","access","req","Response","json","error","status","data","requestSchema","object","id","union","string","number","collection","locale","nullable","update","boolean","optional","default","parse","imageDoc","payload","findByID","depth","pluginConfig","config","custom","altTextPluginConfig","getImageThumbnail","resolver","mimeType","undefined","
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const requestSchema = z.object({\n id: z.union([z.string(), z.number()]),\n collection: z.string(),\n locale: z.string().nullable(),\n update: z.boolean().optional().default(false),\n })\n\n const { id, collection, locale, update } = requestSchema.parse(data)\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n return Response.json(\n {\n error: `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n return Response.json(\n {\n error: `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\n }\n console.error('Error generating alt text:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n"],"names":["z","ZodError","matchesMimeType","generateAltTextEndpoint","access","req","Response","json","error","status","data","requestSchema","object","id","union","string","number","collection","locale","nullable","update","boolean","optional","default","parse","imageDoc","payload","findByID","depth","pluginConfig","config","custom","altTextPluginConfig","getImageThumbnail","resolver","mimeType","undefined","collectionConfig","collections","find","entry","slug","mimeTypes","join","supportedMimeTypes","includes","targetLocale","imageThumbnailUrl","result","resolve","filename","success","alt","altText","keywords","details","issues","map","e","message","path","console","Error"],"mappings":"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"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type { TextareaField } from 'payload';
|
|
2
|
-
export declare function altTextField({ localized, supportedMimeTypes, }: {
|
|
1
|
+
import type { TextareaField, TextareaFieldValidation } from 'payload';
|
|
2
|
+
export declare function altTextField({ localized, supportedMimeTypes, trackedMimeTypes, validate, }: {
|
|
3
3
|
localized?: TextareaField['localized'];
|
|
4
|
+
/** MIME types the resolver can generate for — used to disable the Generate button. */
|
|
4
5
|
supportedMimeTypes?: string[];
|
|
6
|
+
/** MIME types for which alt text is tracked — used to hide the field and skip validation for others. */
|
|
7
|
+
trackedMimeTypes?: string[];
|
|
8
|
+
/** Custom validator that fully replaces the default. */
|
|
9
|
+
validate?: TextareaFieldValidation;
|
|
5
10
|
}): TextareaField;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { validateAltText } from '../utilities/mimeTypes.js';
|
|
1
2
|
import { translatedLabel } from '../utils/translatedLabel.js';
|
|
2
|
-
export function altTextField({ localized, supportedMimeTypes }) {
|
|
3
|
+
export function altTextField({ localized, supportedMimeTypes, trackedMimeTypes, validate }) {
|
|
3
4
|
return {
|
|
4
5
|
name: 'alt',
|
|
5
6
|
type: 'textarea',
|
|
@@ -8,29 +9,14 @@ export function altTextField({ localized, supportedMimeTypes }) {
|
|
|
8
9
|
Field: '@jhb.software/payload-alt-text-plugin/client#AltTextField'
|
|
9
10
|
},
|
|
10
11
|
custom: {
|
|
11
|
-
supportedMimeTypes
|
|
12
|
+
supportedMimeTypes,
|
|
13
|
+
trackedMimeTypes
|
|
12
14
|
}
|
|
13
15
|
},
|
|
14
16
|
label: translatedLabel('alternateText'),
|
|
15
17
|
localized,
|
|
16
18
|
required: true,
|
|
17
|
-
validate: (value,
|
|
18
|
-
// Since https://github.com/payloadcms/payload/pull/14988, when using external storage (e.g., S3),
|
|
19
|
-
// it is no longer possible to detect whether this validation runs during the initial upload
|
|
20
|
-
// or a regular update by checking the existence of the ID.
|
|
21
|
-
// Instead, compare the timestamps of the createdAt and updatedAt fields.
|
|
22
|
-
const isInitialUpload = operation === 'create' || 'createdAt' in data && 'updatedAt' in data && data.createdAt === data.updatedAt;
|
|
23
|
-
// initial upload: allow without alt text
|
|
24
|
-
if (isInitialUpload) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
// regular update: require alt text
|
|
28
|
-
if (!value || value.trim().length === 0) {
|
|
29
|
-
// @ts-expect-error - the translation key type does not include the custom key
|
|
30
|
-
return t('@jhb.software/payload-alt-text-plugin:theAlternateTextIsRequired');
|
|
31
|
-
}
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
19
|
+
validate: validate ?? ((value, args)=>validateAltText(value, args, trackedMimeTypes))
|
|
34
20
|
};
|
|
35
21
|
}
|
|
36
22
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fields/altTextField.ts"],"sourcesContent":["import type { TextareaField } from 'payload'\n\nimport { translatedLabel } from '../utils/translatedLabel.js'\n\nexport function altTextField({\n localized,\n supportedMimeTypes,\n}: {\n localized?: TextareaField['localized']\n supportedMimeTypes?: string[]\n}): TextareaField {\n return {\n name: 'alt',\n type: 'textarea',\n admin: {\n components: {\n Field: '@jhb.software/payload-alt-text-plugin/client#AltTextField',\n },\n custom: {\n supportedMimeTypes,\n },\n },\n label: translatedLabel('alternateText'),\n localized,\n required: true,\n validate: (value,
|
|
1
|
+
{"version":3,"sources":["../../src/fields/altTextField.ts"],"sourcesContent":["import type { TextareaField, TextareaFieldValidation } from 'payload'\n\nimport { validateAltText } from '../utilities/mimeTypes.js'\nimport { translatedLabel } from '../utils/translatedLabel.js'\n\nexport function altTextField({\n localized,\n supportedMimeTypes,\n trackedMimeTypes,\n validate,\n}: {\n localized?: TextareaField['localized']\n /** MIME types the resolver can generate for — used to disable the Generate button. */\n supportedMimeTypes?: string[]\n /** MIME types for which alt text is tracked — used to hide the field and skip validation for others. */\n trackedMimeTypes?: string[]\n /** Custom validator that fully replaces the default. */\n validate?: TextareaFieldValidation\n}): TextareaField {\n return {\n name: 'alt',\n type: 'textarea',\n admin: {\n components: {\n Field: '@jhb.software/payload-alt-text-plugin/client#AltTextField',\n },\n custom: {\n supportedMimeTypes,\n trackedMimeTypes,\n },\n },\n label: translatedLabel('alternateText'),\n localized,\n required: true,\n validate: validate ?? ((value, args) => validateAltText(value, args, trackedMimeTypes)),\n }\n}\n"],"names":["validateAltText","translatedLabel","altTextField","localized","supportedMimeTypes","trackedMimeTypes","validate","name","type","admin","components","Field","custom","label","required","value","args"],"mappings":"AAEA,SAASA,eAAe,QAAQ,4BAA2B;AAC3D,SAASC,eAAe,QAAQ,8BAA6B;AAE7D,OAAO,SAASC,aAAa,EAC3BC,SAAS,EACTC,kBAAkB,EAClBC,gBAAgB,EAChBC,QAAQ,EAST;IACC,OAAO;QACLC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,YAAY;gBACVC,OAAO;YACT;YACAC,QAAQ;gBACNR;gBACAC;YACF;QACF;QACAQ,OAAOZ,gBAAgB;QACvBE;QACAW,UAAU;QACVR,UAAUA,YAAa,CAAA,CAACS,OAAOC,OAAShB,gBAAgBe,OAAOC,MAAMX,iBAAgB;IACvF;AACF"}
|
|
@@ -2,6 +2,11 @@ import { revalidateTag } from 'next/cache.js';
|
|
|
2
2
|
import { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealthCollectionTag } from '../utilities/altTextHealth.js';
|
|
3
3
|
function safeRevalidateTag(req, tag) {
|
|
4
4
|
try {
|
|
5
|
+
// Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16
|
|
6
|
+
// added a required second `profile` arg (a 1-arg call still works at runtime, with a
|
|
7
|
+
// deprecation warning). Cast so the build succeeds regardless of which Next types are
|
|
8
|
+
// resolved, while keeping runtime behaviour correct on both versions.
|
|
9
|
+
;
|
|
5
10
|
revalidateTag(tag);
|
|
6
11
|
} catch (error) {
|
|
7
12
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/revalidateAltTextHealth.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'\n\nimport { revalidateTag } from 'next/cache.js'\n\nimport {\n ALT_TEXT_HEALTH_PLUGIN_SLUG,\n getAltTextHealthCollectionTag,\n} from '../utilities/altTextHealth.js'\n\nfunction safeRevalidateTag(req: PayloadRequest, tag: string): void {\n try {\n revalidateTag(tag)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n if (message.includes('static generation store missing')) {\n req.payload.logger.warn({\n msg: 'Skipping alt text health cache revalidation outside a Next.js request context.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n tag,\n })\n return\n }\n\n throw error\n }\n}\n\nexport const createRevalidateAltTextHealthAfterChangeHook =\n (collectionSlug: string): CollectionAfterChangeHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n\nexport const createRevalidateAltTextHealthAfterDeleteHook =\n (collectionSlug: string): CollectionAfterDeleteHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n"],"names":["revalidateTag","ALT_TEXT_HEALTH_PLUGIN_SLUG","getAltTextHealthCollectionTag","safeRevalidateTag","req","tag","error","message","Error","String","includes","payload","logger","warn","msg","plugin","createRevalidateAltTextHealthAfterChangeHook","collectionSlug","doc","context","disableRevalidate","createRevalidateAltTextHealthAfterDeleteHook"],"mappings":"AAEA,SAASA,aAAa,QAAQ,gBAAe;AAE7C,SACEC,2BAA2B,EAC3BC,6BAA6B,QACxB,gCAA+B;AAEtC,SAASC,kBAAkBC,GAAmB,EAAEC,GAAW;IACzD,IAAI;
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/revalidateAltTextHealth.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'\n\nimport { revalidateTag } from 'next/cache.js'\n\nimport {\n ALT_TEXT_HEALTH_PLUGIN_SLUG,\n getAltTextHealthCollectionTag,\n} from '../utilities/altTextHealth.js'\n\nfunction safeRevalidateTag(req: PayloadRequest, tag: string): void {\n try {\n // Support both Next 15 and Next 16. Next 15 types `revalidateTag(tag)` as 1-arg; Next 16\n // added a required second `profile` arg (a 1-arg call still works at runtime, with a\n // deprecation warning). Cast so the build succeeds regardless of which Next types are\n // resolved, while keeping runtime behaviour correct on both versions.\n ;(revalidateTag as (tag: string) => void)(tag)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n if (message.includes('static generation store missing')) {\n req.payload.logger.warn({\n msg: 'Skipping alt text health cache revalidation outside a Next.js request context.',\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n tag,\n })\n return\n }\n\n throw error\n }\n}\n\nexport const createRevalidateAltTextHealthAfterChangeHook =\n (collectionSlug: string): CollectionAfterChangeHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n\nexport const createRevalidateAltTextHealthAfterDeleteHook =\n (collectionSlug: string): CollectionAfterDeleteHook =>\n ({ doc, req }) => {\n if (!req.context?.disableRevalidate) {\n safeRevalidateTag(req, getAltTextHealthCollectionTag(collectionSlug))\n }\n\n return doc\n }\n"],"names":["revalidateTag","ALT_TEXT_HEALTH_PLUGIN_SLUG","getAltTextHealthCollectionTag","safeRevalidateTag","req","tag","error","message","Error","String","includes","payload","logger","warn","msg","plugin","createRevalidateAltTextHealthAfterChangeHook","collectionSlug","doc","context","disableRevalidate","createRevalidateAltTextHealthAfterDeleteHook"],"mappings":"AAEA,SAASA,aAAa,QAAQ,gBAAe;AAE7C,SACEC,2BAA2B,EAC3BC,6BAA6B,QACxB,gCAA+B;AAEtC,SAASC,kBAAkBC,GAAmB,EAAEC,GAAW;IACzD,IAAI;QACF,yFAAyF;QACzF,qFAAqF;QACrF,sFAAsF;QACtF,sEAAsE;;QACpEL,cAAwCK;IAC5C,EAAE,OAAOC,OAAO;QACd,MAAMC,UAAUD,iBAAiBE,QAAQF,MAAMC,OAAO,GAAGE,OAAOH;QAEhE,IAAIC,QAAQG,QAAQ,CAAC,oCAAoC;YACvDN,IAAIO,OAAO,CAACC,MAAM,CAACC,IAAI,CAAC;gBACtBC,KAAK;gBACLC,QAAQd;gBACRI;YACF;YACA;QACF;QAEA,MAAMC;IACR;AACF;AAEA,OAAO,MAAMU,+CACX,CAACC,iBACD,CAAC,EAAEC,GAAG,EAAEd,GAAG,EAAE;QACX,IAAI,CAACA,IAAIe,OAAO,EAAEC,mBAAmB;YACnCjB,kBAAkBC,KAAKF,8BAA8Be;QACvD;QAEA,OAAOC;IACT,EAAC;AAEH,OAAO,MAAMG,+CACX,CAACJ,iBACD,CAAC,EAAEC,GAAG,EAAEd,GAAG,EAAE;QACX,IAAI,CAACA,IAAIe,OAAO,EAAEC,mBAAmB;YACnCjB,kBAAkBC,KAAKF,8BAA8Be;QACvD;QAEA,OAAOC;IACT,EAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { payloadAltTextPlugin } from './plugin.js';
|
|
2
2
|
export { openAIResolver } from './resolvers/openAI.js';
|
|
3
3
|
export * from './resolvers/types.js';
|
|
4
|
-
export type { IncomingAltTextPluginConfig as AltTextPluginConfig } from './types/AltTextPluginConfig.js';
|
|
4
|
+
export type { AltTextCollectionConfig, IncomingAltTextPluginConfig as AltTextPluginConfig, } from './types/AltTextPluginConfig.js';
|
|
5
|
+
export { matchesMimeType, validateAltText } from './utilities/mimeTypes.js';
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport { openAIResolver } from './resolvers/openAI.js'\nexport * from './resolvers/types.js'\nexport type {
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadAltTextPlugin } from './plugin.js'\nexport { openAIResolver } from './resolvers/openAI.js'\nexport * from './resolvers/types.js'\nexport type {\n AltTextCollectionConfig,\n IncomingAltTextPluginConfig as AltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\nexport { matchesMimeType, validateAltText } from './utilities/mimeTypes.js'\n"],"names":["payloadAltTextPlugin","openAIResolver","matchesMimeType","validateAltText"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,cAAa;AAClD,SAASC,cAAc,QAAQ,wBAAuB;AACtD,cAAc,uBAAsB;AAKpC,SAASC,eAAe,EAAEC,eAAe,QAAQ,2BAA0B"}
|
package/dist/plugin.js
CHANGED
|
@@ -5,6 +5,7 @@ import { altTextField } from './fields/altTextField.js';
|
|
|
5
5
|
import { keywordsField } from './fields/keywordsField.js';
|
|
6
6
|
import { createRevalidateAltTextHealthAfterChangeHook, createRevalidateAltTextHealthAfterDeleteHook } from './hooks/revalidateAltTextHealth.js';
|
|
7
7
|
import { translations } from './translations/index.js';
|
|
8
|
+
import { normalizeCollectionsConfig } from './utilities/mimeTypes.js';
|
|
8
9
|
import { deepMergeSimple } from './utils/deepMergeSimple.js';
|
|
9
10
|
const altTextHealthWidgetDefinition = {
|
|
10
11
|
slug: 'alt-text-health',
|
|
@@ -18,36 +19,6 @@ const altTextHealthWidgetDefinition = {
|
|
|
18
19
|
maxWidth: 'full',
|
|
19
20
|
minWidth: 'medium'
|
|
20
21
|
};
|
|
21
|
-
const defaultAltTextHealthWidgetLayout = {
|
|
22
|
-
widgetSlug: 'alt-text-health',
|
|
23
|
-
width: 'full'
|
|
24
|
-
};
|
|
25
|
-
function appendAltTextHealthWidgetToLayout(layout) {
|
|
26
|
-
if (layout.some((widget)=>widget.widgetSlug === 'alt-text-health')) {
|
|
27
|
-
return layout;
|
|
28
|
-
}
|
|
29
|
-
return [
|
|
30
|
-
...layout,
|
|
31
|
-
defaultAltTextHealthWidgetLayout
|
|
32
|
-
];
|
|
33
|
-
}
|
|
34
|
-
function getDashboardDefaultLayout(defaultLayout) {
|
|
35
|
-
if (!defaultLayout) {
|
|
36
|
-
return [
|
|
37
|
-
{
|
|
38
|
-
widgetSlug: 'collections',
|
|
39
|
-
width: 'full'
|
|
40
|
-
},
|
|
41
|
-
defaultAltTextHealthWidgetLayout
|
|
42
|
-
];
|
|
43
|
-
}
|
|
44
|
-
if (Array.isArray(defaultLayout)) {
|
|
45
|
-
return appendAltTextHealthWidgetToLayout(defaultLayout);
|
|
46
|
-
}
|
|
47
|
-
return async ({ req })=>appendAltTextHealthWidgetToLayout(await defaultLayout({
|
|
48
|
-
req
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
22
|
export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
52
23
|
const config = {
|
|
53
24
|
...incomingConfig
|
|
@@ -58,9 +29,10 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
58
29
|
}
|
|
59
30
|
const locales = config.localization ? config.localization.locales.map((localeConfig)=>typeof localeConfig === 'string' ? localeConfig : localeConfig.code) : [];
|
|
60
31
|
const enableHealthCheck = incomingPluginConfig.healthCheck !== false;
|
|
32
|
+
const normalizedCollections = normalizeCollectionsConfig(incomingPluginConfig.collections);
|
|
61
33
|
const pluginConfig = {
|
|
62
34
|
access: incomingPluginConfig.access ?? (({ req })=>!!req.user),
|
|
63
|
-
collections:
|
|
35
|
+
collections: normalizedCollections,
|
|
64
36
|
enabled: incomingPluginConfig.enabled ?? true,
|
|
65
37
|
fieldsOverride: incomingPluginConfig.fieldsOverride,
|
|
66
38
|
getImageThumbnail: incomingPluginConfig.getImageThumbnail,
|
|
@@ -74,27 +46,34 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
74
46
|
if (locales.length === 0 && !incomingPluginConfig.locale) {
|
|
75
47
|
throw new Error('The alt-text plugin requires a "locale" option when Payload localization is disabled. ' + 'Please add { locale: "en" } (or your preferred locale) to your plugin configuration.');
|
|
76
48
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}),
|
|
82
|
-
keywordsField({
|
|
83
|
-
localized: Boolean(config.localization)
|
|
84
|
-
})
|
|
85
|
-
];
|
|
86
|
-
const fields = incomingPluginConfig.fieldsOverride && typeof incomingPluginConfig.fieldsOverride === 'function' ? incomingPluginConfig.fieldsOverride({
|
|
87
|
-
defaultFields
|
|
88
|
-
}) : defaultFields;
|
|
49
|
+
const collectionConfigBySlug = new Map(normalizedCollections.map((entry)=>[
|
|
50
|
+
entry.slug,
|
|
51
|
+
entry
|
|
52
|
+
]));
|
|
89
53
|
// Ensure collections array exists
|
|
90
54
|
config.collections = config.collections || [];
|
|
91
55
|
// Map over collections and inject AI alt text fields into specified ones
|
|
92
56
|
config.collections = config.collections.map((collectionConfig)=>{
|
|
93
|
-
|
|
57
|
+
const altTextCollectionConfig = collectionConfigBySlug.get(collectionConfig.slug);
|
|
58
|
+
if (altTextCollectionConfig) {
|
|
94
59
|
if (!collectionConfig.upload) {
|
|
95
60
|
console.warn(`AI Alt Text Plugin: Collection "${collectionConfig.slug}" is not an upload collection. Skipping field injection.`);
|
|
96
61
|
return collectionConfig;
|
|
97
62
|
}
|
|
63
|
+
const defaultFields = [
|
|
64
|
+
altTextField({
|
|
65
|
+
localized: Boolean(config.localization),
|
|
66
|
+
supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,
|
|
67
|
+
trackedMimeTypes: altTextCollectionConfig.mimeTypes,
|
|
68
|
+
validate: altTextCollectionConfig.validate
|
|
69
|
+
}),
|
|
70
|
+
keywordsField({
|
|
71
|
+
localized: Boolean(config.localization)
|
|
72
|
+
})
|
|
73
|
+
];
|
|
74
|
+
const fields = incomingPluginConfig.fieldsOverride && typeof incomingPluginConfig.fieldsOverride === 'function' ? incomingPluginConfig.fieldsOverride({
|
|
75
|
+
defaultFields
|
|
76
|
+
}) : defaultFields;
|
|
98
77
|
return {
|
|
99
78
|
...collectionConfig,
|
|
100
79
|
admin: {
|
|
@@ -151,9 +130,6 @@ export const payloadAltTextPlugin = (incomingPluginConfig)=>(incomingConfig)=>{
|
|
|
151
130
|
...config.admin,
|
|
152
131
|
dashboard: {
|
|
153
132
|
...config.admin?.dashboard,
|
|
154
|
-
...enableHealthCheck && {
|
|
155
|
-
defaultLayout: getDashboardDefaultLayout(config.admin?.dashboard?.defaultLayout)
|
|
156
|
-
},
|
|
157
133
|
widgets
|
|
158
134
|
}
|
|
159
135
|
},
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, PayloadRequest, Widget, WidgetInstance } from 'payload'\n\nimport type {\n AltTextPluginConfig,\n IncomingAltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\n\nimport { altTextHealthEndpoint } from './endpoints/altTextHealth.js'\nimport { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js'\nimport { generateAltTextEndpoint } from './endpoints/generateAltText.js'\nimport { altTextField } from './fields/altTextField.js'\nimport { keywordsField } from './fields/keywordsField.js'\nimport {\n createRevalidateAltTextHealthAfterChangeHook,\n createRevalidateAltTextHealthAfterDeleteHook,\n} from './hooks/revalidateAltTextHealth.js'\nimport { translations } from './translations/index.js'\nimport { deepMergeSimple } from './utils/deepMergeSimple.js'\n\nconst altTextHealthWidgetDefinition = {\n slug: 'alt-text-health',\n // `Component` was renamed from `ComponentPath` in Payload 3.79.0. Set both for backward compatibility.\n Component: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n ComponentPath: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n label: {\n de: 'Alternativtexte Zustand',\n en: 'Alt text health',\n },\n maxWidth: 'full',\n minWidth: 'medium',\n} satisfies { ComponentPath: string } & Widget\n\ntype DashboardDefaultLayout = Config['admin'] extends infer TAdmin\n ? TAdmin extends { dashboard?: infer TDashboard }\n ? TDashboard extends { defaultLayout?: infer TDefaultLayout }\n ? TDefaultLayout\n : never\n : never\n : never\n\nconst defaultAltTextHealthWidgetLayout: WidgetInstance = {\n widgetSlug: 'alt-text-health',\n width: 'full',\n}\n\nfunction appendAltTextHealthWidgetToLayout(layout: WidgetInstance[]): WidgetInstance[] {\n if (layout.some((widget) => widget.widgetSlug === 'alt-text-health')) {\n return layout\n }\n\n return [...layout, defaultAltTextHealthWidgetLayout]\n}\n\nfunction getDashboardDefaultLayout(defaultLayout: DashboardDefaultLayout | undefined) {\n if (!defaultLayout) {\n return [\n {\n widgetSlug: 'collections',\n width: 'full',\n },\n defaultAltTextHealthWidgetLayout,\n ] satisfies WidgetInstance[]\n }\n\n if (Array.isArray(defaultLayout)) {\n return appendAltTextHealthWidgetToLayout(defaultLayout)\n }\n\n return async ({ req }: { req: PayloadRequest }) =>\n appendAltTextHealthWidgetToLayout(await defaultLayout({ req }))\n}\n\nexport const payloadAltTextPlugin =\n (incomingPluginConfig: IncomingAltTextPluginConfig) =>\n (incomingConfig: Config): Config => {\n const config = { ...incomingConfig }\n\n // If the plugin is disabled, return the config without modifying it\n if (incomingPluginConfig.enabled === false) {\n return config\n }\n\n const locales = config.localization\n ? config.localization.locales.map((localeConfig) =>\n typeof localeConfig === 'string' ? localeConfig : localeConfig.code,\n )\n : []\n\n const enableHealthCheck = incomingPluginConfig.healthCheck !== false\n\n const pluginConfig: AltTextPluginConfig = {\n access: incomingPluginConfig.access ?? (({ req }) => !!req.user),\n collections: incomingPluginConfig.collections,\n enabled: incomingPluginConfig.enabled ?? true,\n fieldsOverride: incomingPluginConfig.fieldsOverride,\n getImageThumbnail: incomingPluginConfig.getImageThumbnail,\n healthCheck: enableHealthCheck,\n locale: incomingPluginConfig.locale,\n locales,\n maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,\n resolver: incomingPluginConfig.resolver,\n }\n\n // Validate locale requirement for non-localized mode\n if (locales.length === 0 && !incomingPluginConfig.locale) {\n throw new Error(\n 'The alt-text plugin requires a \"locale\" option when Payload localization is disabled. ' +\n 'Please add { locale: \"en\" } (or your preferred locale) to your plugin configuration.',\n )\n }\n\n const defaultFields = [\n altTextField({\n localized: Boolean(config.localization),\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n }),\n keywordsField({\n localized: Boolean(config.localization),\n }),\n ]\n\n const fields =\n incomingPluginConfig.fieldsOverride &&\n typeof incomingPluginConfig.fieldsOverride === 'function'\n ? incomingPluginConfig.fieldsOverride({ defaultFields })\n : defaultFields\n\n // Ensure collections array exists\n config.collections = config.collections || []\n\n // Map over collections and inject AI alt text fields into specified ones\n config.collections = config.collections.map((collectionConfig) => {\n if (pluginConfig.collections.includes(collectionConfig.slug)) {\n if (!collectionConfig.upload) {\n console.warn(\n `AI Alt Text Plugin: Collection \"${collectionConfig.slug}\" is not an upload collection. Skipping field injection.`,\n )\n return collectionConfig\n }\n\n return {\n ...collectionConfig,\n admin: {\n ...collectionConfig.admin,\n components: {\n ...(collectionConfig.admin?.components ?? {}),\n // TODO: use the beforeBulkAction custom component slot once available: https://github.com/payloadcms/payload/pull/11719\n beforeListTable: [\n ...(collectionConfig.admin?.components?.beforeListTable ?? []),\n {\n path: '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',\n props: {\n collectionSlug: collectionConfig.slug,\n },\n },\n ],\n },\n // enhance the search by adding the filename, keywords and alt fields (if the user has not provided their own listSearchableFields)\n listSearchableFields: collectionConfig.admin?.listSearchableFields ?? [\n 'filename',\n 'keywords',\n 'alt',\n ],\n },\n fields: [...(collectionConfig.fields ?? []), ...fields],\n hooks: {\n ...collectionConfig.hooks,\n ...(enableHealthCheck && {\n afterChange: [\n ...(collectionConfig.hooks?.afterChange ?? []),\n createRevalidateAltTextHealthAfterChangeHook(collectionConfig.slug),\n ],\n afterDelete: [\n ...(collectionConfig.hooks?.afterDelete ?? []),\n createRevalidateAltTextHealthAfterDeleteHook(collectionConfig.slug),\n ],\n }),\n },\n }\n }\n\n return collectionConfig\n })\n\n const existingWidgets = config.admin?.dashboard?.widgets ?? []\n const widgets =\n !enableHealthCheck || existingWidgets.some((widget) => widget.slug === 'alt-text-health')\n ? existingWidgets\n : [...existingWidgets, altTextHealthWidgetDefinition]\n\n return {\n ...config,\n admin: {\n ...config.admin,\n dashboard: {\n ...config.admin?.dashboard,\n ...(enableHealthCheck && {\n defaultLayout: getDashboardDefaultLayout(config.admin?.dashboard?.defaultLayout),\n }),\n widgets,\n },\n },\n custom: {\n ...config.custom,\n // Make plugin config available in hooks/actions\n altTextPluginConfig: pluginConfig,\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n handler: generateAltTextEndpoint(pluginConfig.access),\n method: 'post',\n path: '/alt-text-plugin/generate',\n },\n {\n handler: bulkGenerateAltTextsEndpoint(pluginConfig.access),\n method: 'post',\n path: '/alt-text-plugin/generate/bulk',\n },\n ...(enableHealthCheck\n ? [\n {\n handler: altTextHealthEndpoint(pluginConfig.access),\n method: 'get' as const,\n path: '/alt-text-plugin/health',\n },\n ]\n : []),\n ],\n i18n: {\n ...config.i18n,\n translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {}),\n },\n }\n }\n"],"names":["altTextHealthEndpoint","bulkGenerateAltTextsEndpoint","generateAltTextEndpoint","altTextField","keywordsField","createRevalidateAltTextHealthAfterChangeHook","createRevalidateAltTextHealthAfterDeleteHook","translations","deepMergeSimple","altTextHealthWidgetDefinition","slug","Component","ComponentPath","label","de","en","maxWidth","minWidth","defaultAltTextHealthWidgetLayout","widgetSlug","width","appendAltTextHealthWidgetToLayout","layout","some","widget","getDashboardDefaultLayout","defaultLayout","Array","isArray","req","payloadAltTextPlugin","incomingPluginConfig","incomingConfig","config","enabled","locales","localization","map","localeConfig","code","enableHealthCheck","healthCheck","pluginConfig","access","user","collections","fieldsOverride","getImageThumbnail","locale","maxBulkGenerateConcurrency","resolver","length","Error","defaultFields","localized","Boolean","supportedMimeTypes","fields","collectionConfig","includes","upload","console","warn","admin","components","beforeListTable","path","props","collectionSlug","listSearchableFields","hooks","afterChange","afterDelete","existingWidgets","dashboard","widgets","custom","altTextPluginConfig","endpoints","handler","method","i18n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Widget } from 'payload'\n\nimport type {\n AltTextPluginConfig,\n IncomingAltTextPluginConfig,\n} from './types/AltTextPluginConfig.js'\n\nimport { altTextHealthEndpoint } from './endpoints/altTextHealth.js'\nimport { bulkGenerateAltTextsEndpoint } from './endpoints/bulkGenerateAltTexts.js'\nimport { generateAltTextEndpoint } from './endpoints/generateAltText.js'\nimport { altTextField } from './fields/altTextField.js'\nimport { keywordsField } from './fields/keywordsField.js'\nimport {\n createRevalidateAltTextHealthAfterChangeHook,\n createRevalidateAltTextHealthAfterDeleteHook,\n} from './hooks/revalidateAltTextHealth.js'\nimport { translations } from './translations/index.js'\nimport { normalizeCollectionsConfig } from './utilities/mimeTypes.js'\nimport { deepMergeSimple } from './utils/deepMergeSimple.js'\n\nconst altTextHealthWidgetDefinition = {\n slug: 'alt-text-health',\n // `Component` was renamed from `ComponentPath` in Payload 3.79.0. Set both for backward compatibility.\n Component: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n ComponentPath: '@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget',\n label: {\n de: 'Alternativtexte Zustand',\n en: 'Alt text health',\n },\n maxWidth: 'full',\n minWidth: 'medium',\n} satisfies { ComponentPath: string } & Widget\n\nexport const payloadAltTextPlugin =\n (incomingPluginConfig: IncomingAltTextPluginConfig) =>\n (incomingConfig: Config): Config => {\n const config = { ...incomingConfig }\n\n // If the plugin is disabled, return the config without modifying it\n if (incomingPluginConfig.enabled === false) {\n return config\n }\n\n const locales = config.localization\n ? config.localization.locales.map((localeConfig) =>\n typeof localeConfig === 'string' ? localeConfig : localeConfig.code,\n )\n : []\n\n const enableHealthCheck = incomingPluginConfig.healthCheck !== false\n\n const normalizedCollections = normalizeCollectionsConfig(incomingPluginConfig.collections)\n\n const pluginConfig: AltTextPluginConfig = {\n access: incomingPluginConfig.access ?? (({ req }) => !!req.user),\n collections: normalizedCollections,\n enabled: incomingPluginConfig.enabled ?? true,\n fieldsOverride: incomingPluginConfig.fieldsOverride,\n getImageThumbnail: incomingPluginConfig.getImageThumbnail,\n healthCheck: enableHealthCheck,\n locale: incomingPluginConfig.locale,\n locales,\n maxBulkGenerateConcurrency: incomingPluginConfig.maxBulkGenerateConcurrency ?? 16,\n resolver: incomingPluginConfig.resolver,\n }\n\n // Validate locale requirement for non-localized mode\n if (locales.length === 0 && !incomingPluginConfig.locale) {\n throw new Error(\n 'The alt-text plugin requires a \"locale\" option when Payload localization is disabled. ' +\n 'Please add { locale: \"en\" } (or your preferred locale) to your plugin configuration.',\n )\n }\n\n const collectionConfigBySlug = new Map<string, (typeof normalizedCollections)[number]>(\n normalizedCollections.map((entry) => [entry.slug, entry]),\n )\n\n // Ensure collections array exists\n config.collections = config.collections || []\n\n // Map over collections and inject AI alt text fields into specified ones\n config.collections = config.collections.map((collectionConfig) => {\n const altTextCollectionConfig = collectionConfigBySlug.get(collectionConfig.slug)\n\n if (altTextCollectionConfig) {\n if (!collectionConfig.upload) {\n console.warn(\n `AI Alt Text Plugin: Collection \"${collectionConfig.slug}\" is not an upload collection. Skipping field injection.`,\n )\n return collectionConfig\n }\n\n const defaultFields = [\n altTextField({\n localized: Boolean(config.localization),\n supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,\n trackedMimeTypes: altTextCollectionConfig.mimeTypes,\n validate: altTextCollectionConfig.validate,\n }),\n keywordsField({\n localized: Boolean(config.localization),\n }),\n ]\n\n const fields =\n incomingPluginConfig.fieldsOverride &&\n typeof incomingPluginConfig.fieldsOverride === 'function'\n ? incomingPluginConfig.fieldsOverride({ defaultFields })\n : defaultFields\n\n return {\n ...collectionConfig,\n admin: {\n ...collectionConfig.admin,\n components: {\n ...(collectionConfig.admin?.components ?? {}),\n // TODO: use the beforeBulkAction custom component slot once available: https://github.com/payloadcms/payload/pull/11719\n beforeListTable: [\n ...(collectionConfig.admin?.components?.beforeListTable ?? []),\n {\n path: '@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton',\n props: {\n collectionSlug: collectionConfig.slug,\n },\n },\n ],\n },\n // enhance the search by adding the filename, keywords and alt fields (if the user has not provided their own listSearchableFields)\n listSearchableFields: collectionConfig.admin?.listSearchableFields ?? [\n 'filename',\n 'keywords',\n 'alt',\n ],\n },\n fields: [...(collectionConfig.fields ?? []), ...fields],\n hooks: {\n ...collectionConfig.hooks,\n ...(enableHealthCheck && {\n afterChange: [\n ...(collectionConfig.hooks?.afterChange ?? []),\n createRevalidateAltTextHealthAfterChangeHook(collectionConfig.slug),\n ],\n afterDelete: [\n ...(collectionConfig.hooks?.afterDelete ?? []),\n createRevalidateAltTextHealthAfterDeleteHook(collectionConfig.slug),\n ],\n }),\n },\n }\n }\n\n return collectionConfig\n })\n\n const existingWidgets = config.admin?.dashboard?.widgets ?? []\n const widgets =\n !enableHealthCheck || existingWidgets.some((widget) => widget.slug === 'alt-text-health')\n ? existingWidgets\n : [...existingWidgets, altTextHealthWidgetDefinition]\n\n return {\n ...config,\n admin: {\n ...config.admin,\n dashboard: {\n ...config.admin?.dashboard,\n widgets,\n },\n },\n custom: {\n ...config.custom,\n // Make plugin config available in hooks/actions\n altTextPluginConfig: pluginConfig,\n },\n endpoints: [\n ...(config.endpoints ?? []),\n {\n handler: generateAltTextEndpoint(pluginConfig.access),\n method: 'post',\n path: '/alt-text-plugin/generate',\n },\n {\n handler: bulkGenerateAltTextsEndpoint(pluginConfig.access),\n method: 'post',\n path: '/alt-text-plugin/generate/bulk',\n },\n ...(enableHealthCheck\n ? [\n {\n handler: altTextHealthEndpoint(pluginConfig.access),\n method: 'get' as const,\n path: '/alt-text-plugin/health',\n },\n ]\n : []),\n ],\n i18n: {\n ...config.i18n,\n translations: deepMergeSimple(translations, incomingConfig.i18n?.translations ?? {}),\n },\n }\n }\n"],"names":["altTextHealthEndpoint","bulkGenerateAltTextsEndpoint","generateAltTextEndpoint","altTextField","keywordsField","createRevalidateAltTextHealthAfterChangeHook","createRevalidateAltTextHealthAfterDeleteHook","translations","normalizeCollectionsConfig","deepMergeSimple","altTextHealthWidgetDefinition","slug","Component","ComponentPath","label","de","en","maxWidth","minWidth","payloadAltTextPlugin","incomingPluginConfig","incomingConfig","config","enabled","locales","localization","map","localeConfig","code","enableHealthCheck","healthCheck","normalizedCollections","collections","pluginConfig","access","req","user","fieldsOverride","getImageThumbnail","locale","maxBulkGenerateConcurrency","resolver","length","Error","collectionConfigBySlug","Map","entry","collectionConfig","altTextCollectionConfig","get","upload","console","warn","defaultFields","localized","Boolean","supportedMimeTypes","trackedMimeTypes","mimeTypes","validate","fields","admin","components","beforeListTable","path","props","collectionSlug","listSearchableFields","hooks","afterChange","afterDelete","existingWidgets","dashboard","widgets","some","widget","custom","altTextPluginConfig","endpoints","handler","method","i18n"],"mappings":"AAOA,SAASA,qBAAqB,QAAQ,+BAA8B;AACpE,SAASC,4BAA4B,QAAQ,sCAAqC;AAClF,SAASC,uBAAuB,QAAQ,iCAAgC;AACxE,SAASC,YAAY,QAAQ,2BAA0B;AACvD,SAASC,aAAa,QAAQ,4BAA2B;AACzD,SACEC,4CAA4C,EAC5CC,4CAA4C,QACvC,qCAAoC;AAC3C,SAASC,YAAY,QAAQ,0BAAyB;AACtD,SAASC,0BAA0B,QAAQ,2BAA0B;AACrE,SAASC,eAAe,QAAQ,6BAA4B;AAE5D,MAAMC,gCAAgC;IACpCC,MAAM;IACN,uGAAuG;IACvGC,WAAW;IACXC,eAAe;IACfC,OAAO;QACLC,IAAI;QACJC,IAAI;IACN;IACAC,UAAU;IACVC,UAAU;AACZ;AAEA,OAAO,MAAMC,uBACX,CAACC,uBACD,CAACC;QACC,MAAMC,SAAS;YAAE,GAAGD,cAAc;QAAC;QAEnC,oEAAoE;QACpE,IAAID,qBAAqBG,OAAO,KAAK,OAAO;YAC1C,OAAOD;QACT;QAEA,MAAME,UAAUF,OAAOG,YAAY,GAC/BH,OAAOG,YAAY,CAACD,OAAO,CAACE,GAAG,CAAC,CAACC,eAC/B,OAAOA,iBAAiB,WAAWA,eAAeA,aAAaC,IAAI,IAErE,EAAE;QAEN,MAAMC,oBAAoBT,qBAAqBU,WAAW,KAAK;QAE/D,MAAMC,wBAAwBvB,2BAA2BY,qBAAqBY,WAAW;QAEzF,MAAMC,eAAoC;YACxCC,QAAQd,qBAAqBc,MAAM,IAAK,CAAA,CAAC,EAAEC,GAAG,EAAE,GAAK,CAAC,CAACA,IAAIC,IAAI,AAAD;YAC9DJ,aAAaD;YACbR,SAASH,qBAAqBG,OAAO,IAAI;YACzCc,gBAAgBjB,qBAAqBiB,cAAc;YACnDC,mBAAmBlB,qBAAqBkB,iBAAiB;YACzDR,aAAaD;YACbU,QAAQnB,qBAAqBmB,MAAM;YACnCf;YACAgB,4BAA4BpB,qBAAqBoB,0BAA0B,IAAI;YAC/EC,UAAUrB,qBAAqBqB,QAAQ;QACzC;QAEA,qDAAqD;QACrD,IAAIjB,QAAQkB,MAAM,KAAK,KAAK,CAACtB,qBAAqBmB,MAAM,EAAE;YACxD,MAAM,IAAII,MACR,2FACE;QAEN;QAEA,MAAMC,yBAAyB,IAAIC,IACjCd,sBAAsBL,GAAG,CAAC,CAACoB,QAAU;gBAACA,MAAMnC,IAAI;gBAAEmC;aAAM;QAG1D,kCAAkC;QAClCxB,OAAOU,WAAW,GAAGV,OAAOU,WAAW,IAAI,EAAE;QAE7C,yEAAyE;QACzEV,OAAOU,WAAW,GAAGV,OAAOU,WAAW,CAACN,GAAG,CAAC,CAACqB;YAC3C,MAAMC,0BAA0BJ,uBAAuBK,GAAG,CAACF,iBAAiBpC,IAAI;YAEhF,IAAIqC,yBAAyB;gBAC3B,IAAI,CAACD,iBAAiBG,MAAM,EAAE;oBAC5BC,QAAQC,IAAI,CACV,CAAC,gCAAgC,EAAEL,iBAAiBpC,IAAI,CAAC,wDAAwD,CAAC;oBAEpH,OAAOoC;gBACT;gBAEA,MAAMM,gBAAgB;oBACpBlD,aAAa;wBACXmD,WAAWC,QAAQjC,OAAOG,YAAY;wBACtC+B,oBAAoBvB,aAAaQ,QAAQ,CAACe,kBAAkB;wBAC5DC,kBAAkBT,wBAAwBU,SAAS;wBACnDC,UAAUX,wBAAwBW,QAAQ;oBAC5C;oBACAvD,cAAc;wBACZkD,WAAWC,QAAQjC,OAAOG,YAAY;oBACxC;iBACD;gBAED,MAAMmC,SACJxC,qBAAqBiB,cAAc,IACnC,OAAOjB,qBAAqBiB,cAAc,KAAK,aAC3CjB,qBAAqBiB,cAAc,CAAC;oBAAEgB;gBAAc,KACpDA;gBAEN,OAAO;oBACL,GAAGN,gBAAgB;oBACnBc,OAAO;wBACL,GAAGd,iBAAiBc,KAAK;wBACzBC,YAAY;4BACV,GAAIf,iBAAiBc,KAAK,EAAEC,cAAc,CAAC,CAAC;4BAC5C,wHAAwH;4BACxHC,iBAAiB;mCACXhB,iBAAiBc,KAAK,EAAEC,YAAYC,mBAAmB,EAAE;gCAC7D;oCACEC,MAAM;oCACNC,OAAO;wCACLC,gBAAgBnB,iBAAiBpC,IAAI;oCACvC;gCACF;6BACD;wBACH;wBACA,mIAAmI;wBACnIwD,sBAAsBpB,iBAAiBc,KAAK,EAAEM,wBAAwB;4BACpE;4BACA;4BACA;yBACD;oBACH;oBACAP,QAAQ;2BAAKb,iBAAiBa,MAAM,IAAI,EAAE;2BAAMA;qBAAO;oBACvDQ,OAAO;wBACL,GAAGrB,iBAAiBqB,KAAK;wBACzB,GAAIvC,qBAAqB;4BACvBwC,aAAa;mCACPtB,iBAAiBqB,KAAK,EAAEC,eAAe,EAAE;gCAC7ChE,6CAA6C0C,iBAAiBpC,IAAI;6BACnE;4BACD2D,aAAa;mCACPvB,iBAAiBqB,KAAK,EAAEE,eAAe,EAAE;gCAC7ChE,6CAA6CyC,iBAAiBpC,IAAI;6BACnE;wBACH,CAAC;oBACH;gBACF;YACF;YAEA,OAAOoC;QACT;QAEA,MAAMwB,kBAAkBjD,OAAOuC,KAAK,EAAEW,WAAWC,WAAW,EAAE;QAC9D,MAAMA,UACJ,CAAC5C,qBAAqB0C,gBAAgBG,IAAI,CAAC,CAACC,SAAWA,OAAOhE,IAAI,KAAK,qBACnE4D,kBACA;eAAIA;YAAiB7D;SAA8B;QAEzD,OAAO;YACL,GAAGY,MAAM;YACTuC,OAAO;gBACL,GAAGvC,OAAOuC,KAAK;gBACfW,WAAW;oBACT,GAAGlD,OAAOuC,KAAK,EAAEW,SAAS;oBAC1BC;gBACF;YACF;YACAG,QAAQ;gBACN,GAAGtD,OAAOsD,MAAM;gBAChB,gDAAgD;gBAChDC,qBAAqB5C;YACvB;YACA6C,WAAW;mBACLxD,OAAOwD,SAAS,IAAI,EAAE;gBAC1B;oBACEC,SAAS7E,wBAAwB+B,aAAaC,MAAM;oBACpD8C,QAAQ;oBACRhB,MAAM;gBACR;gBACA;oBACEe,SAAS9E,6BAA6BgC,aAAaC,MAAM;oBACzD8C,QAAQ;oBACRhB,MAAM;gBACR;mBACInC,oBACA;oBACE;wBACEkD,SAAS/E,sBAAsBiC,aAAaC,MAAM;wBAClD8C,QAAQ;wBACRhB,MAAM;oBACR;iBACD,GACD,EAAE;aACP;YACDiB,MAAM;gBACJ,GAAG3D,OAAO2D,IAAI;gBACd1E,cAAcE,gBAAgBF,cAAcc,eAAe4D,IAAI,EAAE1E,gBAAgB,CAAC;YACpF;QACF;IACF,EAAC"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Field, PayloadRequest } from 'payload';
|
|
2
2
|
import type { AltTextResolver } from '../resolvers/types.js';
|
|
3
|
+
import type { AltTextCollectionConfig, IncomingCollectionsConfig, NormalizedAltTextCollectionConfig } from '../utilities/mimeTypes.js';
|
|
4
|
+
export type { AltTextCollectionConfig, NormalizedAltTextCollectionConfig };
|
|
3
5
|
/** Configuration options for the alt text plugin. */
|
|
4
6
|
export type IncomingAltTextPluginConfig = {
|
|
5
7
|
/**
|
|
@@ -11,8 +13,22 @@ export type IncomingAltTextPluginConfig = {
|
|
|
11
13
|
access?: (args: {
|
|
12
14
|
req: PayloadRequest;
|
|
13
15
|
}) => boolean | Promise<boolean>;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Collections to enable the plugin for.
|
|
18
|
+
*
|
|
19
|
+
* Each entry may be a bare collection slug or an object with a `slug` and an
|
|
20
|
+
* optional `mimeTypes` array restricting which MIME types are tracked,
|
|
21
|
+
* validated, and generated. Bare slugs default to `['image/*']`.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* collections: [
|
|
26
|
+
* 'images', // shorthand — defaults to ['image/*']
|
|
27
|
+
* { slug: 'media', mimeTypes: ['image/*', 'application/pdf'] },
|
|
28
|
+
* ]
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
collections: IncomingCollectionsConfig;
|
|
16
32
|
/** Whether the plugin is enabled. */
|
|
17
33
|
enabled?: boolean;
|
|
18
34
|
/** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */
|
|
@@ -57,8 +73,8 @@ export type AltTextPluginConfig = {
|
|
|
57
73
|
access: (args: {
|
|
58
74
|
req: PayloadRequest;
|
|
59
75
|
}) => boolean | Promise<boolean>;
|
|
60
|
-
/**
|
|
61
|
-
collections:
|
|
76
|
+
/** Collections with resolved MIME type filters. */
|
|
77
|
+
collections: NormalizedAltTextCollectionConfig[];
|
|
62
78
|
/** Whether the plugin is enabled. */
|
|
63
79
|
enabled: boolean;
|
|
64
80
|
/** Override the default fields inserted by the plugin via a function that receives the default fields and returns the new fields */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/types/AltTextPluginConfig.ts"],"sourcesContent":["import type {
|
|
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 * Function to get the thumbnail URL of an image document.\n * This URL will be sent to the LLM for analysis.\n *\n * @remarks\n * - The URL must be publicly accessible so the LLM can fetch it\n * - Use a thumbnail/preview version of the image when possible (e.g. from the sizes field)\n */\n getImageThumbnail: (doc: Record<string, unknown>) => string\n\n /**\n * Enable alt text health tracking (REST endpoint, cache revalidation hooks, and dashboard widget).\n * Set to `false` to disable the entire feature.\n *\n * @default true\n */\n healthCheck?: boolean\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 /** 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 /** Collections with resolved MIME type filters. */\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: (doc: Record<string, unknown>) => string\n\n /** Whether alt text health tracking is enabled. */\n healthCheck: 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 /** The resolver to use for generating alt text */\n resolver: AltTextResolver\n}\n"],"names":[],"mappings":"AAiFA,2EAA2E,GAC3E,WA8BC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { unstable_cache } from 'next/cache.js';
|
|
2
2
|
import { createCachedAltTextHealthScan } from './altTextHealthCache.js';
|
|
3
3
|
import { localesFromConfig } from './localesFromConfig.js';
|
|
4
|
+
import { buildMimeTypeWhere } from './mimeTypes.js';
|
|
4
5
|
import { summarizeCollection } from './summarizeCollection.js';
|
|
5
6
|
export const ALT_TEXT_HEALTH_PLUGIN_SLUG = 'alt-text';
|
|
6
7
|
export const ALT_TEXT_HEALTH_CACHE_TTL = 3600;
|
|
@@ -21,7 +22,11 @@ const createCollectionReadError = (collection, message)=>({
|
|
|
21
22
|
operation: 'find'
|
|
22
23
|
});
|
|
23
24
|
const PAGE_SIZE = 500;
|
|
24
|
-
async function fetchAllDocs(payload, collection, isLocalized) {
|
|
25
|
+
async function fetchAllDocs(payload, collection, isLocalized, mimeTypes) {
|
|
26
|
+
const where = buildMimeTypeWhere(mimeTypes);
|
|
27
|
+
if (!where) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
25
30
|
const docs = [];
|
|
26
31
|
let page = 1;
|
|
27
32
|
let hasMore = true;
|
|
@@ -36,7 +41,8 @@ async function fetchAllDocs(payload, collection, isLocalized) {
|
|
|
36
41
|
page,
|
|
37
42
|
select: {
|
|
38
43
|
alt: true
|
|
39
|
-
}
|
|
44
|
+
},
|
|
45
|
+
where
|
|
40
46
|
});
|
|
41
47
|
for (const doc of result.docs){
|
|
42
48
|
docs.push({
|
|
@@ -50,27 +56,27 @@ async function fetchAllDocs(payload, collection, isLocalized) {
|
|
|
50
56
|
return docs;
|
|
51
57
|
}
|
|
52
58
|
async function computeAltTextHealthScan({ collections, isLocalized, localeCodes, payload }) {
|
|
53
|
-
const collectionSummaries = await Promise.all(collections.map(async (
|
|
59
|
+
const collectionSummaries = await Promise.all(collections.map(async ({ slug, mimeTypes })=>{
|
|
54
60
|
try {
|
|
55
|
-
const docs = await fetchAllDocs(payload,
|
|
61
|
+
const docs = await fetchAllDocs(payload, slug, isLocalized, mimeTypes);
|
|
56
62
|
return summarizeCollection({
|
|
57
|
-
collection,
|
|
63
|
+
collection: slug,
|
|
58
64
|
docs,
|
|
59
65
|
isLocalized,
|
|
60
66
|
localeCodes
|
|
61
67
|
});
|
|
62
68
|
} catch (error) {
|
|
63
69
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
64
|
-
const collectionError = createCollectionReadError(
|
|
70
|
+
const collectionError = createCollectionReadError(slug, message);
|
|
65
71
|
payload.logger.error({
|
|
66
|
-
collection,
|
|
72
|
+
collection: slug,
|
|
67
73
|
err: error,
|
|
68
74
|
msg: 'Alt text health check failed while reading a collection.',
|
|
69
75
|
operation: 'find',
|
|
70
76
|
plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG
|
|
71
77
|
});
|
|
72
78
|
return {
|
|
73
|
-
collection,
|
|
79
|
+
collection: slug,
|
|
74
80
|
completeDocs: 0,
|
|
75
81
|
error: collectionError,
|
|
76
82
|
invalidDocIds: undefined,
|
|
@@ -112,12 +118,14 @@ async function getAltTextHealthScan(req) {
|
|
|
112
118
|
ALT_TEXT_HEALTH_GLOBAL_TAG,
|
|
113
119
|
[
|
|
114
120
|
...collections
|
|
115
|
-
].
|
|
121
|
+
].map(({ slug, mimeTypes })=>`${slug}:${[
|
|
122
|
+
...mimeTypes
|
|
123
|
+
].sort().join('|')}`).sort().join(','),
|
|
116
124
|
localeCodes.join(',')
|
|
117
125
|
];
|
|
118
126
|
const tags = [
|
|
119
127
|
ALT_TEXT_HEALTH_GLOBAL_TAG,
|
|
120
|
-
...new Set(collections.map((
|
|
128
|
+
...new Set(collections.map(({ slug })=>getAltTextHealthCollectionTag(slug)))
|
|
121
129
|
];
|
|
122
130
|
const getCachedHealthScan = createCachedAltTextHealthScan({
|
|
123
131
|
cacheFactory: unstable_cache,
|
|
@@ -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 { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { createCachedAltTextHealthScan } from './altTextHealthCache.js'\nimport { localesFromConfig } from './localesFromConfig.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: string[]\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): Promise<{ alt: unknown; id: number | string }[]> {\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 })\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 (collection): Promise<AltTextHealthScanCollection> => {\n try {\n const docs = await fetchAllDocs(payload, collection, isLocalized)\n\n return summarizeCollection({\n collection,\n docs,\n isLocalized,\n localeCodes,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n const collectionError = createCollectionReadError(collection, message)\n\n payload.logger.error({\n collection,\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,\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].sort().join(','),\n localeCodes.join(','),\n ]\n\n const tags = [\n ALT_TEXT_HEALTH_GLOBAL_TAG,\n ...new Set(collections.map((collection) => getAltTextHealthCollectionTag(collection))),\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\nexport async function getAltTextHealth(req: PayloadRequest): Promise<AltTextHealthScan> {\n return getAltTextHealthScan(req)\n}\n\nexport async function getAltTextHealthWidgetData(\n req: PayloadRequest,\n): Promise<AltTextHealthWidgetData> {\n const scan = await getAltTextHealthScan(req)\n\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"],"names":["unstable_cache","createCachedAltTextHealthScan","localesFromConfig","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","docs","page","hasMore","result","find","depth","fallbackLocale","undefined","limit","locale","overrideAccess","select","alt","doc","push","id","hasNextPage","computeAltTextHealthScan","collectionSummaries","Promise","all","map","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","getAltTextHealth","getAltTextHealthWidgetData","scan","localeCount","length","reduce","total","c"],"mappings":"AAEA,SAASA,cAAc,QAAQ,gBAAe;AAI9C,SAASC,6BAA6B,QAAQ,0BAAyB;AACvE,SAASC,iBAAiB,QAAQ,yBAAwB;AAC1D,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,OAAO,MAAMC,8BAA8B,WAAU;AACrD,OAAO,MAAMC,4BAA4B,KAAI;AAC7C,OAAO,MAAMC,6BAA6B,kBAAiB;AA8C3D,MAAMC,oBAAoB,CAAC,EACzBC,KAAK,EACLC,WAAW,EACXC,WAAW,EAKZ,GAAyB,CAAA;QACxBC,WAAW,IAAIC,OAAOC,WAAW;QACjCC,aAAa,EAAE;QACfC,QAAQ;YAACP;SAAM;QACfC;QACAC;IACF,CAAA;AAEA,MAAMM,4BAA4B,CAACC,YAAoBC,UAAqB,CAAA;QAC1EC,MAAM;QACNF;QACAC;QACAE,WAAW;IACb,CAAA;AAEA,MAAMC,YAAY;AAElB,eAAeC,aACbC,OAAgB,EAChBN,UAAkB,EAClBR,WAAoB;IAEpB,MAAMe,OAAgD,EAAE;IACxD,IAAIC,OAAO;IACX,IAAIC,UAAU;IAEd,MAAOA,QAAS;QACd,MAAMC,SAAS,MAAMJ,QAAQK,IAAI,CAAC;YAChCX;YACAY,OAAO;YACPC,gBAAgBrB,cAAc,QAAQsB;YACtCC,OAAOX;YACPY,QAAQxB,cAAc,QAAQsB;YAC9BG,gBAAgB;YAChBT;YACAU,QAAQ;gBACNC,KAAK;YACP;QACF;QAEA,KAAK,MAAMC,OAAOV,OAAOH,IAAI,CAAE;YAC7BA,KAAKc,IAAI,CAAC;gBACRC,IAAIF,IAAIE,EAAE;gBACVH,KAAK,SAASC,MAAMA,IAAID,GAAG,GAAGL;YAChC;QACF;QAEAL,UAAUC,OAAOa,WAAW;QAC5Bf;IACF;IAEA,OAAOD;AACT;AAEA,eAAeiB,yBAAyB,EACtC3B,WAAW,EACXL,WAAW,EACXC,WAAW,EACXa,OAAO,EACsB;IAC7B,MAAMmB,sBAAsB,MAAMC,QAAQC,GAAG,CAC3C9B,YAAY+B,GAAG,CAAC,OAAO5B;QACrB,IAAI;YACF,MAAMO,OAAO,MAAMF,aAAaC,SAASN,YAAYR;YAErD,OAAON,oBAAoB;gBACzBc;gBACAO;gBACAf;gBACAC;YACF;QACF,EAAE,OAAOF,OAAO;YACd,MAAMU,UAAUV,iBAAiBsC,QAAQtC,MAAMU,OAAO,GAAG;YACzD,MAAM6B,kBAAkB/B,0BAA0BC,YAAYC;YAE9DK,QAAQyB,MAAM,CAACxC,KAAK,CAAC;gBACnBS;gBACAgC,KAAKzC;gBACL0C,KAAK;gBACL9B,WAAW;gBACX+B,QAAQ/C;YACV;YAEA,OAAO;gBACLa;gBACAmC,cAAc;gBACd5C,OAAOuC;gBACPM,eAAetB;gBACfuB,aAAa;gBACbC,aAAa;gBACbC,WAAW;YACb;QACF;IACF;IAGF,MAAMzC,SAAS2B,oBACZe,MAAM,CAAC,CAACC,UAAYA,QAAQlD,KAAK,EACjCqC,GAAG,CAAC,CAACa,UAAYA,QAAQlD,KAAK;IAEjC,OAAO;QACLG,WAAW,IAAIC,OAAOC,WAAW;QACjCC,aAAa4B;QACb3B;QACAN;QACAC;IACF;AACF;AAEA,OAAO,MAAMiD,gCAAgC,CAACC,iBAC5C,GAAGtD,2BAA2B,CAAC,EAAEsD,gBAAgB,CAAA;AAEnD,eAAeC,qBAAqBC,GAAmB;IACrD,MAAM,EAAEvC,OAAO,EAAE,GAAGuC;IACpB,MAAMC,eAAexC,QAAQyC,MAAM,CAACC,MAAM,EAAEC;IAC5C,MAAMxD,cACJR,kBAAkBqB,QAAQyC,MAAM,KAAMD,CAAAA,cAAc9B,SAAS;QAAC8B,aAAa9B,MAAM;KAAC,GAAG,EAAE,AAAD;IACxF,MAAMxB,cAAc0D,QAAQ5C,QAAQyC,MAAM,CAACI,YAAY;IAEvD,IAAI,CAACL,cAAc;QACjB,OAAOxD,kBAAkB;YACvBC,OAAO;gBACLW,MAAM;gBACND,SAAS;YACX;YACAT;YACAC;QACF;IACF;IAEA,MAAMI,cAAciD,aAAajD,WAAW;IAE5C,MAAMuD,gBAAgB;QACpB/D;QACA;eAAIQ;SAAY,CAACwD,IAAI,GAAGC,IAAI,CAAC;QAC7B7D,YAAY6D,IAAI,CAAC;KAClB;IAED,MAAMC,OAAO;QACXlE;WACG,IAAImE,IAAI3D,YAAY+B,GAAG,CAAC,CAAC5B,aAAe0C,8BAA8B1C;KAC1E;IAED,MAAMyD,sBAAsBzE,8BAA8B;QACxD0E,cAAc3E;QACdqE;QACAO,SAAS,UACPnC,yBAAyB;gBACvB3B;gBACAL;gBACAC;gBACAa;YACF;QACFsD,YAAYxE;QACZmE;IACF;IAEA,OAAOE;AACT;AAEA,OAAO,eAAeI,iBAAiBhB,GAAmB;IACxD,OAAOD,qBAAqBC;AAC9B;AAEA,OAAO,eAAeiB,2BACpBjB,GAAmB;IAEnB,MAAMkB,OAAO,MAAMnB,qBAAqBC;IAExC,OAAO;QACLhD,aAAakE,KAAKlE,WAAW;QAC7BC,QAAQiE,KAAKjE,MAAM;QACnBN,aAAauE,KAAKvE,WAAW;QAC7BwE,aAAaD,KAAKtE,WAAW,CAACwE,MAAM;QACpC1B,WAAWwB,KAAKlE,WAAW,CAACqE,MAAM,CAAC,CAACC,OAAOC,IAAMD,QAAQC,EAAE7B,SAAS,EAAE;IACxE;AACF"}
|
|
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\nexport async function getAltTextHealth(req: PayloadRequest): Promise<AltTextHealthScan> {\n return getAltTextHealthScan(req)\n}\n\nexport async function getAltTextHealthWidgetData(\n req: PayloadRequest,\n): Promise<AltTextHealthWidgetData> {\n const scan = await getAltTextHealthScan(req)\n\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"],"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","getAltTextHealth","getAltTextHealthWidgetData","scan","localeCount","length","reduce","total","c"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CollectionSlug, TextareaFieldValidation, Where } from 'payload';
|
|
2
|
+
export declare const DEFAULT_TRACKED_MIME_TYPES: readonly string[];
|
|
3
|
+
export type AltTextCollectionConfig = {
|
|
4
|
+
/**
|
|
5
|
+
* MIME types for which alt text is tracked, validated, and generated in this collection.
|
|
6
|
+
*
|
|
7
|
+
* Accepts exact MIME types (e.g. `image/png`) or wildcards (e.g. `image/*`).
|
|
8
|
+
* For documents whose mime type does not match, the alt text field is hidden,
|
|
9
|
+
* its validation is skipped, and the document is excluded from the health widget.
|
|
10
|
+
*
|
|
11
|
+
* @default ['image/*']
|
|
12
|
+
*/
|
|
13
|
+
mimeTypes?: string[];
|
|
14
|
+
/** Collection slug to enable the plugin for. */
|
|
15
|
+
slug: CollectionSlug;
|
|
16
|
+
/**
|
|
17
|
+
* Custom validate function for the alt text field on this collection.
|
|
18
|
+
* When provided, it fully replaces the default validator (`validateAltText`).
|
|
19
|
+
*
|
|
20
|
+
* Use this to relax or extend the default — for example, to skip the
|
|
21
|
+
* required-alt check when the request body does not touch `alt`
|
|
22
|
+
* (folder moves, partial API updates).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { validateAltText } from '@jhb.software/payload-alt-text-plugin'
|
|
27
|
+
*
|
|
28
|
+
* collections: [
|
|
29
|
+
* {
|
|
30
|
+
* slug: 'media',
|
|
31
|
+
* validate: (value, args) => {
|
|
32
|
+
* const { req } = args
|
|
33
|
+
* if (!req.data || !('alt' in req.data)) return true
|
|
34
|
+
* return validateAltText(value, args)
|
|
35
|
+
* },
|
|
36
|
+
* },
|
|
37
|
+
* ]
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
validate?: TextareaFieldValidation;
|
|
41
|
+
};
|
|
42
|
+
export type NormalizedAltTextCollectionConfig = {
|
|
43
|
+
mimeTypes: string[];
|
|
44
|
+
slug: CollectionSlug;
|
|
45
|
+
validate?: TextareaFieldValidation;
|
|
46
|
+
};
|
|
47
|
+
export type IncomingCollectionsConfig = (AltTextCollectionConfig | CollectionSlug)[];
|
|
48
|
+
export declare function normalizeCollectionsConfig(incoming: IncomingCollectionsConfig): NormalizedAltTextCollectionConfig[];
|
|
49
|
+
export declare function matchesMimeType(mimeType: string, patterns: readonly string[]): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Builds a Payload `where` clause that matches documents whose `mimeType`
|
|
52
|
+
* is in the given list of patterns. Returns `null` when nothing should match
|
|
53
|
+
* (empty patterns), so callers can short-circuit the query.
|
|
54
|
+
*
|
|
55
|
+
* Wildcards like `image/*` are translated to a `like` (case-insensitive
|
|
56
|
+
* substring) match on the prefix (`image/`). For valid MIME types this is
|
|
57
|
+
* equivalent to a prefix match.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildMimeTypeWhere(patterns: readonly string[]): null | Where;
|
|
60
|
+
/**
|
|
61
|
+
* Default validation logic for the alt text field.
|
|
62
|
+
*
|
|
63
|
+
* - Allows an empty value during the initial upload (no regular update has occurred yet).
|
|
64
|
+
* - Allows an empty value when the document's mime type is not tracked for alt text.
|
|
65
|
+
* - Otherwise requires a non-empty value.
|
|
66
|
+
*
|
|
67
|
+
* Projects with stricter or looser requirements can pass a custom function to
|
|
68
|
+
* a collection's `validate` option instead.
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateAltText(value: Parameters<TextareaFieldValidation>[0], args: Parameters<TextareaFieldValidation>[1], trackedMimeTypes?: readonly string[]): string | true;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const DEFAULT_TRACKED_MIME_TYPES = [
|
|
2
|
+
'image/*'
|
|
3
|
+
];
|
|
4
|
+
export function normalizeCollectionsConfig(incoming) {
|
|
5
|
+
return incoming.map((entry)=>{
|
|
6
|
+
if (typeof entry === 'string') {
|
|
7
|
+
return {
|
|
8
|
+
slug: entry,
|
|
9
|
+
mimeTypes: [
|
|
10
|
+
...DEFAULT_TRACKED_MIME_TYPES
|
|
11
|
+
]
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const normalized = {
|
|
15
|
+
slug: entry.slug,
|
|
16
|
+
mimeTypes: entry.mimeTypes ? [
|
|
17
|
+
...entry.mimeTypes
|
|
18
|
+
] : [
|
|
19
|
+
...DEFAULT_TRACKED_MIME_TYPES
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
if (entry.validate) {
|
|
23
|
+
normalized.validate = entry.validate;
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// Payload stores upload mimeType values as the lowercase MIME string (e.g. `image/png`).
|
|
29
|
+
// Pattern comparisons here are case-sensitive; callers should pass lowercase patterns.
|
|
30
|
+
export function matchesMimeType(mimeType, patterns) {
|
|
31
|
+
return patterns.some((pattern)=>{
|
|
32
|
+
if (pattern === mimeType) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (pattern.endsWith('/*')) {
|
|
36
|
+
const prefix = pattern.slice(0, -1);
|
|
37
|
+
return mimeType.startsWith(prefix);
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds a Payload `where` clause that matches documents whose `mimeType`
|
|
44
|
+
* is in the given list of patterns. Returns `null` when nothing should match
|
|
45
|
+
* (empty patterns), so callers can short-circuit the query.
|
|
46
|
+
*
|
|
47
|
+
* Wildcards like `image/*` are translated to a `like` (case-insensitive
|
|
48
|
+
* substring) match on the prefix (`image/`). For valid MIME types this is
|
|
49
|
+
* equivalent to a prefix match.
|
|
50
|
+
*/ export function buildMimeTypeWhere(patterns) {
|
|
51
|
+
if (patterns.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const exacts = [];
|
|
55
|
+
const wildcardPrefixes = [];
|
|
56
|
+
for (const pattern of patterns){
|
|
57
|
+
if (pattern.endsWith('/*')) {
|
|
58
|
+
wildcardPrefixes.push(pattern.slice(0, -1));
|
|
59
|
+
} else {
|
|
60
|
+
exacts.push(pattern);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const clauses = [];
|
|
64
|
+
if (exacts.length > 0) {
|
|
65
|
+
clauses.push({
|
|
66
|
+
mimeType: {
|
|
67
|
+
in: exacts
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
for (const prefix of wildcardPrefixes){
|
|
72
|
+
clauses.push({
|
|
73
|
+
mimeType: {
|
|
74
|
+
like: prefix
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return clauses.length === 1 ? clauses[0] : {
|
|
79
|
+
or: clauses
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Default validation logic for the alt text field.
|
|
84
|
+
*
|
|
85
|
+
* - Allows an empty value during the initial upload (no regular update has occurred yet).
|
|
86
|
+
* - Allows an empty value when the document's mime type is not tracked for alt text.
|
|
87
|
+
* - Otherwise requires a non-empty value.
|
|
88
|
+
*
|
|
89
|
+
* Projects with stricter or looser requirements can pass a custom function to
|
|
90
|
+
* a collection's `validate` option instead.
|
|
91
|
+
*/ export function validateAltText(value, args, trackedMimeTypes) {
|
|
92
|
+
const data = args.data ?? {};
|
|
93
|
+
const { operation, req } = args;
|
|
94
|
+
// Since https://github.com/payloadcms/payload/pull/14988, when using external storage (e.g., S3),
|
|
95
|
+
// it is no longer possible to detect whether this validation runs during the initial upload
|
|
96
|
+
// or a regular update by checking the existence of the ID.
|
|
97
|
+
// Instead, compare the timestamps of the createdAt and updatedAt fields.
|
|
98
|
+
const isInitialUpload = operation === 'create' || 'createdAt' in data && 'updatedAt' in data && data.createdAt === data.updatedAt;
|
|
99
|
+
if (isInitialUpload) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (trackedMimeTypes && trackedMimeTypes.length > 0) {
|
|
103
|
+
const mimeType = typeof data.mimeType === 'string' ? data.mimeType : undefined;
|
|
104
|
+
if (!mimeType || !matchesMimeType(mimeType, trackedMimeTypes)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
109
|
+
// @ts-expect-error - the translation key type does not include the custom key
|
|
110
|
+
return req.t('@jhb.software/payload-alt-text-plugin:theAlternateTextIsRequired');
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//# sourceMappingURL=mimeTypes.js.map
|
|
@@ -0,0 +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"}
|
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.5.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": "https://github.com/jhb-software/payload-plugins",
|
|
@@ -17,28 +17,28 @@
|
|
|
17
17
|
"main": "./dist/index.js",
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"openai": "^6.
|
|
20
|
+
"openai": "^6.34.0",
|
|
21
21
|
"p-map": "^7.0.4",
|
|
22
22
|
"zod": "^4.3.6"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@payloadcms/translations": "^3.
|
|
26
|
-
"@payloadcms/ui": "^3.
|
|
25
|
+
"@payloadcms/translations": "^3.83.0",
|
|
26
|
+
"@payloadcms/ui": "^3.83.0",
|
|
27
27
|
"next": "15.4.11",
|
|
28
|
-
"payload": "^3.
|
|
29
|
-
"react": "19.2.
|
|
30
|
-
"react-dom": "19.2.
|
|
28
|
+
"payload": "^3.83.0",
|
|
29
|
+
"react": "19.2.5",
|
|
30
|
+
"react-dom": "19.2.5"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@payloadcms/eslint-config": "^3.28.0",
|
|
34
|
-
"@payloadcms/translations": "^3.
|
|
34
|
+
"@payloadcms/translations": "^3.83.0",
|
|
35
35
|
"@swc/cli": "^0.8.1",
|
|
36
|
-
"@swc/core": "^1.15.
|
|
36
|
+
"@swc/core": "^1.15.26",
|
|
37
37
|
"@types/react": "19.2.14",
|
|
38
38
|
"@types/react-dom": "19.2.3",
|
|
39
39
|
"copyfiles": "2.4.1",
|
|
40
40
|
"eslint": "^9.0.0",
|
|
41
|
-
"prettier": "^3.8.
|
|
41
|
+
"prettier": "^3.8.3",
|
|
42
42
|
"rimraf": "6.1.3",
|
|
43
43
|
"typescript": "5.9.3"
|
|
44
44
|
},
|
|
@@ -72,9 +72,11 @@
|
|
|
72
72
|
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
73
73
|
"clean": "rimraf --glob {dist,*.tsbuildinfo}",
|
|
74
74
|
"dev": "tsc -w",
|
|
75
|
-
"format": "prettier --write src",
|
|
75
|
+
"format": "prettier --write src \"*.{json,md,js,mjs,cjs}\"",
|
|
76
76
|
"lint": "eslint src",
|
|
77
77
|
"lint:fix": "eslint src --fix",
|
|
78
|
-
"test
|
|
78
|
+
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
79
|
+
"test:health": "node --experimental-strip-types --test test/altTextHealth.test.ts",
|
|
80
|
+
"typecheck": "tsc --noEmit"
|
|
79
81
|
}
|
|
80
82
|
}
|