Repository navigation
EPBDS-16211: fix the bugs the modernization found, and the docs that drifted - #139
Merged
Merged
Conversation
…drifted
Every fix has a test that fails on the previous code.
- validate: 'maxLength' registered the length FACTORY as the validator,
in FIELD.VALIDATION and in the draft row, so every value failed; both
register maxLength() now, the 100-character default.
- The per-form registries were keyed by react-final-form's per-render
{...form, reset}, so a remembered touch lasted one render and a touched
field remounted by a tab switch lost its error. The wrapper keeps one
object per final-form instance, told apart by its methods, because
StrictMode's discarded first render builds an instance of its own.
- A popup Text bound by name renders its row's field: it rendered nothing
(nested row document) or the whole row object (resolved row scope).
- `meta` and OpenL's `@class` join ENGINE_PROPS, so neither reaches the
DOM; popup items carried meta="[object Object]".
- StandaloneTabs renders a `{text}` tab without an icon as its text.
- The demo's percent slider shows "50%", not "5,000 %": the Percent
renderer takes a fraction, and the example's values are percents.
- The corpus logged five warnings, now none, and a contract test holds
it at zero: two metas put buttoned on a layout, InputNumber rejected
error={false}, a nested document's formProps/instance were marked
required, and @Class reached a span.
- popupScope's search for a row in the values' keys could never
succeed, and is gone; utils/layouts.js and utils/SizeMe.js, imported
by nothing, are deleted.
Docs: roadmap rows 4 and 4-S and risk R14 are brought up to date, the
plan's findings are marked fixed, CLAUDE.md no longer calls the source
JavaScript, TooltipPop's note about refs covers React 19, and the
changelog records the consumer-visible fixes.
Measured: the 38-example corpus is identical to master except the
slider example's tooltip (and one fewer translate call, 1013 -> 1012);
commits are unchanged, 107 at mount and 27 on an edit.
This was referenced Oct 1, 2026
AlexSamBY
added a commit
that referenced
this pull request
Oct 1, 2026
**Stacked on #139.** It is based on that PR's branch, so this diff shows only its own change. After #139 merges, I rebase it onto master and retarget it. ## What changes **`src/core/components` has no JavaScript left outside its tests.** This converts: - `Dropdown`, `Listbox`, `Slider`, `PieChart` and `JsonView`; - the modules they share: `renders`, `types`, `styles`, `files`, `charts/utils`, `charts/constants` and `JsonView/themes`; - the two barrels. Each component exports its props type. `propTypes` stay until E5, as in the earlier batches. ### Dead code, deleted rather than typed Each piece was checked for importers first, counting `jest.mock`, `require` and dynamic `import`. The library's only export is `UIRender`, so nothing outside the repo can reach these. - **Four helper modules:** `utils/img`, `utils/react`, `utils/components` and `utils/interactions`. Their ten helpers were imported by nothing but their own tests. `utils/index` now re-exports `timers` alone. `isFileSrc` in `src/core/utils/string.ts` goes too: its only caller was `cssBgImageFrom`. - **Five of `renders`' exports had no caller:** `colorDropdownOptions`, `languageDropdownOptions`, `colorDropdownChoice`, `renderCurrency` and `renderFloatShort`. `ColorSwatch` goes with them, because only they rendered it. Their CSS rules stay, for the stylesheet's own cleanup. - **`charts/utils`' `chartTooltip`**, a recharts leftover. - **`inputs/translations.js`**, imported by nothing in the library. `engine/translations.js` registers all twelve of its phrases verbatim, and every document loads that file. The one test that loaded the deleted file now loads the engine's. ### What the conversion found - **A `Dropdown` is never `done`, and this PR leaves that alone.** - Since May 2023 the wrapper destructures `value` as `valueFromParent`. Its fallback `done = !error && (!!props.value || props.value === 0)` therefore reads a `props.value` that is always `undefined`. `Input` and `InputNumber` compute the same class from their value. - The visible effect is narrow. `.input--wrapper.done` is what floats a `float` dropdown's label, so with a value and no focus that label stays at rest. No tracked meta puts `float` on a dropdown. - The fix would add `done` to every dropdown wrapper that has a value. That is a DOM change, so it should be its own PR. - **`static/images/flags/` is now unreferenced.** `languageDropdownOptions` was the only thing that read it, and it was already tree-shaken out of `dist` (`flags/` occurs 0 times in either bundle). The folder still ships, because a host that copies `static/` might link a flag from its own markup. Whether to stop shipping it is an open decision gate in the plan's §10. - **The E1 contract question about `renders`' `round(value, decimals)` needed nothing.** #61 had already widened the rounding helpers to `number | string`. The plan's note is marked resolved. ### Casts and probes - `Counter`'s cast of `renderFloat` is deleted. - `InputNative` casts the keyup event it hands `toTextHeight`. React types `target` as any `EventTarget`, and a keyup's target is the field itself. - `types.ts` is `Record<string, any>`, deliberately. It hands out validators and validator factories side by side, and E5 deletes it. - `styles.ts` types the four role aliases it assigns after the palette with one commented cast. - **Every remaining new cast was proven necessary.** Each was removed alone and the checker re-run. Two turned out to be unnecessary and were dropped, and one more became a parameter annotation. - **A probe of 14 wrong call sites fails on all 14.** It covered every new props type and helper signature, and was deleted afterwards. ### Renames Every converted file is a git rename except two: - **`renders.tsx`** shows as a delete and an add. The five deleted renderers and their imports were 52% of the old file, so no version of it can clear git's 50% similarity bar. `git log --follow -M40%` still follows it. - **`utils/index.ts`**, a one-line barrel. ### Docs and comments - Mentions of the renamed files now say `.tsx`, in comments, in the prop reference's generator and curation, and in a test's notes. `public/static/ui-render.built.css` is rebuilt for two LESS comments, and `docs/SUPPORTED-PROPS.md` is regenerated. - `docs/UPGRADE-PLAN.md` records the seventh batch, marks the E1 question resolved, and adds the flags decision gate. - `webpack.library.config.mjs`'s comment on `flags/` is updated. - `CLAUDE.md` says which packages are fully TypeScript. ## Measured | | base (#139's head) | this PR | |---|---|---| | corpus, 38 examples: commits at mount / `translate` calls | 107 / 1012 | 107 / 1012 | | on an edit: commits / `translate` calls | 27 / 64 | 27 / 64 | | DOM after mount and after an edit | | identical, all 38 | | `dist/index.js` | 327,359 bytes | 327,232 bytes | | `src/core` + `src/library`, TS | 71 files, 10,817 lines | 84 files, 13,078 lines | | `src/core` + `src/library`, JS | 84 files, 9,339 lines | 65 files, 6,977 lines | | TypeScript share of the lines | 53.7% | 65.2% | The bundle loses 127 bytes because `ColorSwatch`'s `propTypes` calls had survived tree-shaking. ## Gates All exited 0, run on this branch: - `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`; - `docs:props:check`, `docs:views:check`, `css:fixture:check`; - `test:env-flags`, `build`; - `test:coverage`, thresholds included; - `test:pack`, `test:pack:peers`, `test:types`; - `test:react16`, `test:react17`, `test:react19`, 2870 tests each, with no worker crash; - `test:e2e`, 41 tests; - the manifest contract. The suite went from 2914 to 2870 tests. All 44 removed tests covered the deleted code. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY
added a commit
that referenced
this pull request
Oct 1, 2026
**Stacked on #140, which is stacked on #139.** It is based on #140's branch, so this diff shows only its own change. As the PRs below it merge, I rebase it onto master and retarget it. ## What changes **Everything outside the engine is TypeScript now.** That covers `src/core/modules` except `form/utils.js`, plus `contexts`, `providers`, `common`, `services` and `src/library`. What is left in JavaScript is the engine, 25 files, and `modules/form/utils.js`, which shares a cycle with it. ### Dead code, deleted rather than typed Most of `modules/variables` was boilerplate from the upstream starter kit. Every name was checked for importers, with barrels, `require` and `jest.mock` followed. The library's only export is `UIRender`, so nothing outside the repo can reach these. | deleted | why it was dead | |---|---| | `variables/_envs.js` | `SERVICE`, `Active.SERVICE`, `Active.state` and `Active.usersById` were read by nothing. For `Active.passwordCheck`, see the findings below. | | `variables/configs.js`, except `UI` | seven network and contact constants, plus `CONFIG` and `SERVER`, with no importer | | `variables/defaults.js`, `definitions.js`, `urls.js`, `validations.js` | no importer. `variables/index` called `localise(definitions)` at load. | | `variables/routes.js`, except `ROUTE_HOME` | `common/variables/routes.js` wrote into `ROUTE` and `ROUTES`, and nothing read them. `ROUTE_BASE` and `goTo` stay, because the env-flags gate and the demo read them. | | most of `variables/files.js` | Five file-name helpers that only its test called, the MIME tables, `UPLOAD.PATH`, and a copy of `components/files`' `FILE`. What stays is what `Upload` and the demo read: `FILE.EXT`, `FILE.TYPE`, `IMAGE.EXTENSIONS` and `UPLOAD.BY_ROUTE`. | | `FIELD.ID`, `DEF`, `MIN_MAX`, `FOR` | read by nothing, once `form/constants` loses its `email` definition and `toSlider` | | most of `form/constants` | Nine `= true` prop shorthands, `NAME`, `FORM_ASYNC_VALIDATE`, `API_VALIDATE_FAIL_CODE`, `toSlider` and the `email` field definition, which only tests read. It still registers `FIELD.VALIDATE` and `FIELD.VALIDATION`. | | `form/translations.js` | never imported | | `form/inputs/UploadGridField.js`, `UploadGridsField.js` | never imported, and they imported views that do not exist | | `common/styles.js`, `common/utils/tests.js` | no importer | Five `src/core/utils` exports lose their last caller: `optionsFrom`, `enumFrom`, `localise`, `fileFormatNormalized` and `LANGUAGE_LEVEL`. I left them for a sweep of `utils` of its own, which would find more. ### Findings, left for changes of their own - **A server-side `validate: 'password'` has always thrown.** - In a browser, `Active.passwordCheck` returns `window.zxcvbn` or an accept-all fallback. On a server it returns the stored checker. `_envs.js` stored `() => {}`, so `isGoodPassword` read `.score` of `undefined`. - Measured with the engine loaded in a Node environment: before, `TypeError: Cannot read properties of undefined (reading 'score')`; now, `TypeError: Active.passwordCheck is not a function`. Either way the check throws. - Fixing it means choosing a server fallback, which is a policy decision. - **`validationErrors` renders "Untranslated" for its header.** - The `PLEASE_COMPLETE_` phrase lived in `form/translations.js`, which nothing imported. Measured on the base: `_.PLEASE_COMPLETE_` and `_.EMAIL` read "Untranslated", while `_.REQUIRED` reads "Required". - Only tests read `validationErrors`, so no product screen shows this. ### Fixed on the way - **The view-reference generator's guard walked `.js` and `.jsx` only.** It failed as soon as `fields.ts` existed. That is the extension-keyed-gate hazard the plan has recorded seven times before, and this time it announced itself. It walks TypeScript too now. - **`asField`'s JSDoc described its options bag as a `Function` parameter, and the checker believed it.** The JSDoc in the still-JavaScript `form/utils.js` is corrected, and `DropdownField` annotates its sanitiser. ### Casts - `Upload` loses both of its casts. `AppContext` is typed now, and `UPLOAD.BY_ROUTE` is read as `UploadRouteDefaults | undefined`. - `ConfigContext` is created with an explicit `undefined`, which is what `createContext()` gave it. - **Each cast that remains was proven necessary by removing it alone:** `FIELD`'s registries, `Active.Field` read at render, `AppWrapper`'s config, and `AutoSave`'s latest props. - **A probe of 13 wrong call sites fails on all 13.** It covered every new props type and signature, and was deleted afterwards. ### Paths and docs - The library entry is `src/library/index.ts`. `webpack.library.config.mjs` was updated to match. - `AppWrapper.tsx` is where the version is written, so these were updated too: `npm version`'s `git add`, `scripts/sync-version.js` and the library test's `VERSION_SITES`. - The coverage thresholds follow the renames. So do the generator paths, and the comments that named the files. - `docs/SUPPORTED-VIEWS.md` and `docs/SUPPORTED-PROPS.md` are regenerated, one path each. - `CLAUDE.md` names the new entry, and `docs/UPGRADE-PLAN.md` records the batch. ## Measured | | base (#140's head) | this PR | |---|---|---| | corpus, 38 examples: commits at mount / `translate` calls | 107 / 1012 | 107 / 1012 | | on an edit: commits / `translate` calls | 27 / 64 | 27 / 64 | | DOM after mount and after an edit | | identical, all 38 | | `dist/index.js` | 327,232 bytes | 322,888 bytes | | `src/core` + `src/library`, TS | 84 files, 13,078 lines | 113 files, 13,982 lines | | `src/core` + `src/library`, JS | 65 files, 6,977 lines | 26 files, 5,647 lines | | TypeScript share of the lines | 65.2% | 71.2% | The bundle is 4,344 bytes smaller. The deleted modules shipped, because the barrel that loaded them ran code at load: `localise(definitions)` and the `Active` assignments. ## Gates All exited 0, run on this branch: - `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`; - `docs:props:check`, `docs:views:check`, `css:fixture:check`; - `test:env-flags`, which compiles the source with each real webpack config, the new entry included; - `build`; - `test:coverage`, thresholds included; - `test:pack`, `test:pack:peers`, `test:types`; - `test:react16`, `test:react17`, `test:react19`, 2847 tests each, with no worker crash; - `test:e2e`, 41 tests; - the manifest contract. The suite went from 2870 to 2847 tests. 24 tests went with the code they covered, and one was added, for `FILE.TYPE`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY
added a commit
that referenced
this pull request
Oct 1, 2026
## What changes **E5: every `propTypes` declaration is gone, and so is `prop-types`.** `rg "prop-types" src` returns nothing. The package is out of `devDependencies`, and lint rejects an import of it. ### What is deleted - **39 `propTypes` blocks in 38 files.** The E2 and E3 conversions kept each block on purpose, so this is one deletion rather than forty. - **The semantic proxy `components/types.ts`**, the barrel's `PropTypes` re-export, and every `prop-types` and `type` import. - **The host's and the form layer's handling of prop types.** `documentHost.ts` copied the class's prop types onto the function component, and `withFormSetup` merged them under its own. The four tests that pinned both now pin the rest of what they covered. ### The descriptions survive, in the props types - **They moved into the props types' JSDoc first**, wherever a type did not carry them yet: `TableView`, `Tabs`, `Upload`, `ProgressSteps`, `AutoSave`, `asField`, `asInputDateField` and `ToggleField`. - **`UIRender`'s three undocumented props are documented in the published `UIRender.UIRenderProps`**, where a host reads them: `initialValues`, `form` and `embedded`. The form layer's props are described in a comment where the layer reads them. - **A script compared every commented prop with its TS declaration.** What it still lists is documented by a type alias or by the published type. - **Comments that cited `propTypes` as a fact are reworded:** `Counter`, `Listbox`, `StandaloneTabs`, `documentHost` and the form layer. ### Not done, deliberately: the vocabulary as TS aliases The plan's E5 step 2 asked for the `type` proxy to be recreated as aliases, such as `type Id = string` and `type Px = number`. I measured first: - **The proxy defined 41 names, and the converted files used 16.** - **Twelve of the 16 restate PropTypes itself:** `type.String`, `type.Method`, `type.Of`, and so on. - **Four carry a meaning of their own:** `Id` (used twice), `Fraction`, `UrlOrBase64OrPreview` and `NumberOrString`. - **E2 and E3 already wrote that meaning into the props' JSDoc**, for example "Fraction from 0 to 1" and "URL to link to". An alias of a primitive checks nothing. The vocabulary would have been the same documentation in a second place, and 25 of its names would never have been used. The plan records this decision. ### No diagnostic a test relied on is lost - React 16 to 18 warn in development on a failed prop type. React 19 checks none. - **Two suites allowed prop-type warnings:** four entries in the examples suite, and two in the nested-row suite. - **None of the six fired.** I measured this on React 18, on master, just before the deletion. They are removed, and the nested-row suite now requires no warning at all. - **The `formProps`/`instance` warnings that the plan's §8 flagged were not taken as the contract being met.** The declaration itself was wrong. A nested `Data` document shares its parent's form and is rendered without either prop. #139 made both optional. ### Kept from coming back - **`prop-types` is still installed**, as a dependency of `eslint-plugin-react`, `react-markdown` and the React 16 fixture. An import would therefore resolve and bundle it again. - **`no-restricted-imports` now rejects it in all seven `src` overrides.** I proved this with a probe file in core, in a test and in the demo; each failed with the rule's message. - **`CLAUDE.md` states the rule:** no `propTypes` block. A component's props type, and its JSDoc, are the documentation. ### Also - `README.md` no longer lists `prop-types` among the bundled libraries. - `docs/SUPPORTED-PROPS.md` is regenerated. Only `Dropdown.tsx`'s line count changed, from 381 to 364. - The wrong `colGroup` propType that E3 recorded (`{styles}`) is gone with the rest. The TS type says `style`. - `docs/UPGRADE-PLAN.md` records E5 and closes its items: the §2 audit line, the §8 row, the E-checks row and the Appendix C checklist. The Appendix C command now looks for a dependency entry, since the lint rule names the package seven times. ### Found, and left for a change of its own On React 18, the examples suite's other five allowlist entries match nothing either. Whether any of them still guards something on 16, 17 or 19 is for that change to measure. ## Measured | | master | this PR | |---|---|---| | corpus, 38 examples: commits at mount / `translate` calls | 107 / 1012 | 107 / 1012 | | on an edit: commits / `translate` calls | 27 / 64 | 27 / 64 | | DOM after mount and after an edit | | identical, all 38 | | `dist/index.js` | 322,915 bytes | 311,527 bytes (−11,388, −3.5%) | | `dist/index.js`, gzipped | 107,110 bytes | 104,047 bytes | | `isRequired` in `dist/index.js` | 49 | 0 | | `src/core` + `src/library`, TS | 139 files, 20,203 lines | 138 files, 19,514 lines | Both bundles were built from `git archive` copies in the same directory. Built in the worktree, the same trees measure 322,919 and 311,529 bytes; 322,919 is the figure #144 recorded. ## Gates All exited 0, run on this branch: - `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`; - `docs:props:check`, `docs:views:check`, `css:fixture:check`; - `test:env-flags`, `build`; - `test:coverage`, thresholds included; - `test:pack`, `test:pack:peers`, `test:types`; - `test:react16`, `test:react17`, `test:react19`, 2842 tests each, with no worker crash; - `test:e2e`, 41 tests; - the manifest contract. The suite still has 2842 tests. Four tests, in three suites, pinned prop-type forwarding and merging. Each now pins the rest of its subject: the names, the class left as written, and the state shape. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
This PR covers the bugs found during the modernization that were recorded and left open, and the documentation that had drifted. Every behaviour change has a test that fails on the previous code. I checked each one by running its test against master's version of the file; the two deletions have nothing to fail.
Bugs
validate: 'maxLength'failed every value.FIELD.VALIDATIONregistered the length factory as the validator. The draft row registered the same factory (EPBDS-16211 E2: the five components #132 converted, as TypeScript #133 recorded it there).maxLength(), the validator at the factory's default of 100 characters.UIRender.form-flows.test.jsandLocalDraftTableRow.append-validation.test.js.The per-form registries lasted one render.
{...form, reset}on every render. The registries instate/formRegistry(touched fields, errors, baseline) were keyed by that object.examples.strict-mode.test.jsfailed onnestedDataKind, because StrictMode's discarded first render builds an instance of its own. The document was then bound to that discarded instance, and an edit did not show.A popup item bound by
namedid not show its row. Popup content renders withrelativeData: false, so the item's data is never resolved by name. The mapper'sText/Titleview rendered whatever context it had:_datais unset and the row is indata;_data.It now reads the field it names from that row, as
config.mdpromises. Test:rules.popup-content.test.js.Two keys leaked to the DOM. Both are in
ENGINE_PROPSnow.meta: every popup item carried it asmeta="[object Object]".@class: OpenL's serializer puts this tag on every meta node, and React reported it as an invalid attribute name.The per-component DOM-boundary tests pick both up from the list.
StandaloneTabsrendered a{text}tab without an icon as the object itself. ItspropTypesaccept such a tab, and React rejects rendering it. It now renders the text. The component is demo-only.The demo's percent slider showed "5,000 %" for 50.
Percentrenderer takes a fraction and multiplies it by 100, while the example's marks treat the values as percents.unit: '%'and shows "50%". The renderer is unchanged, since other views rely on its fraction semantics.The corpus logged five warnings, and now logs none.
buttoned, aTabsprop, on aVerticalLayout.InputNumber'spropTypesrejected theerror={false}every field is handed before the user interacts with it.formPropsandinstanceas required. A nestedDatadocument does not pass them, because it shares its parent's form, which the layer's ownformgetter already supports.@classreached a<span>(item 4).A new contract test,
examples.console-clean.test.js, renders every example under StrictMode and expects no console output. With one straybuttonedrestored, it fails.Dead code removed.
popupScope's search for a row in the form values' keys could never succeed, because final-form nests a bracketed name into a real array. Doing what it meant, reading a row from field names, would be the guess §2.6-16 removed, so only its live half is kept: the values.components/utils/layouts.js, imported by nothing, is deleted, and so isutils/SizeMe.js, imported only by it.Documentation
docs/UPGRADE-PLAN.md:CLAUDE.mdno longer says the source is still JavaScript.TooltipPop.tsx's note that a ref cannot reach those components now says that this held on React 16–18. On 19 arefis an ordinary prop and reaches the element; this was measured in EPBDS-16211 E2: the leaf components the first batch still cast, as TypeScript #134.changelog.mdrecords the consumer-visible fixes under Unreleased → Fixes.docs/SUPPORTED-PROPS.mdis regenerated: the boundary lists and one line count.Measured
translatecall, because thePercentrenderer's "%" text is gone: 1013 → 1012 in total.Gates
All exited 0:
lint:js,lint:css,typecheck,typecheck:contract;docs:props:check,docs:views:check,css:fixture:check;test:env-flags,build;test:coverage, thresholds included;test:pack,test:pack:peers,test:types;test:react16,test:react17,test:react19, 2914 tests each, with no worker crash;test:e2e, 41 tests;In the demo on the dev server:
dropdownExperience,ratingDetailsandtableForm.🤖 Generated with Claude Code