feat(react): pilot AffineScript port of Button per migration playbook#15
Merged
hyperpolymath merged 4 commits intomainfrom May 4, 2026
Merged
feat(react): pilot AffineScript port of Button per migration playbook#15hyperpolymath merged 4 commits intomainfrom
hyperpolymath merged 4 commits intomainfrom
Conversation
Step (c) of the TS-elimination plan. First .affine source in this repo.
What's done:
- Button.affine — full re-decomposition of Button.tsx per the playbook's
Cardinal Rule ("re-decompose, don't transliterate"). Concrete moves:
* forwardRef class → standalone make_button fn returning own Element
* React 'a | b | c' string unions → ButtonVariant/ButtonSize sum types
* HTMLButtonAttrs spread escape hatch → dropped (caller mutates returned own Element)
* iconBefore?: ReactNode → Option[own Element]
* Defaults via default_button_props() factory
* DOM mutations declared `/ IO` until @affinescript/dom publishes a typed DOM effect
* Inline DOM extern declarations (replace with @affinescript/dom import once
cross-package import story is settled in this repo)
- Index.affine — version + wcag_level constants + PORTING_NOTES block
documenting the Modal decomposition strategy for the next session.
- Button/Button.tsx removed
- index.ts removed
What's not done (deferred):
- Modal.tsx left in place. Its 5 useEffect blocks (portal mount, focus trap,
escape-key, scroll lock, focus restoration) each become a separate own-
resource with Drop semantics in AffineScript — designed but not written.
See PORTING_NOTES in Index.affine.
- package.json still references rollup + jest + @reach/auto-id; build pipeline
swap to affinescript-vite is a separate concern, not blocking this pilot.
Reference: hyperpolymath/affinescript:docs/guides/migration-playbook.adoc
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 114f7bf which deleted Button.tsx + index.ts but missed staging the new .affine files due to a chained-shell-command failure mid-pipeline. The .affine bodies and decomposition rationale are unchanged from what 114f7bf's commit message described. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Conformance pass against the conventions resolution log (see feedback_affinescript_conventions.md), which were locked 2026-05-03 after PR #15 surfaced four open questions: - Q1 face = canonical: Effect annotation switched from suffix `-> X / IO` to canonical `-{Effects}-> X` between args and return type. - Q3 effects = domain-specific: DOM mutations now declared `-{DOM}->` instead of conflating with terminal `IO` (which the warmup reserves for `println`/`eprintln`/`read_line`). - Q4 cross-pkg = per-package Externs.affine shim (interim, pending the agreed-in-principle `.affex` compatibility filesystem): * New `Externs.affine` declares `pub extern type Element/Event` and `pub effect DOM { ... }` with all DOM extern fns * `Button.affine` and `Index.affine` `use Externs;` and reference `Externs.create_element(...)`, `Externs.DOM`, etc. * Single TODO ref to the .affex parking-lot entry, replacing the point-of-use externs scattered through Button.affine Cardinal stance applied: keeps the affine core of the package (Button, Index) legible — compat plumbing isolated to one file per package. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
hyperpolymath
added a commit
that referenced
this pull request
May 4, 2026
…#16) ## Summary Step **(a)** of the migration plan: AffineScript `.affine` design files for all 5 Node-bound packages, written alongside the JS fallback ([PR #14](#14)). Bodies remain TODO until the Node-target lands ([affinescript#35](hyperpolymath/affinescript#35)) — the type-level contract is the design lock-in. **Conformance**: every file follows the locked AffineScript conventions (see `feedback_affinescript_conventions.md` resolution log). Q1=canonical face, Q2=`.affine` extension, Q3=domain-specific effect names, Q4=per-package `Externs.affine` shim (interim, pending `.affex` compatibility filesystem). ## What's in this PR 14 new `.affine` files across the 5 Node-bound packages: | Package | Files | Effects declared | |---|---|---| | `scanner` | Externs, Scanner, Index | `Browser`, `Fs`, `Clock` | | `core` | Externs, Arangodb, Index | `Db`, `Process` | | `github-action` | Externs, Action, Index | `Action`, `Github` | | `cli` | Externs, Cli, Index | `IO`, `Process`, `Fs`, `Path`, `Commander`, `Spinner`, `Style`, `TableBuilder` | | `monitoring-api` | Externs, Server | `Net`, `Joi`, `Dotenv`, `IO`, `Process`, `Clock` (+ re-declared `Db` / `Scanner` from cross-pkg shims) | ## Key playbook re-decompositions captured at the type level Per the [migration playbook](https://github.com/hyperpolymath/affinescript/blob/main/docs/guides/migration-playbook.adoc) Cardinal Rule (*re-decompose, don't transliterate*): - **Module-level singletons** (`server.ts`'s top-level `db` / `scanner`) → owned `AppState` threaded as `ref` parameter through route handlers — kills the service-locator pattern the playbook flags. - **`try/catch` around every handler** → handler effect rows (`Net + Db + Joi + Clock + …`) make impurity visible at the signature. - **Tagged-template `aql\`…\`` queries** → bind-vars-form `Db.query[B](db, str, bindVars) -> Cursor` per the playbook recommendation. - **`'A' | 'AA' | 'AAA'` literal-string unions** → AffineScript sum types (`A | AA | AAA`). - **TS class with private fields + methods** (e.g. `ArangoDBService`) → owned `Service` record + standalone fns taking `ref Service` / `mut Service`. - **`React.forwardRef` class** (in [PR #15](#15)) → standalone `make_button` fn returning `own Element`. ## Caveats - **Bodies are stubs.** Each fn has the right signature + effect row but the body is `// TODO: implement when Node-target lands` plus `let _ = …; ()`-style placeholders. This is the agreed shape of "step (a) design lock-in" — type-level contract captured, runtime impl a separate session. - **Cross-package types** (e.g. monitoring-api consuming core's `Service` and `Db` effect, or scanner's `ScanResult`) are currently re-declared as opaque `extern type` + a slim re-declared effect in each consumer. When `.affex` lands, those re-decls collapse to `use Core` / `use Scanner` and the duplication disappears. - **Stdlib-style helpers** (`String.join`, `List.fold`, `Int.to_string`, etc.) are referenced but not yet pinned to a specific stdlib path — to settle when the language's stdlib path scheme is finalised. - **`pub` visibility on top-level `let` / `type`** is used per `stdlib/Core.affine` precedent; if the canonical face has different export semantics in some constructs, those are renamings. ## How this fits the four PRs | Step | Branch | PR | What it gives | |---|---|---|---| | (d) | `js-strip-types` | [#14](#14) | TS → JS via ts-blank-space; Node packages run today | | (c) | `react-affinescript-pilot` | [#15](#15) | Button.affine pilot; Modal deferred | | **(a)** | `affinescript-node-design` | this PR | `.affine` design for the 5 Node-bound packages | PR #14 keeps the runtime working today; PR #15 + this PR capture the AffineScript design while context is fresh. The two layers (`.js` + `.affine`) coexist until the Node-target lands; at that point the `.js` files retire and `.affine` becomes canonical. ## Test plan - [ ] AffineScript compile-check via `affinescript check <file>` once a working environment is set up — couldn't run in this session - [ ] Specify `.affex` per the parking-lot entry; collapse cross-package type re-decls - [ ] Implement TODO-bodies once Node-target lands (multi-session) - [ ] Modal port (deferred from PR #15) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Step (c) of the TS-elimination plan: the first AffineScript source in this repo, ported from
Button.tsxper the migration playbook's Cardinal Rule (re-decompose, don't transliterate).What's done
src/Button.affine— full re-decomposition ofButton.tsx:forwardRefclass → standalonemake_buttonfn returningown Element'primary' | 'secondary' | …) → AffineScript sum types (ButtonVariant,ButtonSize)extends React.ButtonHTMLAttributesspread escape hatch → dropped; callers compose by mutating the returnedown ElementiconBefore?: ReactNode→Option[own Element](caller pre-builds the icon subtree)/ IO; ready to swap to a typedDOMeffect once@affinescript/dompublishes oneimport @affinescript/domonce the cross-package import story is settled)src/Index.affine— public surface (version,wcag_level) plus aPORTING_NOTESblock laying out the Modal decomposition strategy for the next session.Button/Button.tsxandindex.tsdeleted.Deferred
useEffectblocks (portal mount, focus trap, escape-key listener, scroll lock, focus restoration) each become anown-resource with Drop semantics in AffineScript — the design is sketched inIndex.affine'sPORTING_NOTESbut not yet written.package.jsonstill references rollup + jest +@reach/auto-id. Migration toaffinescript-viteis a separate concern, not blocking this pilot.Caveats / open questions
.affinevs.as.affinescript-dom/src/dom.asuses.as; everywhere else (conformance, warmup, examples) uses.affine. I went with.affineto match the playbook + warmup. Happy to rename if.asis the React/browser-target convention./ IO. If the conventional row for browser DOM is something more specific (e.g.Webor aDOMeffect), trivially renameable.Primary | Secondary | …). If the convention here is the rattle/cafe/jaffa face, can rewrite.Test plan
affinescript check Button.affine(requires the compiler in a cwd with the right deps; couldn't run in this environment)affinescript-vite🤖 Generated with Claude Code