Identify command form fields and columns by a tamper-resistant marker - #2443
Identify command form fields and columns by a tamper-resistant marker#2443woksin wants to merge 2 commits into
Conversation
A CommandForm child was a field only if component.displayName equaled 'CommandFormField', so any transform that set displayName unbound every field with no error and no warning - the form renders, the input accepts typing, and nothing reaches the command. react-docgen-typescript does exactly that by default, and Storybook selects it through a documented option. Adds an isCommandFormField/isCommandFormColumn static marker, checked first, with the displayName comparison kept as the fallback. The fallback is not on a deprecation path and is the point: it is what lets a version of this package interoperate with a version of a consuming package that knows only the string, in both directions. Removing it would silently unbind every field across that boundary - the exact failure the marker exists to prevent, caused by the fix. Covers the column half too. Fixing only fields would leave columns breakable by the same transform. Shipped as a static property rather than a Symbol. A Symbol resists a name-based transform more thoroughly, but it is a public-surface choice for Cratis to make, and it needs Symbol.for to survive a duplicate install - both reasons to put the question rather than answer it here. Owed, and not mine to do: the three read sites in @cratis/components. Until they move, a field surviving a displayName rewrite is recognized by CommandForm and not by CommandDialog. Nothing regresses in the meantime, because the fallback keeps both directions working. Recount for the record: Arc has 3 field read sites, 1 column read, 4 field stamps and 1 column stamp. The corrected "six field read sites" in the proposal is the total across both repositories.
The marker is a cross-package contract that nothing enforces. @cratis/components reads the field marker written here and writes the column marker read here, and neither imports the other's helper — a consuming package declares this one as a version range, so a named import would be a hard module-link error against any version in that range predating the marker. The contract is therefore carried entirely by two property names being spelled identically in two repositories. That is not hypothetical. The two packages were briefly implemented with different shapes, one a static property and the other a Symbol, and every spec in both repositories passed: both kept the displayName fallback, so nothing threw, and the marker simply stopped crossing the boundary. A field whose displayName a build transform had rewritten bound in a bare CommandForm and silently unbound inside a CommandDialog — the exact failure the marker was added to prevent, surviving the fix and invisible to every gate. Adds the spec that catches it, in both directions: a component marked the way a consuming package marks one, with its displayName then overwritten, is recognized here; and a component marked here exposes the marker under the property name that package reads, with the legacy displayName still set for one that predates the marker. Renaming either marker now reds this spec. Also corrects "recognising" to American English per .ai/rules/general.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRCRgRvMz9N8P8NGwR34MM
|
Reviewer context — none of this is release-note material. Pairs with Cratis/Components#112. That PR carries the consumer half: Marker shape. A static The two packages briefly disagreed, and every gate stayed green. This repository chose the boolean while
The Verification. Open for a maintainer: whether a dev-build No issue references in the description: a search of this repository's issues found nothing matching, and inventing or reusing a number is worse than omitting one. Happy to add them. |
There was a problem hiding this comment.
🟢 Ready to approve
The implementation and specs align with the stated compatibility goal; the only feedback is a small documentation wording correction.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Review result:
This PR hardens CommandForm child detection by introducing explicit, static “marker” properties for command form fields/columns (with displayName retained as a backwards-compatible fallback), addressing cases where build tools rewrite displayName (e.g., Storybook/react-docgen-typescript).
Changes:
- Added
commandFormMarkersutilities (markAs*,is*) plus exported marker/displayName constants. - Updated
CommandFormand built-in field/column components to write/read markers first, withdisplayNamefallback. - Added specs covering displayName rewrite scenarios and cross-package marker shape compatibility, plus docs explaining recognition/hand-marking.
File summaries
| File | Description |
|---|---|
| Source/JavaScript/Arc.React/commands/CommandForm/index.ts | Re-exports marker utilities from the CommandForm barrel. |
| Source/JavaScript/Arc.React/commands/CommandForm/commandFormMarkers.ts | Introduces marker shape, constants, and helpers for marking/identifying fields/columns. |
| Source/JavaScript/Arc.React/commands/CommandForm/CommandForm.tsx | Switches field/column identification from displayName checks to marker-first helpers; marks column component. |
| Source/JavaScript/Arc.React/commands/CommandForm/asCommandFormField.tsx | Marks wrapped fields via the new marker helper instead of setting displayName directly. |
| Source/JavaScript/Arc.React/commands/CommandForm/CommandFormField.tsx | Marks the CommandFormField marker component via the new helper. |
| Source/JavaScript/Arc.React/commands/CommandForm/fields/RadioButtonField.tsx | Marks the field component via the new marker helper. |
| Source/JavaScript/Arc.React/commands/CommandForm/fields/RadioGroupField.tsx | Marks the field component via the new marker helper. |
| Source/JavaScript/Arc.React/commands/CommandForm/for_commandFormMarkers/when_a_transform_has_rewritten_displayName.ts | Adds spec validating marker-based recognition survives displayName rewrites + legacy fallback behavior. |
| Source/JavaScript/Arc.React/commands/CommandForm/for_commandFormMarkers/when_exchanging_marked_components_with_a_consuming_package.ts | Adds cross-package contract spec to prevent drift in marker property names. |
| Documentation/frontend/react/command-form/custom-fields.md | Documents marker-based recognition and how to mark hand-rolled fields. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| :::caution | ||
| **Do not overwrite `displayName` on a command form field**, and do not strip static properties from one. A build transform that does either can unbind the field from its command — the form renders, the input accepts typing, and nothing reaches the command. There is no error and no warning. | ||
|
|
||
| The most likely source is Storybook's `reactDocgen: 'react-docgen-typescript'` setting, whose plugin rewrites `displayName` by default. The `isCommandFormField` marker is there to survive exactly that, so a field keeps working under it — but a transform that removes both loses the binding. | ||
| ::: |
|
Superseded by #2446, which carries this unchanged so the whole set lands as one release rather than four. Closing so it cannot be merged twice — the branch is untouched. |
Summary
A command form field or column is now identified by a marker a build transform does not touch, with the legacy
displayNamekept as a permanent fallback.Added
markAsCommandFormFieldandmarkAsCommandFormColumnfor marking a hand-rolled field or column, andisCommandFormFieldandisCommandFormColumnfor identifying one, exported from@cratis/arc.react/commandsCommandFormMarked, the marker shape, and theCommandFormFieldDisplayNameandCommandFormColumnDisplayNameconstantsChanged
CommandFormidentifies field and column children through the marker, falling back todisplayName, so consumers marking a field by hand are unaffectedasCommandFormField,CommandFormField,RadioButtonField,RadioGroupFieldand the column component set the marker alongside thedisplayNamethey already setFixed
displayNameis rewritten by a build transform — such as Storybook'sreactDocgen: 'react-docgen-typescript', which rewrites it by default — is no longer silently unbound from its command, rendering an input that accepts typing while nothing reaches the command