Skip to content

feat!: add file transform state - #18601

Open
nathanlentz wants to merge 6 commits into
mainfrom
feat/file-transform-state-main
Open

nathanlentz wants to merge 6 commits into
mainfrom
feat/file-transform-state-main

Conversation

@nathanlentz

@nathanlentz nathanlentz commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Uploads can now save crop, focal point, and other edits in _transforms while keeping the original file. Change or clear those edits later and regenerate the output from the original. How neat!

  • Works with stored files, dynamic requests, and file versions. Saved edits run before request-only overrides.
  • All transform keys are optional. State defaults to null, and {} also becomes null.
  • Custom JSON keys need no registration. Built-in shapes stay validated; adapters handle custom validation and operation order.
  • Sharp and the Admin crop/focal editor use the same saved state.

⚠️ Breaking Changes

  • Upload edit API: write crop and focal edits through _transforms. Legacy crop/focal upload-edit query writes are removed; focalX and focalY are read-only compatibility fields.

    // Before
    data: { focalX: 75, focalY: 25 }
    // After
    data: { _transforms: { focalPoint: { x: 75, y: 25 } } }
  • Upload hooks: beforeValidate and beforeChange now run before transforms and see source metadata on new or replacement uploads. Move calculations that need processed bytes into a transformer; use afterChange for final filenames.

  • Dynamic defaults: output metadata stays null until delivery, and logical default filenames have no format extension. Use the response Content-Type for the output format.

  • Custom transformers: update adapters to the new document/source contract. transformFile receives source and originalSource instead of file. Adapters must claim saved keys through handledTransformKeys.

  • Sharp variants: variants derive from the transformed default image. Larger variants may be skipped when enlargement is disabled.

  • Version restoration: saved edits replay through the current adapters, so regenerated bytes may differ from historical output.

_transforms replaces the whole state. Omit it to keep saved edits, or send null to clear them. Replacing the original without supplying state clears the previous edits.

@github-actions

github-actions Bot commented Oct 7, 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 242.21 KB ✅ No change
packages/payload/meta_index.json esbuild/index.js 1.95 MB ⚠️ +23.18 KB (+1.2%)
packages/payload/meta_shared.json esbuild/exports/shared.js 543.02 KB ✅ No change
packages/richtext-lexical/meta_client.json esbuild/exports/client_optimized/index.js 296.99 KB ✅ No change
packages/ui/meta_client.json esbuild/exports/client_optimized/index.js 1.32 MB ⚠️ +1.22 KB (+0.1%)
packages/ui/meta_shared.json esbuild/exports/shared_optimized/index.js 19.14 KB ✅ No change
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.9%, 237.09 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.1%, 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}{ ████████████████▉ }}}$ 67.8%, 1.32 MB
dist/collections/operations ${{\color{Goldenrod}{ ▉ }}}$ 3.6%, 69.66 KB
dist/fields/hooks ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 46.57 KB
dist/uploads/fileVersioning ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 22.51 KB
dist/globals/operations ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 19.40 KB
dist/auth/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 17.10 KB
dist/utilities/configToJSONSchema.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 16.08 KB
dist/uploads/transformers ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 15.68 KB
dist/queues/operations ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 14.33 KB
dist/fields/config ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 13.78 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 11.96 KB
dist/utilities/telemetry ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 11.93 KB
dist/uploads/generateFileData.js ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 11.45 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 10.29 KB
dist/cli/commands ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 9.93 KB
dist/config/orderable ${{\color{Goldenrod}{ ▏ }}}$ 0.5%, 8.78 KB
dist/uploads/fetchAPI-multipart ${{\color{Goldenrod}{ }}}$ 0.4%, 8.39 KB
dist/config/sanitize.js ${{\color{Goldenrod}{ }}}$ 0.4%, 8.17 KB
dist/database/migrations ${{\color{Goldenrod}{ }}}$ 0.4%, 8.16 KB
dist/utilities/entityInputSchema ${{\color{Goldenrod}{ }}}$ 0.4%, 8.02 KB
(other) ${{\color{Goldenrod}{ ████████ }}}$ 32.2%, 625.80 KB

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

Path Size
../../node_modules ${{\color{Goldenrod}{ ██████████████████████ }}}$ 88.2%, 474.44 KB
dist/fields/validations.js ${{\color{Goldenrod}{ ▍ }}}$ 1.9%, 10.26 KB
dist/fields/config ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 5.83 KB
dist/utilities/traverseFields.js ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 4.34 KB
dist/utilities/deepCopyObject.js ${{\color{Goldenrod}{ ▏ }}}$ 0.7%, 3.52 KB
dist/collections/config ${{\color{Goldenrod}{ ▏ }}}$ 0.6%, 3.44 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.58 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%, 957 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/errors/ValidationError.js ${{\color{Goldenrod}{ }}}$ 0.1%, 772 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%, 681 B
(other) ${{\color{Goldenrod}{ ██▉ }}}$ 11.8%, 63.65 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.5%, 42.66 KB
dist/lexical/plugins ${{\color{Goldenrod}{ ██▉ }}}$ 11.7%, 34.51 KB
dist/lexical/ui ${{\color{Goldenrod}{ ██▊ }}}$ 11.4%, 33.54 KB
dist/features/table ${{\color{Goldenrod}{ ██▎ }}}$ 9.3%, 27.46 KB
dist/features/toolbars ${{\color{Goldenrod}{ █▋ }}}$ 6.8%, 19.93 KB
dist/features/link ${{\color{Goldenrod}{ █▌ }}}$ 6.3%, 18.56 KB
dist/features/upload ${{\color{Goldenrod}{ █▏ }}}$ 4.8%, 14.03 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.32 KB
dist/features/converters ${{\color{Goldenrod}{ ▋ }}}$ 2.9%, 8.41 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.7%, 5.14 KB
dist/features/indent ${{\color{Goldenrod}{ ▎ }}}$ 1.4%, 4.19 KB
dist/features/lists ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 3.67 KB
dist/lexical/LexicalEditor.js ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.33 KB
dist/features/horizontalRule ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.29 KB
dist/features/format ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 3.28 KB
dist/field/Field.js ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 3.06 KB
(other) ${{\color{Goldenrod}{ █████████████████████▍ }}}$ 85.5%, 251.12 KB

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

Path Size
../../node_modules ${{\color{Goldenrod}{ ███████▎ }}}$ 29.3%, 382.60 KB
dist/elements/Hierarchy ${{\color{Goldenrod}{ ▉ }}}$ 3.8%, 49.34 KB
dist/elements/BulkUpload ${{\color{Goldenrod}{ ▌ }}}$ 2.3%, 30.54 KB
dist/elements/Table ${{\color{Goldenrod}{ ▌ }}}$ 2.2%, 28.86 KB
dist/views/Version ${{\color{Goldenrod}{ ▌ }}}$ 2.1%, 27.71 KB
dist/views/Edit ${{\color{Goldenrod}{ ▍ }}}$ 1.7%, 22.15 KB
dist/views/HierarchyList ${{\color{Goldenrod}{ ▍ }}}$ 1.6%, 21.53 KB
dist/views/Dashboard ${{\color{Goldenrod}{ ▍ }}}$ 1.5%, 19.60 KB
dist/elements/WhereBuilder ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 17.65 KB
dist/providers/Auth ${{\color{Goldenrod}{ ▎ }}}$ 1.3%, 17.45 KB
dist/fields/Relationship ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 16.23 KB
dist/forms/Form ${{\color{Goldenrod}{ ▎ }}}$ 1.2%, 15.99 KB
dist/fields/Upload ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 14.96 KB
dist/fields/Blocks ${{\color{Goldenrod}{ ▎ }}}$ 1.1%, 14.91 KB
dist/elements/Popup ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.32 KB
dist/views/List ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 13.21 KB
dist/elements/ReactSelect ${{\color{Goldenrod}{ ▎ }}}$ 1.0%, 12.98 KB
dist/elements/PublishButton ${{\color{Goldenrod}{ ▏ }}}$ 0.9%, 11.99 KB
dist/elements/QueryPresets ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.82 KB
dist/widgets/RecentlyViewed ${{\color{Goldenrod}{ ▏ }}}$ 0.8%, 10.04 KB
(other) ${{\color{Goldenrod}{ █████████████████▋ }}}$ 70.7%, 925.26 KB

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

Path Size
dist/graphics/Logo ${{\color{Goldenrod}{ ███████▌ }}}$ 30.4%, 5.61 KB
../../node_modules ${{\color{Goldenrod}{ ███▌ }}}$ 14.4%, 2.65 KB
dist/graphics/Icon ${{\color{Goldenrod}{ ██ }}}$ 8.2%, 1.51 KB
dist/utilities/formatDocTitle ${{\color{Goldenrod}{ █▊ }}}$ 7.1%, 1.32 KB
dist/providers/TableColumns ${{\color{Goldenrod}{ █▏ }}}$ 4.7%, 866 B
dist/utilities/getGlobalData.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 762 B
dist/utilities/api.js ${{\color{Goldenrod}{ █ }}}$ 4.1%, 756 B
dist/utilities/groupNavItems.js ${{\color{Goldenrod}{ █ }}}$ 4.0%, 745 B
dist/utilities/traverseForLocalizedFields.js ${{\color{Goldenrod}{ ▊ }}}$ 3.1%, 570 B
dist/elements/Translation ${{\color{Goldenrod}{ ▋ }}}$ 2.7%, 493 B
dist/utilities/handleTakeOver.js ${{\color{Goldenrod}{ ▌ }}}$ 2.4%, 440 B
dist/elements/withMergedProps ${{\color{Goldenrod}{ ▍ }}}$ 1.8%, 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.2%, 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.6%, 12.84 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.

@nathanlentz
nathanlentz force-pushed the feat/file-transform-state-main branch from ce8c967 to bf5a4f1 Compare October 7, 2026 19:18
@nathanlentz
nathanlentz force-pushed the feat/file-transform-state-main branch from bf5a4f1 to eb62f07 Compare October 7, 2026 20:06

@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.

Written with AI

let replayPipeline: PlannedTransformer[] | undefined
let replaySource: FileSource | undefined

if (!file && retainedOriginal && transformStateWrite.hasChanged) {

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.

Replay transforms for legacy uploads

Replay requires retainedOriginal, but legacy local records do not have an original object. An update can save _transforms without updating the file or its variants. Normalise legacy uploads before generation, or use the verified legacy file as the retained source. Add single and bulk update tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Legacy uploads now use the verified existing file as the original. Added single and bulk update tests, including a missing-file case.

const result = await transformer.handleRequest({
collectionSlug: collection.config.slug,
doc,
getOriginalFile: async () => {

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.

Give each transformer its own original response

The request uses one single-use original getter for every stage. Persisted processing can consume it before a later stage calls getOriginalFile. The source and original also share one abort controller, which can close or leak a response body. Give each stage a fresh getter and track each response with its own controller.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. Each stage gets its own original response and cleanup handling. Added tests for shared streams, failures, and cancellation.

if (state.rotate) {
output = output.rotate(((state.rotate.angle % 360) + 360) % 360)
}
if (state.resize) {

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.

Limit persisted resize dimensions

API-controlled _transforms.resize values reach Sharp without width, height, pixel, or frame limits. A permitted update can cause excessive CPU or memory use during replay or request processing. Apply configurable limits before both resize paths, including derived dimensions and animation frames.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added configurable limits for uploads and requests, including calculated sizes and all animation frames. Defaults are 4096px per side and about 16.8 million total pixels.

const dimensions = await sharpDependency(rotated, constructorOptions).metadata()
const requestedScale = Math.max(
state.resize.width / dimensions.width,
state.resize.height / dimensions.height,

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.

Use the height of each animation frame

Sharp reports the total stacked height for animated images and provides the frame height as pageHeight. This calculation creates an oversized intermediate image and an incorrect crop. Use metadata.pageHeight ?? metadata.height for per-frame calculations. Add animated focal-point pixel and frame-count tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed to use pageHeight when available. Added a test that checks the crop in both frames and preserves frame count and timing.

path: (number | string)[]
previous: Document
}): Promise<void> {
if (!data || typeof data !== 'object') {

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.

Validate required fields inside removed containers

This return stops validation when a transformer removes a group or named tab. Required descendant fields are not checked, so invalid documents can be saved. Continue with an empty current object and force descendant validation when an ancestor changes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. Removing a group or named tab now checks its required fields. Added tests to make sure invalid changes aren’t saved.

return
}
if (isNewFile) {
setTransforms(null)

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.

Keep transforms when replacement is cancelled

Entering replacement mode clears _transforms, but Cancel only hides the replacement controls. Saving another field then removes crop, focal-point, and custom transform data. Clear transforms after a replacement file is selected, or restore the previous state on Cancel. Extend the test to save and verify _transforms.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Cancel now keeps the pending transforms. The test also saves another field afterward and checks that all transform keys survive.

const write = () => updateDocument(updateArgs)
const prepared = await prepareUpdateDocument(updateArgs)
generatedFileData = collectionConfig.upload
? await generateFileData({

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.

Pass the bulk update access mode to validation

This call omits overrideAccess, so final validation always receives false. A trusted bulk Local API update can fail when an equivalent single-document update succeeds. Pass overrideAccess and add a parity test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed....should be required. /s

closeModal(editDrawerSlug)
}

const onDragEnd = React.useCallback(({ x, y }) => {

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.

Clear errors after correction through alternate controls

Dragging or using arrow keys updates the focal point without clearing its input errors. The crop control has the same problem. Valid values can still show an alert and keep Apply disabled. Revalidate or clear the related errors, then test drag and keyboard correction.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. Dragging, arrow keys, and reset now clear the errors they correct. Added coverage for crop and focal controls.

@nathanlentz
nathanlentz marked this pull request as ready for review October 8, 2026 02:14
@nathanlentz

Copy link
Copy Markdown
Collaborator Author

Open to supporting legacy to make this additive for now if we cannot afford the breaking change in beta.

@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.

Written with AI

The current E2E failures reproduce the unnamed filter option and animated WebP encoding changes in both app frameworks.

Comment thread packages/payload/src/uploads/getBaseFields.ts
Comment thread packages/payload/src/uploads/generateFileData.ts
Comment thread packages/payload/src/uploads/transformState/validateTransformState.ts Outdated

const sizeResultFile = await transform({
fieldPath,
file: mainResultFile,

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.

Avoid a second lossy encoding pass for variants

Each variant now reads the encoded main result. Animated WebP files are encoded again, and both upload E2E jobs produce 200380 bytes instead of 211638. Compose the main and variant operations from the retained original, or use a lossless intermediate. Keep a regression test for output quality.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Variants now use bytes from original, keeping the main image's edits

const { data: newFileData, files: filesToUpload } = await generateFileData({
collection,
config,
data = await prepareUploadData({

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.

Keep final upload metadata available to hooks

prepareUploadData gives hooks source metadata, while generateFileData now runs after every before-hook. Hooks that derive fields from dimensions, MIME, filename, or filesize can store values that disagree with the final file. Preserve the former contract, or document the breaking change and provide a migration path with focused tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

To achieve this we are keeping hooks before transforms so they can set _transforms. I elevated the symptom to metadata in the breaking changes of the PR.

// need the whole file, so leave such an upload untouched rather than buffering it.
const canRunTransformers = pipeline.length > 0 && hasFullFileContents(file)
assertTransformCoverage({ pipeline: requestPipeline, state: workingDoc._transforms })
expectedDefaultMimeType = workingDoc.mimeType

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.

Persist the effective identity for request-only conversions

This code copies the source MIME and derives the logical filename from the original extension. A request-only PNG-to-WebP conversion then stores PNG metadata while returning image/webp. Let adapters declare the output identity, or store unknown metadata as null and make routing support it. Add a request-only conversion test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Went with null for output metadata we don't know yet and ditched the extension from logical default filenames.

typeof value !== 'object' ||
seen.has(value) ||
Object.getOwnPropertySymbols(value).length > 0 ||
(Array.isArray(value) && Object.keys(value).length !== value.length) ||

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.

Reject arrays that change during JSON serialisation

This key-count check accepts a sparse array when it also has an extra enumerable property. JSON then fills the hole with null and removes the extra property, so replay receives a different value. Verify that every array key is its canonical numeric index and that every index is present. Add tests for sparse arrays and custom properties.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This has been buttoned up.

This branch has not been deployed

No deployments
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