Aalexbit-codemod

sentry-form-migration

Migrate JsonForm/FormModel to TanStack form system

transformationmigration
Public
11 executions
Run locally
npx codemod sentry-form-migration
Documentation

Sentry Form Migration Codemod

Two-step migration from JsonForm/FormModel to the TanStack form system (useScrapsForm, AutoSaveField).

See .agents/skills/migrate-frontend-forms/SKILL.md for the full migration guide.

Two-Step Workflow

Step 1 (this codemod) applies safe, deterministic edits and leaves comments/markers so Step 2 (AI) can find each legacy form and finish the TanStack migration. The graph, globs, and the full AI instructions live in workflow.yaml (and prompts/form-migration-ai.md mirrors the AI-facing text—keep both in sync when editing instructions).

Scope: test files and explicit targets

workflow.yaml excludes **/*.spec.*, **/*.test.*, and **/*.stories.* from Step 1 when scanning directories (e.g. -t ./static). Default runs skip unit/integration tests and stories.

If you pass -t to a single file path (e.g. a *.spec.tsx), the CLI may still process that file—Step 1 can add markers to the spec. Prefer migrating production form code first (*.tsx under views/components, not *.spec.*). For tests, Step 2 expects a test harness pattern (useScrapsForm + form.AppField) when wrapping custom controls—not the same checklist as production (AutoSaveField, FormSearch, extract-form-fields). See Pick the migration context in workflow.yaml Step 2 or prompts/form-migration-ai.md.

Step 1 — Deterministic (js-ast-grep)

Mechanical transformations, safe to run:

  1. Add hintText — For field config objects that have help but not hintText, adds hintText with the same value.
  2. Migration comments for showHelpInTooltip — Adds // Migration: use variant="compact" on field.Layout.Row.
  3. Migration comments for disabledReason — Adds // Migration: use disabled="<reason>" on field.Input.
  4. Legacy Form marker — Inserts {/* Migration: legacy Form — TanStack */} as the first child inside each <Form>...</Form>. Self-closing <Form /> is expanded to an open/close pair so the marker stays valid JSX. This aligns touched files with metered Form sites (every counted Form produces at least this edit unless the marker is already present).

Metrics (form-migration)

One increment per legacy <Form> imported from sentry/components/forms/form (not jsonForm or other modules). Cardinality dimensions:

  1. filepath — Source file path.
  2. migration-easeeasy or tricky only.
  3. features — Sorted |-joined tokens for that Form, or none.

Files that do not match the legacy-form text gate emit no metrics. Before the per-Form marker, only files with hintText / field migration comments changed; metrics still counted every legacy <Form>, so the number of changed files could be far below the number of files with form-migration metrics. The marker step fixes that alignment for Form-metered files.

How features are collected

  • On the Form element: mapFormErrorsform_mapFormErrors, resetOnErrorform_resetOnError, saveOnBlur={false}saveOnBlur_false. Presence-only shell props → form_apiEndpoint, form_initialData, form_model, form_onCancel, form_onSubmit, form_requireChanges.
  • Under that Form in JSX: inline fields={...} / forms={...} on descendants (e.g. JsonForm, StyledJsonForm). Nested legacy <Form> tags are skipped so inner forms are not attributed to the outer shell.
  • Single-Form files: any remaining { fields: [...] } form-group object literals in the file (not necessarily under JSX) are merged into that lone Form’s tokens and ease (e.g. forms={helper()} where the config lives in a const).

Limitations: forms={fn()} / fields={ident} with no inline literal are not expanded without semantic analysis; those configs only contribute in the single-Form file merge above.

Cardinality collision: Two Forms in the same file with identical filepath, migration-ease, and features merge into one count in aggregated metrics. Use raw exports if you need each instance distinguished.

See Feature tokens for token meanings.

Feature tokens

Per field config (if the prop exists): confirm, disabledReason, extraHelp, formatMessageValue, getData, mapFormErrors, resetOnError, saveMessage, showHelpInTooltip, and saveOnBlur_false when saveOnBlur is false on a field.

On the Form JSX element: form_mapFormErrors, form_resetOnError, saveOnBlur_false, and form_* shell tokens above (see “How features are collected”).

migration-ease (tricky) when any tricky token appears in the merged feature set for that Form (same idea as before: custom mutation, errors, save UX, reset behavior, or saveOnBlur false on the shell).

CLI output may only summarize a subset of dimensions; full cardinality appears in metric exports / dashboards.

Future improvement: emitting a file_role (e.g. spec vs prod) dimension for touched files is not implemented; classification is documented in the Step 2 prompt instead.

Step 2 — AI-Assisted

Handles transformations that require context and judgment. The prompt branches by context (production vs test vs custom FormField); read Pick the migration context in workflow.yaml or prompts/form-migration-ai.md.

Production (typical settings / PR-style migration):

  • getDatamutationFn — Data transformation in mutation function
  • mapFormErrorssetFieldErrors — Error mapping in catch block
  • saveOnBlur: falseuseScrapsForm — Explicit Save button forms
  • AutoSaveField — Per-field auto-save with mutationOptions (e.g. boolean toggles + org PUT)
  • FormSearch + extract-form-fields — When the form participates in settings search

Tests: use a TanStack test harness where appropriate; do not require AutoSaveField or field registry extraction unless the test covers that behavior.

Choosing patterns:

SituationPrefer
Save-on-blur field + API per fieldAutoSaveField + Zod + mutationOptions
Full form submit / Save-Cancel / saveOnBlur: false on shelluseScrapsForm + form.AppForm + validators
*.spec.* mounting legacy Form + FormModel for a custom controluseScrapsForm harness + form.AppField (or defer until the field component migrates)
Custom FormField still in productionMigrate field + parents together, or align harness with shared transforms

Usage

Run full two-step workflow (recommended)

Runs Step 1 (deterministic) then Step 2 (AI):

bash

AI step requires LLM_API_KEY. Without it, the CLI prints [AI INSTRUCTIONS] so a coding agent (e.g. Cursor) can complete the migration manually.

Run Step 1 only (deterministic)

bash

Remove --dry-run to apply.

AI prompt reference

The full AI prompt lives in prompts/form-migration-ai.md for reference or manual agent handoff.

Changelog

Release notes: CHANGELOG.md.

Tests

bash
Before

This is one example from the codemod's test cases. The codemod may handle many more cases.

Ready to contribute?

Build your own codemod and share it with the community.