Skip to content

feat: add sharp file transformer with dynamic image resizing - #18411

Merged
paulpopus merged 10 commits into
feat/file-transformers-corefrom
feat/file-transformers-sharp
Oct 5, 2026
Merged

paulpopus merged 10 commits into
feat/file-transformers-corefrom
feat/file-transformers-sharp

Conversation

@r1tsuu

@r1tsuu r1tsuu commented Sep 30, 2026

Copy link
Copy Markdown
Member

Note

Part 3 of 3 in a stacked PR. See #17827 for the full description, breaking changes and migration.

  1. feat!: add file transformers and migrate upload sizes to variants #17827 — tests, fixtures, test configuration and generated test types
  2. feat!: add support for file transformer adapters #18410 — Payload core, storage adapters, codemod, templates and documentation
  3. This PR — @payloadcms/transformer-sharp, its README, workspace registration and lockfile

This is the top of the stack, where CI is expected to pass.

Scope

  • New @payloadcms/transformer-sharp package and README: upload-time processing (variants, crop, focal point, format/trim/resize options, metadata) and opt-in dynamic resizing
  • Sharp logic moved out of payload (getImageResizeAction, sanitizeResizeConfig, optionallyAppendMetadata)
  • Workspace registration: build:transformer-sharp script, root tsconfig.json reference
  • sharp removed from payload, bumped 0.32.6 → 0.35.4; lockfile

@r1tsuu r1tsuu changed the title feat(transformer-sharp): add sharp file transformer with dynamic image resizing feat: add sharp file transformer with dynamic image resizing Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📦 esbuild Bundle Analysis for payload

This analysis was generated by esbuild-bundle-analyzer. 🤖

Meta File Out File Size (raw) Note
packages/next/meta_index.json esbuild/index.js 216.59 KB 🆕 Added
packages/payload/meta_index.json esbuild/index.js 1.88 MB 🆕 Added
packages/payload/meta_shared.json esbuild/exports/shared.js 563.48 KB 🆕 Added
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 296.14 KB 🆕 Added
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 36.81 KB 🆕 Added
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 18.98 KB 🆕 Added
Largest paths These visualization shows top 20 largest paths in the bundle.

Meta file: packages/next/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ████████████████████████▋ }}}$ 98.8%, 212.07 KB
dist/adapters/router.js ${{\color{Goldenrod}{ }}}$ 0.3%, 718 B
dist/adapters/server.js ${{\color{Goldenrod}{ }}}$ 0.2%, 533 B
dist/adapters/layout.js ${{\color{Goldenrod}{ }}}$ 0.2%, 529 B
dist/adapters/views.js ${{\color{Goldenrod}{ }}}$ 0.2%, 324 B
dist/utilities/initAdminContext.js ${{\color{Goldenrod}{ }}}$ 0.1%, 315 B
dist/utilities/selectiveCache.js ${{\color{Goldenrod}{ }}}$ 0.1%, 263 B
dist/esbuildEntry.js ${{\color{Goldenrod}{ }}}$ 0.0%, 0 B

Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js

Path Size
../../node_modules ${{\color{Goldenrod}{ █████████████████▉ }}}$ 71.7%, 1.34 MB
dist/collections/operations ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 54.67 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 46.14 KB
dist/auth/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 16.95 KB
dist/globals/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 16.18 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 16.02 KB
dist/queues/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 14.31 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 13.65 KB
dist/utilities/telemetry ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 11.88 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 10.98 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 10.82 KB
dist/cli/commands ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 9.92 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 8.76 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ }}}$ 0.4%, 8.38 KB
dist/database/migrations ${{\color{Goldenrod}{ }}}$ 0.4%, 8.16 KB
dist/config/sanitize.js ${{\color{Goldenrod}{ }}}$ 0.4%, 8.01 KB
dist/hierarchy/utils ${{\color{Goldenrod}{ }}}$ 0.4%, 7.66 KB
dist/index.js ${{\color{Goldenrod}{ }}}$ 0.4%, 7.54 KB
dist/auth/strategies ${{\color{Goldenrod}{ }}}$ 0.4%, 7.36 KB
dist/utilities/entityInputSchema ${{\color{Goldenrod}{ }}}$ 0.4%, 7.34 KB
(other) ${{\color{Goldenrod}{ ███████ }}}$ 28.3%, 528.68 KB

Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js

Path Size
../../node_modules ${{\color{Goldenrod}{ ██████████████████████▏ }}}$ 88.8%, 495.80 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 10.79 KB
dist/fields/config ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 5.83 KB
dist/utilities/traverseFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 4.34 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 3.52 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 3.40 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 3.13 KB
dist/fields/baseFields ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 2.79 KB
dist/config/client.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 2.56 KB
dist/auth/cookies.js ${{\color{Goldenrod}{ }}}$ 0.3%, 1.55 KB
dist/utilities/flattenTopLevelFields.js ${{\color{Goldenrod}{ }}}$ 0.3%, 1.42 KB
dist/utilities/getVersionsConfig.js ${{\color{Goldenrod}{ }}}$ 0.2%, 1.04 KB
dist/globals/config ${{\color{Goldenrod}{ }}}$ 0.2%, 939 B
dist/uploads/generatePayloadFileURL.js ${{\color{Goldenrod}{ }}}$ 0.2%, 864 B
dist/utilities/unflatten.js ${{\color{Goldenrod}{ }}}$ 0.2%, 850 B
dist/utilities/flattenAllFields.js ${{\color{Goldenrod}{ }}}$ 0.1%, 798 B
dist/utilities/sanitizeUserDataForEmail.js ${{\color{Goldenrod}{ }}}$ 0.1%, 713 B
dist/auth/extractJWT.js ${{\color{Goldenrod}{ }}}$ 0.1%, 696 B
dist/utilities/getFieldPermissions.js ${{\color{Goldenrod}{ }}}$ 0.1%, 651 B
dist/utilities/fieldPath.js ${{\color{Goldenrod}{ }}}$ 0.1%, 639 B
(other) ${{\color{Goldenrod}{ ██▊ }}}$ 11.2%, 62.77 KB

Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/features/blocks ${{\color{Goldenrod}{ ███▋ }}}$ 14.6%, 42.74 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▉ }}}$ 11.8%, 34.51 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▊ }}}$ 11.4%, 33.54 KB
dist/features/table ${{\color{Goldenrod}{ ██▎ }}}$ 9.4%, 27.46 KB
dist/features/link ${{\color{Goldenrod}{ █▋ }}}$ 6.5%, 18.92 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▌ }}}$ 6.3%, 18.41 KB
dist/features/upload ${{\color{Goldenrod}{ █▏ }}}$ 4.9%, 14.28 KB
dist/features/textState ${{\color{Goldenrod}{ ▉ }}}$ 3.8%, 11.08 KB
dist/lexical/utils ${{\color{Goldenrod}{ ▊ }}}$ 3.4%, 10.02 KB
dist/features/relationship ${{\color{Goldenrod}{ ▊ }}}$ 3.2%, 9.43 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 8.40 KB
dist/utilities/fieldsDrawer ${{\color{Goldenrod}{ ▋ }}}$ 2.8%, 8.12 KB
dist/features/debug ${{\color{Goldenrod}{ ▋ }}}$ 2.5%, 7.40 KB
dist/lexical/config ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 5.14 KB
dist/features/indent ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 4.19 KB
dist/features/lists ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 3.67 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.33 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.28 KB
dist/features/horizontalRule ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.18 KB
dist/field/Field.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 3.05 KB
(other) ${{\color{Goldenrod}{ █████████████████████▎ }}}$ 85.4%, 250.19 KB

Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js

Path Size
dist/exports/client ${{\color{Goldenrod}{ █████████████████████████ }}}$ 100.0%, 27.10 KB

Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ ███████▋ }}}$ 30.7%, 5.61 KB
../../node_modules ${{\color{Goldenrod}{ ███▌ }}}$ 14.5%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██ }}}$ 8.3%, 1.51 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ █▊ }}}$ 7.2%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▏ }}}$ 4.7%, 866 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █ }}}$ 4.2%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 756 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 745 B
dist/elements/Translation ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 440 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▌ }}}$ 2.3%, 419 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 339 B
dist/utilities/getNavGroups.js ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 338 B
dist/utilities/getVisibleEntities.js ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 329 B
dist/elements/WithServerSideProps ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 232 B
dist/layouts/Root ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 230 B
dist/utilities/handleGoBack.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 180 B
dist/fields/mergeFieldStyles.js ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 158 B
dist/forms/Form ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 152 B
dist/utilities/handleBackToDashboard.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 152 B
(other) ${{\color{Goldenrod}{ █████████████████▎ }}}$ 69.3%, 12.68 KB
Details

Next to the size is how much the size has increased or decreased compared with the base branch of this PR.

  • ‼️: Size increased by 20% or more. Special attention should be given to this.
  • ⚠️: Size increased in acceptable range (lower than 20%).
  • ✅: No change or even downsized.
  • 🗑️: The out file is deleted: not found in base branch.
  • 🆕: The out file is newly found: will be added to base branch.

Comment thread packages/transformer-sharp/README.md Outdated
Comment thread packages/transformer-sharp/src/initSharpCollections.ts Outdated
Comment thread packages/transformer-sharp/src/prepareLegacyUpload.ts Outdated

@paulpopus paulpopus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also the question of downgraded pnpm lock file dependencies vs main but lets fix that in the base PR right before it goes back to main to ensure it catches all instances properly

@@ -0,0 +1,69 @@
{
"name": "@payloadcms/transformer-sharp",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@payloadcms/transformer-sharp is absent from packagePublishList. The release tool will not update its version or publish it. Add transformer-sharp after payload and add a completeness test for public packages.

Comment thread packages/transformer-sharp/package.json Outdated
"lint:fix": "eslint . --fix"
},
"dependencies": {
"file-type": "22.0.1",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file-type and sanitize-filename are not imported by this package. Remove both dependencies and regenerate the lockfile.

@r1tsuu
r1tsuu force-pushed the feat/file-transformers-sharp branch from b91074d to 8563a75 Compare October 5, 2026 10:54
@paulpopus
paulpopus merged commit d65d1e5 into feat/file-transformers-core Oct 5, 2026
283 checks passed
@paulpopus
paulpopus deleted the feat/file-transformers-sharp branch October 5, 2026 11:39
paulpopus pushed a commit that referenced this pull request Oct 5, 2026
> [!NOTE]
> Part 2 of 3 in a stacked PR. See #17827 for the full description,
breaking changes and migration.
> 1. #17827 — tests, fixtures, test configuration and generated test
types
> 2. **This PR** — Payload core, storage adapters, codemod, templates
and documentation
> 3. #18411 — `@payloadcms/transformer-sharp`, its README, workspace
registration and lockfile

## Scope

- `payload`: `upload.transformers` pipeline, dynamic file requests
behind `access.read` / `req.fileTransform`, `generatePayloadFileURL`,
removal of the built-in Sharp processing and Sharp-specific config/types
- `@payloadcms/ui`: upload and file manager updates
- Storage adapters and `plugin-cloud-storage`: internal `transform`
file-handler operation
- `@payloadcms/codemod`: `migrate-sharp-to-transformer`
- `create-payload-app` and templates: migrated to `sharpTransformer`
- Docs: `upload/transformers.mdx`, upload and storage adapter docs, v4
migration guide
paulpopus added a commit that referenced this pull request Oct 5, 2026
> [!NOTE]
> Stacked on #18411. Draft.

### What?

Upload documents now store generated image sizes under `variants`
instead of `sizes`, matching the `variants` config option.

```diff
- doc.sizes?.thumbnail?.url
+ doc.variants?.thumbnail?.url
```

### How?

- Renames the stored field and every place that reads or queries it,
across core, the admin UI, the storage plugins, tests, templates and
docs.
- Adds a `sizes-to-variants` migration for each database adapter
(MongoDB, Postgres, Vercel Postgres, SQLite, D1): `payload
migrate:create --file @payloadcms/db-<adapter>/sizes-to-variants`. On
SQL databases it renames the columns in place, so no data is lost.
- Stops the predefined migration before it changes data or indexes when
legacy `sizes` storage conflicts with custom `variants` storage.
- Escapes custom SQL identifiers, makes SQLite index replacement safe to
retry, and preserves and validates MongoDB index definitions, including
partial filters and text-index weights.
- Fixes the `db-postgres` and `db-vercel-postgres` builds, which shipped
`migration-utils` importing a predefined-migration helper that was only
published as `.mjs`, so importing
`@payloadcms/db-postgres/migration-utils` failed.

### Breaking changes

Payload 4 uses `variants` as the top-level field for generated upload
variants. An upload collection can already define a custom field with
this name.

If a collection has a custom `variants` field, rename it before running
the predefined migration:

1. Choose a new field name, such as `customVariants`.
2. Update the collection config to use the new name.
3. Write a custom migration that moves the existing custom data from
`variants` to the new field.
4. Run the predefined `sizes-to-variants` migration after the custom
data has moved.
5. Start Payload 4 only after both migrations succeed.

The predefined migration cannot choose a name or merge custom `variants`
data because Payload cannot determine the intended schema. It checks
MongoDB documents, SQL columns, and SQL snapshots for conflicts and
reports what must be moved before it can continue. A partly completed
migration that contains only expected generated variant fields remains
safe to retry.

### Testing

- `test/sizes-to-variants` covers the migration on MongoDB, SQLite and
Postgres, including camelCase variant names and running inside a
transaction, the way `payload migrate` does.
- Regression coverage includes custom `variants` collisions, quoted SQL
identifiers, SQLite retry and rerun behaviour, MongoDB partial filters
and index options, text indexes, and incompatible destination indexes.
- End to end on blank projects outside the monorepo, upgrading a real
Payload 3.90.2 project to this branch's packed packages:
1. Set up and seed with 3.x (`sharp` + `imageSizes`), using migrations
for SQL.
  2. Upgrade and run the `migrate-sharp-to-transformer` codemod.
  3. Run the `sizes-to-variants` migration.
4. For SQL, generate a plain v4 migration and check it creates v4's new
tables without dropping any `sizes`/`variants` columns.
5. Compare every stored size before and after, run runtime checks, then
roll back with `migrate:down`, migrate up again and compare.

| | Postgres | SQLite | MongoDB |
| ------------------------------- |
-------------------------------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------
|
| Config | drafts + localization, camelCase/webp sizes, custom `dbName`,
a collection without sizes, upload relationship | versions without
drafts, crop/focal point, `generateImageName`, upload in an array,
`hasMany` upload | drafts + 3 locales, `generateImageName`, avif size,
upload in a block, polymorphic `hasMany` upload |
| Documents and versions compared | 15 | 10 | 16 |

Runtime checks after the upgrade cover `where` and `select` on
`variants.*`, generated files on disk, and new uploads. Index names
match the v4 schema.

### Notes

- The `templates/with-vercel-website` initial migration still creates
`sizes_*` columns and needs regenerating.

Written with AI

---------

Co-authored-by: Paul Popus <paul@payloadcms.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants