Skip to content

Fix #155: record-props is THE output form — nameable props type for every component - #156

Merged
jagguji merged 1 commit into
mainfrom
feat/155-record-props-flag
Jul 22, 2026
Merged

Fix #155: record-props is THE output form — nameable props type for every component#156
jagguji merged 1 commit into
mainfrom
feat/155-record-props-flag

Conversation

@jagguji

@jagguji jagguji commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #155. Amended per review discussion: no flag — record-props is now the only output form.

Problem

Whether a component binds as type props = {…} + external make: React.component<props> or as classic labeled args was decided solely by "extends *HTMLAttributes / shares a base" (recordProps in emit.mjs). A component with a plain, self-contained props object — blend's AccordionItem — always fell to labeled args and never got a nameable props type, which blocks the wrapper/override pattern:

module Item = {
  type props = JuspayRescriptBlend.AccordionItem.props   // ← needs a nameable type
  @react.componentWithProps
  let make = (props: props) => <JuspayRescriptBlend.AccordionItem {...props} onClick=mine />
}

Fix — one output form, no flag

Every component with ≥1 prop emits the record form. Deliberately not a flag (the first cut was --record-props opt-in; review pushback was right): two output modes double the test surface forever and force every consumer regen script to carry an option — for zero benefit, since the forms are behaviorally identical for JSX (v4 lowers to make/props either way). --record-props remains accepted as a no-op so any script that adopted it during the preview keeps working.

// AccordionItem.res (was labeled-args, no nameable type)
type props = {
  value: string,                                  // required stays required
  subtext?: string,
  @as("type") type_?: RecordPropsTypes.recordPropsType,   // reserved words unchanged
  weird?: string,  // ⚪ loose — was `string | { a: 1; } | { b: 2; }`   ← flags survive
}
@module("demo") external make: React.component<props> = "AccordionItem"

Correctness

  • Flags carry overpropLine renders both forms; flag markers byte-equal on the benchmark corpus (blend: 910 = 910).
  • Generics parameterize — a type-var prop becomes type props<'a> + React.component<props<'a>>; this also makes the Catalog of minor fidelity widenings (TS type vs generated ReScript) for triage — blend 0.0.37-beta.6 #65 variant-fallback path compile-safe (tyvar declared, not free).
  • No empty record possible — extraction already skips props-less components (no-props).
  • The motivating wrapper pattern is compile-proven in the sandbox ({...props, subtext: ?mine} spread-with-override).

Verification

  • All 105 goldens regenerated (form-only flip: labeled → record) and all compile on ReScript — the compile run is the safety net for record-form edges.
  • Benchmark: all 9 packages compile with identical usable/review/broken metrics — pure form churn, accepted. Consumers' generated files churn once.
  • Fixture record-props (renamed from record-props-flag, no flag in args); smoke checks updated to assert the record form as the default.
  • Docs: TYPE_MAPPING (record-props is the form; the HtmlAttrs fall-through cases now emit inline-field records) + CHANGELOG flagged as an output-form change.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Benchmark: ✅ PASS

Package Compile Diff vs baseline usable review broken Verdict
@juspay/blend-design-system@0.0.36 identical 102 5 0 ✅ PASS
@juspay/blend-design-system@0.0.37-beta.8 identical 215 7 0 ✅ PASS
react-day-picker@10.0.1 identical 19 7 0 ✅ PASS
react-tooltip@6.0.7 identical 1 0 0 ✅ PASS
react-markdown@10.1.0 identical 0 2 0 ✅ PASS
@smastrom/react-rating@1.5.0 identical 1 0 0 ✅ PASS
clsx@2.1.1 identical 0 0 0 ✅ PASS
hono@4.12.25 identical 0 0 0 ✅ PASS
@base-ui-components/react@1.0.0-rc.0 identical 174 21 0 ✅ PASS

@pkg-pr-new

pkg-pr-new Bot commented Jul 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@juspay/rescript-bindgen@156

commit: c4d1f95

…meable props type

Whether a component binds as `type props = {...}` + `external make:
React.component<props>` or as labeled args was decided solely by "extends
*HTMLAttributes / shares a base" (recordProps gate in emit.mjs). A plain-props
component (blend's AccordionItem) never got a nameable `props` type, blocking the
wrapper/override pattern (type props = Lib.Item.props + <Item {...props} onClick=mine/>).

Every component with >=1 prop now emits the record form — NO FLAG, one output form
only (per review discussion: a second output mode doubles the test surface and forces
every consumer regen script to carry an option, for zero benefit — the two forms are
behaviorally identical for JSX, v4 lowers to make/props either way).

- emit.mjs: the recordProps gate is unconditional (`|| ir.props.length > 0`). A
  props-less component keeps labeled args (a record can't be empty; extraction skips
  those as `no-props` anyway). A generic prop parameterizes the record (type props<'a>).
- cli.mjs: `--record-props` is ACCEPTED as a no-op so any script that adopted it
  during the pkg.pr.new preview keeps working; no new flag surface.
- All 105 goldens regenerated (form-only flip) and ALL COMPILE on ReScript — the
  compile run is the safety net for record-form edges (reserved words keep
  @as("type") type_, tyvars declared via type props<'a>).
- Benchmark: all 9 packages compile, usable/review/broken metrics IDENTICAL, flag
  markers byte-equal (910=910 on blend) — pure form churn, accepted via bench:update.
- #65 fallback note: a generic/imperfect variant-branch field now falls back to the
  record form, where the tyvar is DECLARED (type props<'b>) — still compile-safe.

Verified: the motivating wrapper pattern compiles against the generated output
({...props, subtext: ?mine} spread-with-override in the sandbox). Fixture:
record-props (renamed from record-props-flag); smoke checks updated to the new form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jagguji
jagguji force-pushed the feat/155-record-props-flag branch from c451640 to c4d1f95 Compare July 22, 2026 10:13
@jagguji jagguji changed the title Add #155: --record-props — nameable props type for every component Fix #155: record-props is THE output form — nameable props type for every component Jul 22, 2026
@jagguji
jagguji merged commit 90c2714 into main Jul 22, 2026
10 of 12 checks passed
@jagguji
jagguji deleted the feat/155-record-props-flag branch July 22, 2026 13:35
jagguji added a commit that referenced this pull request Jul 22, 2026
The repo has **9 open CodeQL alerts on `main`** (visible in the
default-branch scan). None come from any feature PR — they get
re-attributed onto large-regeneration PRs (#153, #156) by CodeQL's
"changes too large" re-scan, showing up as a red aggregate **CodeQL**
check even though the `Analyze` jobs pass. This clears the root debt so
that check goes green.

## Fixes

**Code — `js/file-system-race` (high ×3) + `js/identity-replacement`
(medium)**
- `test/lib/diff.mjs` — `readdirSync(dir, { withFileTypes: true })`
instead of `readdir` + a separate `statSync` (the stat is a
check-then-read TOCTOU: the file could vanish between the two syscalls).
- `src/cli.mjs` — `unlink` directly inside the existing try/catch
instead of `existsSync`-then-`unlink` (the check-then-unlink is the
race; the catch already handles "already gone").
- `benchmark/run.mjs` — read the prior lock-stamp via try/catch instead
of `existsSync`-then-`readFileSync`; and drop the `.replace(/^_/, '_')`
in `slugOf`, which was an **identity no-op** (`"@s/p"` → `"_s_p"`
already starts with `_`).

**Workflows — `actions/missing-workflow-permissions` (medium ×3) +
`actions/unpinned-tag` (medium ×2)**
- `ci.yml` — add `permissions: contents: read` to the `test` / `compile`
/ `fixture-guard` jobs (least-privilege; the `preview` job already
declared its own).
- `benchmark.yml` + `yama-review.yml` — pin
`marocchino/sticky-pull-request-comment` and `juspay/yama` to their
release commit SHAs, with the `# v3` / `# v2.7.1` version as trailing
comments (the CodeQL-recommended form). *Note: `yama-review.yml` is
generated by juspay/yama's setup script, so a future regen may need the
pin re-applied.*

## Safety
All behaviour-preserving — verified: **104 goldens match + compile**,
**benchmark byte-identical** (the `slugOf` and lock-stamp changes are
proven by the baselines still resolving), and the workflows parse. No
`src/` runtime logic changed (the `cli.mjs` edit is the `--clean`
stale-file removal, same effect).

After this merges, the pre-existing alerts clear repo-wide and the red
**CodeQL** check on #156 (and future large-regen PRs) goes green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Labeled-args components (no HTML-attrs base) never get a nameable props type — blocks wrapper/override pattern

1 participant