@payloadcms/codemod 4.0.0-internal.44ac9c8 → 4.0.0-internal.4c2ee12

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.
Files changed (63) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.help.d.ts +4 -0
  4. package/dist/cli.help.d.ts.map +1 -0
  5. package/dist/cli.help.js +29 -0
  6. package/dist/cli.help.js.map +1 -0
  7. package/dist/cli.js +8 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/cli.parseFlags.d.ts +1 -0
  10. package/dist/cli.parseFlags.d.ts.map +1 -1
  11. package/dist/cli.parseFlags.js +6 -0
  12. package/dist/cli.parseFlags.js.map +1 -1
  13. package/dist/registry.d.ts.map +1 -1
  14. package/dist/registry.js +4 -0
  15. package/dist/registry.js.map +1 -1
  16. package/dist/runbook/payload-v4-upgrade.md +2 -2
  17. package/dist/runbook/v4.mdx +284 -10
  18. package/dist/transforms/migrate-admin-context-properties/basic.input.js +32 -0
  19. package/dist/transforms/migrate-admin-context-properties/basic.input.js.map +1 -0
  20. package/dist/transforms/migrate-admin-context-properties/basic.output.js +30 -0
  21. package/dist/transforms/migrate-admin-context-properties/basic.output.js.map +1 -0
  22. package/dist/transforms/migrate-admin-context-properties/index.d.ts +3 -0
  23. package/dist/transforms/migrate-admin-context-properties/index.d.ts.map +1 -0
  24. package/dist/transforms/migrate-admin-context-properties/index.js +302 -0
  25. package/dist/transforms/migrate-admin-context-properties/index.js.map +1 -0
  26. package/dist/transforms/migrate-admin-context-properties/no-match.input.js +30 -0
  27. package/dist/transforms/migrate-admin-context-properties/no-match.input.js.map +1 -0
  28. package/dist/transforms/migrate-admin-context-properties/no-match.output.js +30 -0
  29. package/dist/transforms/migrate-admin-context-properties/no-match.output.js.map +1 -0
  30. package/dist/transforms/migrate-field-component-types/index.d.ts.map +1 -1
  31. package/dist/transforms/migrate-field-component-types/index.js +4 -0
  32. package/dist/transforms/migrate-field-component-types/index.js.map +1 -1
  33. package/dist/transforms/migrate-sharp-to-transformer/basic.output.js +1 -1
  34. package/dist/transforms/migrate-sharp-to-transformer/basic.output.js.map +1 -1
  35. package/dist/transforms/migrate-sharp-to-transformer/existing-transformers.output.js +1 -1
  36. package/dist/transforms/migrate-sharp-to-transformer/existing-transformers.output.js.map +1 -1
  37. package/dist/transforms/migrate-sharp-to-transformer/index.d.ts.map +1 -1
  38. package/dist/transforms/migrate-sharp-to-transformer/index.js +232 -82
  39. package/dist/transforms/migrate-sharp-to-transformer/index.js.map +1 -1
  40. package/dist/transforms/remove-storage-always-insert-fields/index.d.ts.map +1 -1
  41. package/dist/transforms/remove-storage-always-insert-fields/index.js +4 -0
  42. package/dist/transforms/remove-storage-always-insert-fields/index.js.map +1 -1
  43. package/dist/transforms/rename-diff-value-props/basic.input.js +38 -0
  44. package/dist/transforms/rename-diff-value-props/basic.input.js.map +1 -0
  45. package/dist/transforms/rename-diff-value-props/basic.output.js +38 -0
  46. package/dist/transforms/rename-diff-value-props/basic.output.js.map +1 -0
  47. package/dist/transforms/rename-diff-value-props/index.d.ts +3 -0
  48. package/dist/transforms/rename-diff-value-props/index.d.ts.map +1 -0
  49. package/dist/transforms/rename-diff-value-props/index.js +328 -0
  50. package/dist/transforms/rename-diff-value-props/index.js.map +1 -0
  51. package/dist/transforms/rename-diff-value-props/no-match.input.js +8 -0
  52. package/dist/transforms/rename-diff-value-props/no-match.input.js.map +1 -0
  53. package/dist/transforms/rename-experimental-table-feature/index.d.ts.map +1 -1
  54. package/dist/transforms/rename-experimental-table-feature/index.js +7 -1
  55. package/dist/transforms/rename-experimental-table-feature/index.js.map +1 -1
  56. package/dist/upgrade/rewritePackageJson.d.ts.map +1 -1
  57. package/dist/upgrade/rewritePackageJson.js +52 -11
  58. package/dist/upgrade/rewritePackageJson.js.map +1 -1
  59. package/dist/utils/project.d.ts +7 -1
  60. package/dist/utils/project.d.ts.map +1 -1
  61. package/dist/utils/project.js +20 -7
  62. package/dist/utils/project.js.map +1 -1
  63. package/package.json +2 -2
@@ -156,6 +156,31 @@ Run a typecheck after the codemod. Three patterns it cannot resolve:
156
156
 
157
157
  `payload.jobs.queue`, `run`, `runByID`, `cancel`, and `cancelByID` now also default `overrideAccess` to `false`. The option remains optional, but omitting it runs the corresponding `jobs.access.queue`, `jobs.access.run`, or `jobs.access.cancel` function. The codemod adds `overrideAccess: true` to these calls—including zero-argument `payload.jobs.run()` calls—to preserve Payload 3 behavior for review.
158
158
 
159
+ ### On-demand validation adds a `validate` operation
160
+
161
+ The `Operation` type now includes `validate`. The `operation` argument for collection, global, and field `beforeValidate` and `beforeChange` hooks also includes `validate`.
162
+
163
+ On-demand validation runs these hooks without saving the candidate. Update exhaustive operation checks and skip external side effects that should only run during `create` or `update`:
164
+
165
+ ```ts
166
+ import type { CollectionBeforeChangeHook } from 'payload'
167
+
168
+ const syncCustomer: CollectionBeforeChangeHook = async ({
169
+ data,
170
+ operation,
171
+ }) => {
172
+ if (operation === 'validate') {
173
+ return data
174
+ }
175
+
176
+ await syncCustomerWithStripe(data)
177
+
178
+ return data
179
+ }
180
+ ```
181
+
182
+ Sanitized collection and global access types now require a `validate` key. Payload supplies this key during configuration sanitization and uses `update` access by default. Code that constructs a sanitized access object directly must add the key.
183
+
159
184
  ### Collection and Global Access Control callbacks receive `slug`
160
185
 
161
186
  Collection and Global Access Control callback arguments now include the document's `slug`. This additive property is unlikely to affect most projects, but code that manually invokes access callbacks or `executeAccess` must now pass `slug`. Code that validates, enumerates, or compares the exact callback argument shape may also need updating.
@@ -558,6 +583,39 @@ To migrate automatically, there's a codemod for this change available by running
558
583
  npx @payloadcms/codemod --transform migrate-field-component-types
559
584
  ```
560
585
 
586
+ ### Diff component props `comparisonValue` and `versionValue` renamed to `valueFrom` and `valueTo`
587
+
588
+ Custom Diff Components (`admin.components.Diff`) receive the two field values being compared as `valueFrom` and `valueTo`. The `comparisonValue` and `versionValue` props have been removed from `FieldDiffClientProps` and `FieldDiffServerProps`, including every field-specific variant such as `TextFieldDiffClientProps`.
589
+
590
+ | Old prop | New prop | Description |
591
+ | ----------------- | ----------- | ------------------------------------------------ |
592
+ | `comparisonValue` | `valueFrom` | Field value from the version being compared from |
593
+ | `versionValue` | `valueTo` | Field value from the version being compared to |
594
+
595
+ ```diff
596
+ import type { TextFieldDiffClientProps } from 'payload'
597
+ import type React from 'react'
598
+
599
+ export const MyTextDiff: React.FC<TextFieldDiffClientProps> = ({
600
+ - comparisonValue,
601
+ - versionValue,
602
+ + valueFrom,
603
+ + valueTo,
604
+ }) => {
605
+ // ...
606
+ }
607
+ ```
608
+
609
+ #### Codemod
610
+
611
+ To migrate automatically, there's a codemod for this change available by running:
612
+
613
+ ```bash
614
+ npx @payloadcms/codemod --transform rename-diff-value-props
615
+ ```
616
+
617
+ The codemod updates functions whose props are typed with a Diff props or component type imported from `payload` (e.g. `TextFieldDiffClientProps`), or with a type in the same file built from one. It renames destructured props and reads such as `props.comparisonValue`. Untyped components and props passed on to other components (e.g. `<OtherDiff comparisonValue={...} />`) aren't changed automatically; the codemod prints a note for each leftover in files that import a Diff type.
618
+
561
619
  ### `DefaultValue` function variant is now typed to allow returning a `Promise` ([#17522](https://github.com/payloadcms/payload/pull/17522))
562
620
 
563
621
  The exported `DefaultValue` type's function variant previously declared a synchronous-only return type (`SerializableValue`), even though Payload has always awaited the result of a `defaultValue` function at runtime. The type now correctly reflects this and allows `Promise<SerializableValue> | SerializableValue`.
@@ -855,6 +913,49 @@ When a missing locale is already valid for the receiving API, optional chaining
855
913
  const locale = useLocale()?.code
856
914
  ```
857
915
 
916
+ ### `readOnly` removed from the `useField` return type
917
+
918
+ The deprecated `readOnly` property has been removed from `FieldType`, the object returned by `useField`. `useField` already stopped returning it in 3.x, so it was always `undefined` at runtime.
919
+
920
+ Read `readOnly` from your field component's props instead:
921
+
922
+ ````diff
923
+ 'use client'
924
+ import type { TextFieldClientComponent } from 'payload'
925
+ import { useField } from '@payloadcms/ui'
926
+
927
+ -export const MyField: TextFieldClientComponent = ({ path }) => {
928
+ - const { readOnly, value } = useField({ path })
929
+ +export const MyField: TextFieldClientComponent = ({ path, readOnly }) => {
930
+ + const { value } = useField({ path })
931
+
932
+ ### Deprecated `TableColumnsProvider` props removed
933
+
934
+ The following deprecated props have been removed from `TableColumnsProvider` and its `TableColumnsProviderProps` type:
935
+
936
+ - `docs`
937
+ - `enableRowSelections`
938
+ - `listPreferences`
939
+ - `preferenceKey`
940
+ - `renderRowTypes`
941
+ - `setTable`
942
+ - `sortColumnProps`
943
+ - `tableAppearance`
944
+
945
+ `TableColumnsProvider` has ignored these props since 3.26.0, when column state moved to the URL. Remove them where you render the provider. It now only accepts `children`, `collectionSlug`, `columnState`, and `LinkedCellOverride`.
946
+
947
+ ```diff
948
+ <TableColumnsProvider
949
+ collectionSlug={collectionSlug}
950
+ columnState={columnState}
951
+ - enableRowSelections
952
+ - preferenceKey={preferenceKey}
953
+ - tableAppearance="condensed"
954
+ >
955
+ {children}
956
+ </TableColumnsProvider>
957
+ ````
958
+
858
959
  ### RichText adapter `i18n` property removed
859
960
 
860
961
  The `i18n` property on `RichTextAdapter` (and its return type from adapter provider functions) has been removed. It was deprecated in v3 in favour of merging translations directly into `config.i18n.translations` inside the adapter provider.
@@ -916,6 +1017,52 @@ If an affected adapter or feature performs asynchronous work, move that work to
916
1017
 
917
1018
  The `transactionIDPromise` property on `PayloadRequest` has been removed. It was deprecated and unused — `transactionID` (which already supports `Promise<number | string>`) is the correct property to use for both resolved values and pending transactions.
918
1019
 
1020
+ ### `headers` and `languageCode` removed from `AdminContext`
1021
+
1022
+ The `headers` and `languageCode` properties have been removed from `AdminContext`, the object returned by `initAdminContext`. Both duplicated values that are already available on `req`:
1023
+
1024
+ ```diff
1025
+ const context = await initAdminContext(args)
1026
+
1027
+ -const userAgent = context.headers.get('user-agent')
1028
+ +const userAgent = context.req.headers.get('user-agent')
1029
+
1030
+ -const languageCode = context.languageCode
1031
+ +const languageCode = context.req.i18n.language
1032
+ ```
1033
+
1034
+ `req.i18n.language` is typed as `string`. Cast it to `AcceptedLanguages` if you need the narrower type.
1035
+
1036
+ `languageCode` has also been removed from `PartialAdminContext`. Use `i18n.language` instead. `getRootLayoutData` from `@payloadcms/ui` no longer accepts `headers` or `languageCode` and reads both from `req`.
1037
+
1038
+ #### Codemod
1039
+
1040
+ To migrate automatically, run:
1041
+
1042
+ ```bash
1043
+ npx @payloadcms/codemod --transform migrate-admin-context-properties
1044
+ ```
1045
+
1046
+ The codemod rewrites property reads and destructures on `initAdminContext()` results and on parameters or variables typed as `AdminContext` or `PartialAdminContext`. It also updates `Pick<AdminContext, ...>` and `AdminContext['...']` types and drops both arguments from `getRootLayoutData()` calls. Element access such as `context['headers']`, and contexts passed through your own wrapper functions, are left unchanged for manual migration.
1047
+
1048
+ ### `DiffMethod` type removed
1049
+
1050
+ The deprecated `DiffMethod` type has been removed from `payload`. It mirrored an enum from `react-diff-viewer-continued`, which Payload no longer depends on. Remove any imports of `DiffMethod`.
1051
+
1052
+ ### `diffMethod` prop removed from Diff components
1053
+
1054
+ Custom Diff components (`admin.components.Diff`) no longer receive a `diffMethod` prop. It has been removed from `FieldDiffClientProps` and every field-specific variant like `TextFieldDiffClientProps`. Payload always passed the same `'diffWordsWithSpace'` value, so the prop carried no information. Each Diff component handles its own diffing.
1055
+
1056
+ Remove `diffMethod` from your props. If you passed it on to a diffing utility, pass `'diffWordsWithSpace'` directly instead:
1057
+
1058
+ ```diff
1059
+ export const MyTextDiff: React.FC<TextFieldDiffClientProps> = ({
1060
+ - diffMethod,
1061
+ valueFrom,
1062
+ valueTo,
1063
+ }) => {
1064
+ ```
1065
+
919
1066
  ### Collection `afterOperation` hook: `operation: 'read'` removed
920
1067
 
921
1068
  The deprecated `'read'` value for the `operation` argument of collection `afterOperation` hooks has been removed. Payload has not dispatched `afterOperation` with `operation: 'read'` for some time — the `find` and `findByID` operations report their own names (`'find'` and `'findByID'`) instead.
@@ -1025,13 +1172,22 @@ If you use a `declare module` augmentation to extend `GeneratedDatabaseSchema` (
1025
1172
 
1026
1173
  Run `npx @payloadcms/codemod --transform migrate-db-types-subpath` to migrate automatically. The codemod handles import declarations, re-export declarations, and `declare module` augmentations.
1027
1174
 
1175
+ ### `findMigrationDir` removed from `@payloadcms/drizzle`
1176
+
1177
+ The deprecated `findMigrationDir` re-export has been removed from `@payloadcms/drizzle`. Import it from `payload` instead:
1178
+
1179
+ ```diff
1180
+ - import { findMigrationDir } from '@payloadcms/drizzle'
1181
+ + import { findMigrationDir } from 'payload'
1182
+ ```
1183
+
1028
1184
  ### Minimum supported Node.js version is now 24.15.0 ([#16540](https://github.com/payloadcms/payload/pull/16540))
1029
1185
 
1030
1186
  The minimum supported Node.js version is now **24.15.0** (previously 18.20.2). Upgrade your runtime — along with any CI, Docker images, and deployment targets — to the latest node LTS version or newer before upgrading to 4.0.
1031
1187
 
1032
- ### Minimum supported Next.js version is now 16.2.6 ([#16537](https://github.com/payloadcms/payload/pull/16537))
1188
+ ### Minimum supported Next.js version is now 16.4.0 ([#16537](https://github.com/payloadcms/payload/pull/16537))
1033
1189
 
1034
- Older versions of Next.js are no longer supported. Update Next.js to **16.2.6** or newer.
1190
+ Older versions of Next.js are no longer supported. Update Next.js to **16.4.0** or newer.
1035
1191
 
1036
1192
  ### Minimum supported TypeScript version is now 6.0.3 ([#16692](https://github.com/payloadcms/payload/pull/16692))
1037
1193
 
@@ -1087,9 +1243,9 @@ The deprecated `isHTMLElement` utility has been removed from `@payloadcms/richte
1087
1243
 
1088
1244
  `lexical`'s built-in `isHTMLElement` is the canonical implementation and narrows against the node's owning-document `HTMLElement`, so it is also safe across iframes and other realms.
1089
1245
 
1090
- ### Lexical: vendored `@lexical/markdown` fork removed and lexical upgraded to `0.50.0`
1246
+ ### Lexical: vendored `@lexical/markdown` fork removed and lexical upgraded to `0.52.0`
1091
1247
 
1092
- `@payloadcms/richtext-lexical` no longer ships a vendored copy of `@lexical/markdown`. The package is now a direct dependency, every `@lexical/*` package was upgraded from `0.41.0` to `0.50.0`, and `@payloadcms/richtext-lexical/lexical/markdown` is a pure passthrough of `@lexical/markdown`.
1248
+ `@payloadcms/richtext-lexical` no longer ships a vendored copy of `@lexical/markdown`. The package is now a direct dependency, every `@lexical/*` package was upgraded from `0.41.0` to `0.52.0`, and `@payloadcms/richtext-lexical/lexical/markdown` is a pure passthrough of `@lexical/markdown`.
1093
1249
 
1094
1250
  Most projects use markdown through the editor or through `convertMarkdownToLexical` / `convertLexicalToMarkdown` and need no changes. You are only affected if you import from `@payloadcms/richtext-lexical/lexical/markdown` directly.
1095
1251
 
@@ -1113,7 +1269,16 @@ If you keep calling `$convertFromMarkdownString` directly, pass `editorConfig.fe
1113
1269
 
1114
1270
  **Whitespace-only markdown table cells now import as an empty paragraph** instead of a text node of padding spaces. This comes from the lexical upgrade, not Payload's own code. No action is required unless you assert on those padding spaces in stored content.
1115
1271
 
1116
- If you import lexical APIs directly through `@payloadcms/richtext-lexical/lexical/*`, also review the [lexical breaking changes](https://github.com/facebook/lexical/releases) between `0.41.0` and `0.50.0`. None of them require action for typical Payload usage.
1272
+ If you import lexical APIs directly through `@payloadcms/richtext-lexical/lexical/*`, also review the [lexical breaking changes](https://github.com/facebook/lexical/releases) between `0.41.0` and `0.52.0`. None of them require action for typical Payload usage.
1273
+
1274
+ If your app imports Lexical types or adds custom features, check these changes from `0.50.0`. They also apply to imports through `@payloadcms/richtext-lexical/lexical` and its subpaths:
1275
+
1276
+ - Lexical packages are ESM only. Use ESM imports; CommonJS `require()` needs Node.js 20.19 or later.
1277
+ - `SELECTION_CHANGE_COMMAND` now runs before DOM reconciliation. Defer DOM lookups and floating UI positioning with `$onUpdate(() => editor.read('latest', callback))`, or use an update listener for work that should run after every commit.
1278
+ - Call `node.getLatest().exportJSON()` when serializing a node reference retained across an update. Payload continues to write the full JSON format; compact export is not enabled.
1279
+ - `SerializedEditorState<T>`, `SerializedElementNode<T>`, and `SerializedRootNode<T>` no longer accept a type argument. Remove `<T>` if you only need the base node type. For a document with your own node types, use Payload's `TypedEditorState<T>` instead. Without a type argument, Lexical's `children` are still typed as `SerializedLexicalNode[]`.
1280
+ - `ReturnType<Node['exportJSON']>` can now infer partial JSON, making fields such as `version` and `url` optional. If your code expects the full format, use the node's explicit serialized type, such as `SerializedLinkNode`. A normal `node.exportJSON()` call still returns the full format.
1281
+ - `LexicalNode.getCommonAncestor` was removed. Use `$getCommonAncestor(a, b)?.commonAncestor ?? null`; pass the parents of non-element nodes if you need the old method's behavior.
1117
1282
 
1118
1283
  ### Lexical: `EXPERIMENTAL_TableFeature` renamed to `TableFeature`
1119
1284
 
@@ -1131,6 +1296,43 @@ The table feature is no longer experimental, so it drops its prefix.
1131
1296
 
1132
1297
  Stored content is unaffected.
1133
1298
 
1299
+ ### Lexical: `LexicalEditorProps` renamed to `LexicalEditorArgs`
1300
+
1301
+ The `LexicalEditorProps` type exported from `@payloadcms/richtext-lexical` has been renamed to `LexicalEditorArgs`. It describes the arguments of the `lexicalEditor` function, not component props. The shape is unchanged.
1302
+
1303
+ ````diff
1304
+ - import type { LexicalEditorProps } from '@payloadcms/richtext-lexical'
1305
+ + import type { LexicalEditorArgs } from '@payloadcms/richtext-lexical'
1306
+
1307
+ ### Lexical: `UploadDataImproved` replaced by `UploadData` ([#18606](https://github.com/payloadcms/payload/pull/18606))
1308
+
1309
+ The internal `UploadDataImproved` type has been removed, and its definition is now used by the public `UploadData` type. Replace any `UploadDataImproved` imports and type references with `UploadData`:
1310
+
1311
+ ```ts
1312
+ import type { UploadData } from '@payloadcms/richtext-lexical'
1313
+ ````
1314
+
1315
+ ### Lexical: the `version` of nodes is ignored
1316
+
1317
+ Lexical deprecated the `version` property of serialized nodes, and now ignores it when it loads a document. Payload no longer reads it either.
1318
+
1319
+ **Rich text written by the Payload 3.0 betas is no longer upgraded when it's loaded.** Payload used `version: 1` to detect two older shapes:
1320
+
1321
+ - Block nodes whose fields were wrapped in an extra `data` object (`fields.data`)
1322
+ - Relationship and upload nodes that stored the whole related document in `value`, instead of its ID
1323
+
1324
+ If your database may still contain rich text from the 3.0 betas, run `upgradeLexicalData` on the latest 3.x release before you upgrade to 4.0. It re-saves every document, which converts these shapes:
1325
+
1326
+ ```ts
1327
+ import { upgradeLexicalData } from '@payloadcms/richtext-lexical'
1328
+
1329
+ await upgradeLexicalData({ payload })
1330
+ ```
1331
+
1332
+ **`version` is optional in the JSON schema of Lexical nodes.** The CLI and MCP no longer require it, and leave it out of the schema they show agents. They still accept it, so existing documents, which contain `version`, stay valid input.
1333
+
1334
+ **Payload doesn't add or remove `version` when it saves rich text.** Rich text created by Lexical, like in the admin panel, still has `version: 1` on every node, because Lexical's JSON export writes it. Payload's own nodes used to write their own numbers instead (like `2` for blocks), and now write `1` as well. Rich text sent through the API, CLI or MCP is saved as sent, so it may not contain `version`. The generated types still include `version`, marked as deprecated, so don't rely on it being there.
1335
+
1134
1336
  ### Field `typescriptSchema` renamed to `jsonSchema`
1135
1337
 
1136
1338
  `typescriptSchema` accepted JSON Schema, not TypeScript, so the name lied. Renamed to `jsonSchema`. Same shape, same array of transforms. The underlying schema is now also consumed by the MCP plugin for runtime validation, not just type generation.
@@ -1668,6 +1870,8 @@ If you typed `clientUploads.access`, replace `ClientUploadsAccess` with `UploadI
1668
1870
 
1669
1871
  #### If you maintain a custom storage adapter
1670
1872
 
1873
+ File versioning adds a required `GeneratedAdapter.copyFile` method. Copy `from` to `to` (complete storage keys) without changing the source, reject an existing destination, and return only once the destination is readable. `moveFile` stays optional. See [Custom Storage Adapters](../upload/storage-adapters) for the full contract.
1874
+
1671
1875
  Replace `clientUploadContext` with `uploadReference` in generated file metadata and static handler parameters. `handleUpload` no longer receives it because Payload skips uploading files that are already stored.
1672
1876
 
1673
1877
  Set `uploadInstructions.enabled`. When false, Payload adds `adminHandler` to the import map without rendering it. When true, Payload also renders it and exposes the instructions and supporting endpoint.
@@ -1809,11 +2013,13 @@ Run `npx @payloadcms/codemod --transform migrate-azure-chunk-large-files` to rem
1809
2013
 
1810
2014
  Image resizing is no longer built into `payload` itself. It now ships as an official [file transformer](../upload/transformers), `@payloadcms/transformer-sharp`, that you register under `upload.transformers`.
1811
2015
 
1812
- This affects three things:
2016
+ This affects four things:
1813
2017
 
1814
2018
  1. The top-level `sharp` config property has been removed. Passing a Sharp instance now happens through `sharpTransformer({ sharp })` instead.
1815
2019
  2. Per-collection Sharp options — `constructorOptions`, `formatOptions`, `resizeOptions`, `trimOptions`, and `withMetadata` — have been removed from `CollectionConfig['upload']`. They're now authored through `sharpTransformer({ collections })`.
2020
+ - `imageSizes` has also been removed from `CollectionConfig['upload']`. Image sizes are now declared as `variants` on `sharpTransformer({ collections: { <slug>: { variants } } })`. The entries themselves (`name`, `width`, `height`, `generateImageName`, and so on) are unchanged. A collection that still sets `upload.imageSizes` fails the build.
1816
2021
  3. `sharp` is no longer a dependency of `payload` at all (previously a `devDependency`, not a runtime `dependency`). Install `@payloadcms/transformer-sharp` explicitly.
2022
+ 4. The Sharp-specific types `SharpDependency`, `ImageUploadFormatOptions`, `ImageUploadTrimOptions`, and `SharpImageSizeOptions` are no longer exported from `payload`. Import `SharpDependency` from `@payloadcms/transformer-sharp`. For the option types, use `SharpCollectionConfig['formatOptions']`, `SharpCollectionConfig['trimOptions']`, and `NonNullable<SharpCollectionConfig['variants']>[number]` from the same package.
1817
2023
 
1818
2024
  ```sh
1819
2025
  pnpm add @payloadcms/transformer-sharp
@@ -1841,7 +2047,7 @@ pnpm add @payloadcms/transformer-sharp
1841
2047
  + collections: {
1842
2048
  + media: {
1843
2049
  + resizeOptions: { width: 2048, height: 2048 },
1844
- + imageSizes: [{ name: 'thumbnail', width: 400, height: 300 }],
2050
+ + variants: [{ name: 'thumbnail', width: 400, height: 300 }],
1845
2051
  + },
1846
2052
  + },
1847
2053
  + }),
@@ -1850,9 +2056,9 @@ pnpm add @payloadcms/transformer-sharp
1850
2056
  })
1851
2057
  ```
1852
2058
 
1853
- Run `npx @payloadcms/codemod --transform migrate-sharp-to-transformer` to migrate automatically. The codemod moves a top-level `sharp` dependency and inline collection Sharp options into `sharpTransformer({ collections })`, appends to an existing `upload.transformers` array if you already have one, and leaves anything it can't safely rewrite in place with a note — for example, a `collections` array built by a function call, or a collection defined outside the `buildConfig` call. Review those manually.
2059
+ Run `npx @payloadcms/codemod --transform migrate-sharp-to-transformer` to migrate automatically. The codemod moves a top-level `sharp` dependency and inline collection Sharp options — including `crop` and `focalPoint` — into `sharpTransformer({ collections })` (writing `imageSizes` as `variants`), appends to an existing `upload.transformers` array if you already have one, and leaves anything it can't safely rewrite in place with a note — for example, a `collections` array built by a function call, or a collection defined outside the `buildConfig` call. Review those manually. For a config that already registers `sharpTransformer`, it only renames `imageSizes` to `variants` inside the existing `sharpTransformer` call and flags a leftover top-level `sharp` property; it doesn't move any remaining collection options.
1854
2060
 
1855
- `imageSizes`, `crop`, and `focalPoint` are still readable at `collection.upload.imageSizes`/`.crop`/`.focalPoint` — `sharpTransformer` writes a projection of what you configure back onto the sanitized collection at startup, so the Admin Panel, generated types, and the stored `sizes` shape on your documents are unchanged. This write-back is why `crop`/`focalPoint`/`imageSizes` are not in the list of removed options above — only the Sharp-specific processing options moved.
2061
+ Your `variants`, `crop`, and `focalPoint` are readable at `collection.upload.variants`/`.crop`/`.focalPoint` on the **sanitized** config (for example `req.payload.collections.media.config.upload.variants`). Code that read the sanitized `upload.imageSizes` in 3.x must read `upload.variants` instead. `sharpTransformer` writes a projection of what you configure back onto the sanitized collection at startup, so the Admin Panel and the rest of core see the same configuration. For a collection configured on `sharpTransformer`, a `crop` or `focalPoint` set on the transformer replaces the collection's own value; one left unset there keeps the collection's. Neither `imageSizes` nor `variants` can be set on the collection at all.
1856
2062
 
1857
2063
  If you inject a custom Sharp build or version, pass it the same way:
1858
2064
 
@@ -1861,7 +2067,67 @@ If you inject a custom Sharp build or version, pass it the same way:
1861
2067
  + sharpTransformer({ sharp: myCustomSharp })
1862
2068
  ```
1863
2069
 
1864
- `sharpTransformer` can also resize images at request time (`/api/media/file/photo.png?width=400`). This is new in Payload 4 and **disabled by default**, so a migrated config keeps serving only the original file and your pre-generated `imageSizes`, as in Payload 3.x. The codemod doesn't enable it. To opt in, pass `dynamic: true` or `dynamic: { collections: ['media'] }` — see [Dynamic (request-time) resizing](../upload/transformers#dynamic-request-time-resizing), and [Securing dynamic resizing](../upload/transformers#securing-dynamic-resizing) before enabling it on a publicly readable collection.
2070
+ `sharpTransformer` can also resize images at request time (`/api/media/file/photo.png?width=400`). This is new in Payload 4 and **disabled by default**, so a migrated config keeps serving only the original file and your pre-generated `variants`. The codemod doesn't enable it. To opt in, pass `dynamic: true` or `dynamic: { collections: ['media'] }` — see [Dynamic (request-time) resizing](../upload/transformers#dynamic-request-time-resizing), and [Securing dynamic resizing](../upload/transformers#securing-dynamic-resizing) before enabling it on a publicly readable collection.
2071
+
2072
+ #### The stored `sizes` field on upload documents is renamed to `variants`
2073
+
2074
+ The group field that holds each generated image size on an upload document is now `variants` instead of `sizes`. Every place that reads or queries it changes:
2075
+
2076
+ ```diff
2077
+ - doc.sizes?.thumbnail?.url
2078
+ + doc.variants?.thumbnail?.url
2079
+
2080
+ - where: { 'sizes.thumbnail.filename': { equals: 'photo-400x300.png' } }
2081
+ + where: { 'variants.thumbnail.filename': { equals: 'photo-400x300.png' } }
2082
+
2083
+ - select: { sizes: { thumbnail: true } }
2084
+ + select: { variants: { thumbnail: true } }
2085
+ ```
2086
+
2087
+ GraphQL queries select `variants { thumbnail { url } }` instead of `sizes { thumbnail { url } }`, and regenerated types expose `variants?: { ... }` (and `variants?: T | { ... }` on the `*Select` types) in place of `sizes`. Update any frontend code that reads `media.sizes`, including values you pass to helpers such as `getBestFitFromSizes({ sizes: doc.variants })`.
2088
+
2089
+ Existing documents must be migrated. Create the predefined migration for your database adapter and run it:
2090
+
2091
+ <Banner type="warning">
2092
+ Payload 4 uses `variants` as the top-level field for generated upload
2093
+ variants. If an upload collection already defines a custom field named
2094
+ `variants`, rename that field before you run the predefined migration. Choose
2095
+ a new name, update your config, and write a custom migration that moves the
2096
+ existing custom data from `variants` to the new field. Then run the predefined
2097
+ `sizes-to-variants` migration. If legacy `sizes` data or columns and custom
2098
+ `variants` data or columns are both present, the predefined migration stops
2099
+ before it changes data or indexes. It cannot choose a new name or merge this
2100
+ custom data for you.
2101
+ </Banner>
2102
+
2103
+ ```sh
2104
+ # MongoDB
2105
+ payload migrate:create --file @payloadcms/db-mongodb/sizes-to-variants
2106
+ # Postgres
2107
+ payload migrate:create --file @payloadcms/db-postgres/sizes-to-variants
2108
+ # Vercel Postgres
2109
+ payload migrate:create --file @payloadcms/db-vercel-postgres/sizes-to-variants
2110
+ # SQLite
2111
+ payload migrate:create --file @payloadcms/db-sqlite/sizes-to-variants
2112
+ # Cloudflare D1
2113
+ payload migrate:create --file @payloadcms/db-d1-sqlite/sizes-to-variants
2114
+
2115
+ payload migrate
2116
+ ```
2117
+
2118
+ **Postgres and SQLite (including Vercel Postgres and D1):** the columns are renamed in place from `sizes_<name>_<property>` to `variants_<name>_<property>`, on each upload collection's table and its versions table, along with their indexes. Use the predefined migration above **instead of** a plain `payload migrate:create` for this change. A generated migration would drop the `sizes_*` columns and add new `variants_*` columns, losing the stored image size data. The predefined migration also writes the schema snapshot, so the next generated migration starts from the renamed columns.
2119
+
2120
+ On MongoDB the migration renames `sizes` (and `version.sizes` on versions) with `$rename` and re-creates the indexes on those fields under the new path. Run the migration before the upgraded app writes to upload documents.
2121
+
2122
+ #### The file endpoint changes once any transformer is registered
2123
+
2124
+ Registering any transformer under `upload.transformers` — including `sharpTransformer()` with dynamic resizing off — routes every file request through the transformer-aware handler:
2125
+
2126
+ - A file must belong to an upload document to be served. A file that exists in storage without a matching document now returns `404`.
2127
+ - For files whose MIME type a transformer declares (every image, with `sharpTransformer()`), `access.read` can run twice per request: once with `req.fileTransform` set to `true` to decide a possible transform, and once without it for an ordinary read. Keep `access.read` cheap, and use `req.fileTransform` to restrict transformed reads — see [Access control](../upload/transformers#access-control).
2128
+ - Responses produced by a transformer always carry Payload's CORS headers, which `modifyResponseHeaders` can't override.
2129
+
2130
+ To link to a file from your own code, including with transformer query parameters, use the new `generatePayloadFileURL` helper (exported from `payload` and `payload/shared`). It always builds the access-controlled Payload file endpoint, so links never bypass `access.read` by pointing straight at storage.
1865
2131
 
1866
2132
  ### Storage adapters always insert the `prefix` field now — `alwaysInsertFields` removed
1867
2133
 
@@ -1878,6 +2144,12 @@ The `alwaysInsertFields` option (on `cloudStoragePlugin` and every `storage-*` a
1878
2144
  - Remove `alwaysInsertFields` from your storage adapter or `cloudStoragePlugin` config — it has no effect and no longer exists on the option types.
1879
2145
  - For Postgres, SQLite, or D1 projects, run `payload migrate:create` to generate the migration that adds the `prefix` column, then run `payload migrate` to apply it.
1880
2146
 
2147
+ ### Upload collections retain originals and file history
2148
+
2149
+ Upload collections now store `original` metadata and a `prefix` and `_objectKey` for each stored file (the main file, `original`, and each variant), so saved versions can find their files. New uploads keep their source under an `-original` filename. Update any code or infrastructure that relies on the old filename, URL, or object key.
2150
+
2151
+ **Postgres and SQLite:** after updating, run `payload migrate:create`, check that it adds these columns to each upload collection table and its versions table, then run `payload migrate`. Existing rows keep `null` values until their next write.
2152
+
1881
2153
  ### `FileSizeImproved` removed — use `FileSize` ([#16593](https://github.com/payloadcms/payload/pull/16593))
1882
2154
 
1883
2155
  `FileSizeImproved` has been merged into `FileSize`. The `url`, `width`, `height`, `filesize`, `mimeType`, and `filename` properties now accept `null` directly on `FileSize`, matching what the database stores for sizes that were not generated.
@@ -2132,6 +2404,8 @@ By default, MCP items require an authenticated user. Built-in collection and glo
2132
2404
 
2133
2405
  **Dependencies.** The plugin now builds on `@modelcontextprotocol/server` instead of `@modelcontextprotocol/sdk` + `mcp-handler`. It no longer depends on Zod directly; its built-in tools use the Zod Mini export from `payload`. If your custom tools use regular Zod, install Zod v4 in your project and import it directly.
2134
2406
 
2407
+ **HTTP requests have a size limit.** `POST /api/mcp` now answers request bodies over 4 MiB with `413`. Base64 file uploads count toward this limit. Raise it with `mcp.maxRequestBodySize`.
2408
+
2135
2409
  ### `@payloadcms/ui/scss` export removed and legacy `--theme-*` tokens retired
2136
2410
 
2137
2411
  The Admin UI styles were migrated from SCSS to plain CSS. The `@payloadcms/ui/scss` package export and its SCSS source files have been removed, and the legacy `--theme-*` CSS variables were retired in favor of semantic `--color-*` tokens and the raw `--ramp-*` color palette.
@@ -0,0 +1,32 @@
1
+ import { getRootLayoutData } from '@payloadcms/ui/layouts/Root/getRootLayoutData';
2
+ import { initAdminContext } from 'payload/internal';
3
+ export async function getLayoutData(args) {
4
+ const context = await initAdminContext(args);
5
+ const { cookies, headers, languageCode, permissions, req, user } = context;
6
+ const data = await getRootLayoutData({
7
+ cookies,
8
+ headers,
9
+ importMap: args.importMap,
10
+ languageCode,
11
+ permissions,
12
+ req,
13
+ user
14
+ });
15
+ return {
16
+ data,
17
+ userAgent: context.headers.get('user-agent')
18
+ };
19
+ }
20
+ export async function getTheme(args) {
21
+ const { headers: requestHeaders, languageCode, user } = await initAdminContext(args);
22
+ return {
23
+ languageCode,
24
+ theme: requestHeaders.get('sec-ch-prefers-color-scheme'),
25
+ user
26
+ };
27
+ }
28
+ export const getUserAgent = async (args)=>(await initAdminContext(args)).headers.get('user-agent');
29
+ export const getDirection = (context)=>context.languageCode === 'ar' ? 'rtl' : 'ltr';
30
+ export const getAcceptLanguage = ({ headers })=>headers.get('accept-language');
31
+
32
+ //# sourceMappingURL=basic.input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/transforms/migrate-admin-context-properties/basic.input.ts"],"sourcesContent":["import type { AdminContext } from 'payload'\nimport type { InitAdminContextArgs } from 'payload/internal'\n\nimport { getRootLayoutData } from '@payloadcms/ui/layouts/Root/getRootLayoutData'\nimport { initAdminContext } from 'payload/internal'\n\ntype LayoutArgs = Pick<AdminContext, 'cookies' | 'headers' | 'languageCode' | 'req'>\n\ntype LanguageCode = AdminContext['languageCode']\n\nexport async function getLayoutData(args: InitAdminContextArgs) {\n const context = await initAdminContext(args)\n\n const { cookies, headers, languageCode, permissions, req, user } = context\n\n const data = await getRootLayoutData({\n cookies,\n headers,\n importMap: args.importMap,\n languageCode,\n permissions,\n req,\n user,\n })\n\n return { data, userAgent: context.headers.get('user-agent') }\n}\n\nexport async function getTheme(args: InitAdminContextArgs) {\n const { headers: requestHeaders, languageCode, user } = await initAdminContext(args)\n\n return { languageCode, theme: requestHeaders.get('sec-ch-prefers-color-scheme'), user }\n}\n\nexport const getUserAgent = async (args: InitAdminContextArgs) =>\n (await initAdminContext(args)).headers.get('user-agent')\n\nexport const getDirection = (context: AdminContext) =>\n context.languageCode === 'ar' ? 'rtl' : 'ltr'\n\nexport const getAcceptLanguage = ({ headers }: AdminContext) => headers.get('accept-language')\n"],"names":["getRootLayoutData","initAdminContext","getLayoutData","args","context","cookies","headers","languageCode","permissions","req","user","data","importMap","userAgent","get","getTheme","requestHeaders","theme","getUserAgent","getDirection","getAcceptLanguage"],"mappings":"AAGA,SAASA,iBAAiB,QAAQ,gDAA+C;AACjF,SAASC,gBAAgB,QAAQ,mBAAkB;AAMnD,OAAO,eAAeC,cAAcC,IAA0B;IAC5D,MAAMC,UAAU,MAAMH,iBAAiBE;IAEvC,MAAM,EAAEE,OAAO,EAAEC,OAAO,EAAEC,YAAY,EAAEC,WAAW,EAAEC,GAAG,EAAEC,IAAI,EAAE,GAAGN;IAEnE,MAAMO,OAAO,MAAMX,kBAAkB;QACnCK;QACAC;QACAM,WAAWT,KAAKS,SAAS;QACzBL;QACAC;QACAC;QACAC;IACF;IAEA,OAAO;QAAEC;QAAME,WAAWT,QAAQE,OAAO,CAACQ,GAAG,CAAC;IAAc;AAC9D;AAEA,OAAO,eAAeC,SAASZ,IAA0B;IACvD,MAAM,EAAEG,SAASU,cAAc,EAAET,YAAY,EAAEG,IAAI,EAAE,GAAG,MAAMT,iBAAiBE;IAE/E,OAAO;QAAEI;QAAcU,OAAOD,eAAeF,GAAG,CAAC;QAAgCJ;IAAK;AACxF;AAEA,OAAO,MAAMQ,eAAe,OAAOf,OACjC,AAAC,CAAA,MAAMF,iBAAiBE,KAAI,EAAGG,OAAO,CAACQ,GAAG,CAAC,cAAa;AAE1D,OAAO,MAAMK,eAAe,CAACf,UAC3BA,QAAQG,YAAY,KAAK,OAAO,QAAQ,MAAK;AAE/C,OAAO,MAAMa,oBAAoB,CAAC,EAAEd,OAAO,EAAgB,GAAKA,QAAQQ,GAAG,CAAC,mBAAkB"}
@@ -0,0 +1,30 @@
1
+ import { getRootLayoutData } from '@payloadcms/ui/layouts/Root/getRootLayoutData';
2
+ import { initAdminContext } from 'payload/internal';
3
+ export async function getLayoutData(args) {
4
+ const context = await initAdminContext(args);
5
+ const { cookies, permissions, req, user } = context;
6
+ const data = await getRootLayoutData({
7
+ cookies,
8
+ importMap: args.importMap,
9
+ permissions,
10
+ req,
11
+ user
12
+ });
13
+ return {
14
+ data,
15
+ userAgent: context.req.headers.get('user-agent')
16
+ };
17
+ }
18
+ export async function getTheme(args) {
19
+ const { req: { headers: requestHeaders, i18n: { language: languageCode } }, user } = await initAdminContext(args);
20
+ return {
21
+ languageCode,
22
+ theme: requestHeaders.get('sec-ch-prefers-color-scheme'),
23
+ user
24
+ };
25
+ }
26
+ export const getUserAgent = async (args)=>(await initAdminContext(args)).req.headers.get('user-agent');
27
+ export const getDirection = (context)=>context.req.i18n.language === 'ar' ? 'rtl' : 'ltr';
28
+ export const getAcceptLanguage = ({ req: { headers } })=>headers.get('accept-language');
29
+
30
+ //# sourceMappingURL=basic.output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/transforms/migrate-admin-context-properties/basic.output.ts"],"sourcesContent":["import type { AdminContext } from 'payload'\nimport type { InitAdminContextArgs } from 'payload/internal'\n\nimport { getRootLayoutData } from '@payloadcms/ui/layouts/Root/getRootLayoutData'\nimport { initAdminContext } from 'payload/internal'\n\ntype LayoutArgs = Pick<AdminContext, 'cookies' | 'req'>\n\ntype LanguageCode = AdminContext['req']['i18n']['language']\n\nexport async function getLayoutData(args: InitAdminContextArgs) {\n const context = await initAdminContext(args)\n\n const { cookies, permissions, req, user } = context\n\n const data = await getRootLayoutData({\n cookies,\n importMap: args.importMap,\n permissions,\n req,\n user,\n })\n\n return { data, userAgent: context.req.headers.get('user-agent') }\n}\n\nexport async function getTheme(args: InitAdminContextArgs) {\n const { req: { headers: requestHeaders, i18n: { language: languageCode } }, user } = await initAdminContext(args)\n\n return { languageCode, theme: requestHeaders.get('sec-ch-prefers-color-scheme'), user }\n}\n\nexport const getUserAgent = async (args: InitAdminContextArgs) =>\n (await initAdminContext(args)).req.headers.get('user-agent')\n\nexport const getDirection = (context: AdminContext) =>\n context.req.i18n.language === 'ar' ? 'rtl' : 'ltr'\n\nexport const getAcceptLanguage = ({ req: { headers } }: AdminContext) => headers.get('accept-language')\n"],"names":["getRootLayoutData","initAdminContext","getLayoutData","args","context","cookies","permissions","req","user","data","importMap","userAgent","headers","get","getTheme","requestHeaders","i18n","language","languageCode","theme","getUserAgent","getDirection","getAcceptLanguage"],"mappings":"AAGA,SAASA,iBAAiB,QAAQ,gDAA+C;AACjF,SAASC,gBAAgB,QAAQ,mBAAkB;AAMnD,OAAO,eAAeC,cAAcC,IAA0B;IAC5D,MAAMC,UAAU,MAAMH,iBAAiBE;IAEvC,MAAM,EAAEE,OAAO,EAAEC,WAAW,EAAEC,GAAG,EAAEC,IAAI,EAAE,GAAGJ;IAE5C,MAAMK,OAAO,MAAMT,kBAAkB;QACnCK;QACAK,WAAWP,KAAKO,SAAS;QACzBJ;QACAC;QACAC;IACF;IAEA,OAAO;QAAEC;QAAME,WAAWP,QAAQG,GAAG,CAACK,OAAO,CAACC,GAAG,CAAC;IAAc;AAClE;AAEA,OAAO,eAAeC,SAASX,IAA0B;IACvD,MAAM,EAAEI,KAAK,EAAEK,SAASG,cAAc,EAAEC,MAAM,EAAEC,UAAUC,YAAY,EAAE,EAAE,EAAEV,IAAI,EAAE,GAAG,MAAMP,iBAAiBE;IAE5G,OAAO;QAAEe;QAAcC,OAAOJ,eAAeF,GAAG,CAAC;QAAgCL;IAAK;AACxF;AAEA,OAAO,MAAMY,eAAe,OAAOjB,OACjC,AAAC,CAAA,MAAMF,iBAAiBE,KAAI,EAAGI,GAAG,CAACK,OAAO,CAACC,GAAG,CAAC,cAAa;AAE9D,OAAO,MAAMQ,eAAe,CAACjB,UAC3BA,QAAQG,GAAG,CAACS,IAAI,CAACC,QAAQ,KAAK,OAAO,QAAQ,MAAK;AAEpD,OAAO,MAAMK,oBAAoB,CAAC,EAAEf,KAAK,EAAEK,OAAO,EAAE,EAAgB,GAAKA,QAAQC,GAAG,CAAC,mBAAkB"}
@@ -0,0 +1,3 @@
1
+ import type { Transform } from '../../types.js';
2
+ export declare const migrateAdminContextProperties: Transform;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transforms/migrate-admin-context-properties/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAwC/C,eAAO,MAAM,6BAA6B,EAAE,SA8J3C,CAAA"}