Skip to content

Identify command form fields and columns by a tamper-resistant marker - #2443

Closed
woksin wants to merge 2 commits into
mainfrom
feat/command-form-marker
Closed

Identify command form fields and columns by a tamper-resistant marker#2443
woksin wants to merge 2 commits into
mainfrom
feat/command-form-marker

Conversation

@woksin

@woksin woksin commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

A command form field or column is now identified by a marker a build transform does not touch, with the legacy displayName kept as a permanent fallback.

Added

  • markAsCommandFormField and markAsCommandFormColumn for marking a hand-rolled field or column, and isCommandFormField and isCommandFormColumn for identifying one, exported from @cratis/arc.react/commands
  • CommandFormMarked, the marker shape, and the CommandFormFieldDisplayName and CommandFormColumnDisplayName constants
  • Documentation on how a field is recognized, on marking a hand-rolled field, and on the Storybook setting that breaks it

Changed

  • CommandForm identifies field and column children through the marker, falling back to displayName, so consumers marking a field by hand are unaffected
  • asCommandFormField, CommandFormField, RadioButtonField, RadioGroupField and the column component set the marker alongside the displayName they already set

Fixed

  • A command form field whose displayName is rewritten by a build transform — such as Storybook's reactDocgen: '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

woksin and others added 2 commits August 4, 2026 12:05
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
Copilot AI lite review requested due to automatic review settings August 4, 2026 10:09
@woksin

woksin commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer context — none of this is release-note material.

Pairs with Cratis/Components#112. That PR carries the consumer half: CommandDialog and CommandStepper read the field marker written here, and CommandDialog.Column writes the column marker read here. Either can ship first — both sides keep the legacy displayName, so a new version of one works against an old version of the other in both directions.

Marker shape. A static isCommandFormField / isCommandFormColumn boolean rather than a Symbol. A plain property needs no cross-package import, so neither package has to know the other's version — which matters because @cratis/components declares this package as a version range, and a named import would be a hard module-link error against any version in that range predating the marker. It is also visible in DevTools and trivial for a consumer to set by hand.

The two packages briefly disagreed, and every gate stayed green. This repository chose the boolean while @cratis/components independently chose Symbol.for. Nothing threw, because both kept the displayName fallback — and that is what hid it: the marker simply stopped crossing the package boundary, so a field whose displayName a build transform had rewritten still bound in a bare CommandForm and silently unbound inside a CommandDialog. The failure the marker was added to prevent, surviving the fix, invisible to every spec in both repositories. It was only findable by reading the other repo.

when_exchanging_marked_components_with_a_consuming_package is the guard that closes it, asserting 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. Renaming either marker now reds it.

The displayName fallback should not get a deprecation path. It is what lets the two independently versioned packages interoperate in both directions, and @cratis/components has a spec that stamps the string on a fake to make it a field.

Verification. yarn ci green in Arc.React — 157 spec files, 398 tests — with lint clean. Mutation-proven rather than asserted: renaming the marker property reds the cross-package spec. Worth noting that vitest does not typecheck, so the specs passed while tsc -b in the build step still failed on a bad cast; green tests alone would have shipped a broken build.

Open for a maintainer: whether a dev-build console.warn for a CommandForm that receives children and recognizes zero fields is wanted. It would have ended the original investigation in one page load, but whether this package wants runtime warnings at all is a house-style call, so it is not included here.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 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: ⚠️ Approved with comments

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 commandFormMarkers utilities (markAs*, is*) plus exported marker/displayName constants.
  • Updated CommandForm and built-in field/column components to write/read markers first, with displayName fallback.
  • 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.

Comment on lines +79 to +83
:::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.
:::
@woksin

woksin commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

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.

@woksin woksin closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants