Skip to content

EPBDS-16211 E3: transforms, mapper, rules and form/utils, as TypeScript - #144

Merged
AlexSamBY merged 1 commit into
masterfrom
ts-engine-3
Oct 1, 2026
Merged

AlexSamBY merged 1 commit into
masterfrom
ts-engine-3

Conversation

@AlexSamBY

Copy link
Copy Markdown
Member

What changes

This finishes E3, and E2 with it: src/core and src/library hold no JavaScript outside their tests. The last four files are converted: transforms, mapper, rules and form/utils.

How the engine is typed

A meta node is typed as open JSON: any where the engine reads and rewrites it by key. transforms, mapper and rules take meta from the host and change it in place. What a node may hold is checked at runtime by validateMeta and described by the published types. A narrower type here would claim something the compiler cannot check, about data it never sees. Each any says so where it is declared.

Everything the engine computes from meta is typed:

  • the handler registries;
  • the transform config;
  • the form helpers;
  • the props of asField, withForm and the wrapper;
  • the renderers that Render.Method returns;
  • UIRender's fields.

isObject from utils is aliased so that it does not narrow, in transforms and rules. Its type guard narrowed an any node to Record<string, unknown>, and every key read after that was unknown. The alias is captured at module level. That is safe: utils is outside the engine's import cycle, and CLAUDE.md now states which captures are and are not safe.

The two layered classes keep their shape.

  • Decorator and withFormSetup each subclass the class they are handed, over an untyped base.
  • UIRender declares the members the layers give its instance by interface merging, the way DocumentInstance does. No instance gets an own field in front of a layer's getter.

FIELD's type lists every registry the engine writes: CROSS_VALIDATE, NORMALIZE, NORMALIZER, PARSER and METHODS, besides FUNC, VALIDATE and VALIDATION.

Three small edits, none of them observable:

  • UIRender.contextType is a static field instead of an assignment after the class.
  • The data setter no longer returns the setState result, which a setter drops anyway.
  • popupAlert declares the options its callers pass. Nothing reads them; see the finding below.

Fixed: a misconfigured Table.group never said what was wrong

  • The mapper read a popup key off AppContext, and the context has none. Only the two suites that rendered the mapper by hand supplied one.
  • So each of the three checks threw a TypeError. The error boundary then showed TypeError: Cannot read properties of undefined (reading 'setPopupState'). Measured on master, for all three checks.
  • They now throw their own message, and the boundary shows that. Example: Incorrect config for Table with {name: "rows"}! Table.group.by must have 'id', got '{}'. The two required ids are reported together, as the two popups were meant to report them.
  • Tests: mapper.table-group-misconfig.test.js is new and fails on the previous code, 3 of 3. The two suites that faked the key now run against the real context.
  • Opening the popup instead was considered and not done. The call omits isOpen, so the popup would not show, and a state update made during render can loop. The node failed before and still fails. Only the message is different.

Found, and left for a change of its own

popupOpen's options, and a popup's own props, reach popupAlert, and nothing reads them. The popup shows only a title and content.

Casts and probes

  • Every new cast was removed alone and the checker re-run. Two were unnecessary and are gone. Each remaining cast has its reason beside it.
  • A probe of 11 wrong call sites fails on all 11. It covers each typed surface: mapProps, metaToProps, relativePathFrom, asField, withForm, withFormSetup, the form readers, initSelectStatesFromData, UIRender and Render.Method. A control line of correct calls compiles. I deleted the probe afterwards.

Paths and docs

  • 143 mentions of the four files in 68 files now name .tsx/.ts. That covers comments, tests, scripts and the coverage thresholds. History stays as it was: the plan and the changelog.
  • generate-view-reference.js and the prop-reference contract read these files by path. Both references are regenerated. Comparing them, only those paths changed.
  • The curation cited mapper.js:184 and :486. Those line numbers were already stale; they now point at the current lines.
  • CLAUDE.md describes the converted state. It covers the meta-as-JSON rule, the import-cycle rule as it applies now, and engine/utils.ts.
  • docs/UPGRADE-PLAN.md records the batch and marks E3 done.

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,926 bytes 322,919 bytes
src/core + src/library, TS 135 files, 17,075 lines 139 files, 20,203 lines
src/core + src/library, JS 4 files, 2,925 lines none

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 grew by the 3 tests of the new Table.group suite, from 2839 to 2842.

🤖 Generated with Claude Code

This finishes E3, and E2 with it: src/core and src/library hold no
JavaScript outside their tests.

- A meta node is typed as open JSON, `any` where the engine reads and
  rewrites it by key. validateMeta checks it at runtime, and the
  published types describe it.
- The util isObject type guard narrowed such a node to
  Record<string, unknown>, so transforms and rules use a non-narrowing
  alias of it.
- Decorator and withFormSetup subclass the class they are handed, as
  written. UIRender declares by interface merging what the layers give
  its instance, so no instance gets an own field in front of a layer's
  getter.
- FIELD's type lists every registry the engine writes.

Fixed, because the checker found it: a misconfigured Table.group never
said what was wrong. The mapper read a `popup` key off AppContext, and
the context has none; only the suites that rendered the mapper by hand
faked one. Each check threw "Cannot read properties of undefined
(reading 'setPopupState')", which the error boundary then showed. The
checks now throw their own message, with both required ids reported
together. A new suite fails on the previous code, and the two that
faked the key run against the real context. Opening the popup instead
was not done: the call omits isOpen, and a state update during render
can loop.

Found, and left for its own change: popupOpen's options and a popup's
own props reach popupAlert, and nothing reads them.

Every new cast was removed in turn to prove it is needed. Two were not
and are gone. A probe of 11 wrong call sites fails on all 11.

Measured: the corpus is identical, with 107 / 1012 at mount, 27 / 64 on
an edit and the DOM unchanged. dist/index.js went from 322,926 to
322,919 bytes.
@AlexSamBY
AlexSamBY merged commit 6bbc9c0 into master Oct 1, 2026
5 checks passed
@AlexSamBY
AlexSamBY deleted the ts-engine-3 branch October 1, 2026 11:48
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)
AlexSamBY added a commit that referenced this pull request Oct 1, 2026
## What changes

**This finishes the TypeScript migration.** It meets every clause of the
definition of done in `docs/UPGRADE-PLAN.md` §9.6:
- `src/core` + `src/library` are TypeScript under `strict`. That was E2
and E3, the last of it in #144.
- `allowJs` is off. That is this PR.
- The declarations are generated from the source. That was E4, #146.
- `propTypes` and `prop-types` are gone. That was E5, #145.

The demo and the test suites stay JavaScript, as the definition of done
allows.

### `allowJs` is off

- **The typecheck program is the TypeScript and nothing else:** 418
files before, 137 now. The 281 that left were 234 `.js`, 9 `.jsx` and 36
`.json` files that tsc resolved but never checked, plus the two guard
files deleted below.
- **A `.ts` file that imports a `.js` module now fails** with TS7016. I
measured this with a probe file, then deleted it. During the migration
this was `allowJs: true` with `checkJs: false`, so that a converted file
could import an unconverted one.
- **The typecheck takes 1.56 s, down from 1.80 s**, the median of three
runs each.
- `tsconfig.build.json` loses its own `allowJs: false`, which it now
inherits.

### The E0 guard is deleted

`src/toolchain/` was a two-file fixture. It proved that the checker sees
`.ts` and that Babel strips it. Its header asked for its deletion "once
real converted modules cover the same ground", and they do: the
product's TypeScript is built by all three pipelines and imported by
every suite. No `.test.ts` is left, because the tests are JavaScript.

### Three casts that still gave the migration as their reason

**`PopupContent` re-typed the renderer because it was "still
JavaScript".** I removed the cast, and the checker found what it had
been hiding:
- the popup hands `Render` a `null` `relativePath` and `relativeIndex`;
- `Render` passes them on to its children, because it tests `!==
undefined`;
- `Render`'s prop types said neither could be `null`.

They now allow `null`, and the cast is gone. No behaviour changed.

**`Data` and `ToggleField` called their casts `UnconvertedComponent`.**
Both still need a cast, for reasons of their own:
- a registry slot that `utils` types `unknown`, because it sits below
the layer that fills it;
- a nested document's `initialValues`, which the form layer types as a
record and `Data` as `unknown`.

The type is now `OpenComponent`, and the comments give those reasons.
`Data`'s import is renamed from `UIRenderWithUISetupJs` to
`DocumentWithOwnForm`. Its `as unknown as` cast was double; I checked
that a single `as` suffices.

### Docs

- `CLAUDE.md` says the migration is complete, and that a `.ts` file
cannot import JavaScript.
- `docs/UPGRADE-PLAN.md` marks the definition of done as met and records
the guard's deletion.
- The CI comment on the Typecheck step no longer describes unconverted
JavaScript.

## Measured

| | master | this PR |
|---|---|---|
| typecheck program | 418 files | 137 files |
| `npm run typecheck`, median of 3 | 1.80 s | 1.56 s |
| 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` | 311,539 bytes | 311,535 bytes |

The 4 bytes are `PopupContent`'s alias. Both bundles were built from
`git archive` copies in directories of the same name length.

## 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`, 2855 tests each, with
no worker crash;
- `test:e2e`, 41 tests;
- the manifest contract.

The suite went from 2857 to 2855 tests: the two were the deleted guard's
own.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant