@farm.js/react 0.1.0-beta.0 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @farm.js/react
2
2
 
3
- React renderer integration for FARMJS, including the experimental Group 1 AOT compiler.
3
+ React renderer integration for FARMJS, including the experimental AOT compiler.
4
4
 
5
5
  React remains the default renderer and needs no package or configuration. Install this package only
6
6
  when opting into the compiler experiment:
@@ -49,27 +49,293 @@ export function Counter() {
49
49
 
50
50
  The directive is configurable and only has meaning in annotation mode.
51
51
 
52
- ## Group 1 contract
52
+ ## Current compiler contract
53
53
 
54
- The first compiler group handles components that the compiler can prove have:
54
+ The current compiler handles components that it can prove have:
55
55
 
56
- - one host-element root and a static host-element tree;
56
+ - one host-element root and a statically known host-element tree around supported conditional,
57
+ keyed-list, and React component-island boundaries;
58
+ - an identifier props parameter or flat object destructuring with aliases and defaults;
57
59
  - top-level `useState` declarations;
58
- - state-driven text and basic attribute/property bindings;
60
+ - optional compiler-safe derived `const` values declared after state and in source order;
61
+ - whitelisted `Boolean`, `Number`, `String`, and deterministic `Math` calculations;
62
+ - optional synchronous `const` or function-declaration handlers used by JSX events;
63
+ - state-driven text, attributes, per-property inline styles, and controlled form properties;
64
+ - host-rooted `condition && <element>` and `condition ? <element> : <element>` child blocks at
65
+ statically known locations, including supported nested boundaries;
66
+ - item-keyed `collection.map(...)` children and explicit `List` boundaries at statically known
67
+ container locations, including supported non-mutating collection pipelines;
68
+ - inline synchronous events inside otherwise eligible host-only keyed rows;
69
+ - row-local logical and ternary host branches inside stable keyed-row containers;
70
+ - stable module-level child components with compiler-safe props;
59
71
  - React-managed event handlers; and
60
- - no refs, effects, custom child components, keyed lists, or conditional child structure.
72
+ - no refs, effects, or unsupported dynamic child structures.
61
73
 
62
- The generated component preserves React ownership of placement, props, events, SSR, and hydration.
63
- Local state cells batch updates into a microtask and patch only compiler-known DOM paths. A local
64
- state update therefore does not schedule a React render or reconciliation pass.
74
+ The generated component preserves React ownership of initial placement, props, events, SSR, and
75
+ hydration. Local state cells batch updates into a microtask and patch only compiler-known DOM
76
+ targets. Proven, dedicated host containers can transfer child ownership after mount for host-only
77
+ conditional branches and non-interactive host-only keyed rows. An interactive row or a row with
78
+ local conditional slots uses a hybrid boundary instead: React retains its events, conditional
79
+ Fibers, and structural reconciliation, while Farm patches same-key bindings and refreshes only
80
+ changed row-local branches. React still creates or hydrates all initial DOM. Anything outside those
81
+ narrow contracts uses a small React-owned boundary or the complete original component.
82
+
83
+ For a common dedicated conditional, no new component or annotation is required:
84
+
85
+ ```tsx
86
+ <div className="status-slot">
87
+ {enabled ? <strong>Enabled {count}</strong> : <span>Disabled {count}</span>}
88
+ </div>
89
+ ```
90
+
91
+ When the conditional is the container's only meaningful child, the container has only static
92
+ properties, and each branch is a statically known host tree without events, refs, custom components,
93
+ or other dynamic structures, the compiler emits
94
+ both host descriptors and their exact text/attribute/style bindings. After the initial React render
95
+ or hydration, a same-branch update patches those bindings without replacing the element. A condition
96
+ change creates, removes, or replaces only the selected branch. No marker node is added to SSR output.
97
+ More complex conditional shapes keep the existing React-owned conditional boundary.
98
+
99
+ For a common keyed map, no new API is required:
100
+
101
+ ```tsx
102
+ <ul>
103
+ {items.map((item) => (
104
+ <li key={item.id}>{item.label}</li>
105
+ ))}
106
+ </ul>
107
+ ```
108
+
109
+ When this map is the only meaningful child of a dedicated host container and its rows are host-only,
110
+ the compiler prepares a row descriptor, a key reader, and exact text/attribute/style bindings at
111
+ build time. After React performs the initial render or hydration, Farm adopts those row elements.
112
+ Later list updates patch surviving rows by key, create or remove only the changed rows, and use a
113
+ longest increasing subsequence (LIS) to minimize DOM moves during a reorder. The outer user
114
+ component and the list callback do not rerun for those compiler-cell updates.
115
+
116
+ An otherwise eligible host row may also contain an inline synchronous React event:
117
+
118
+ ```tsx
119
+ <ul>
120
+ {items.map((item, index) => (
121
+ <li key={item.id}>
122
+ <span>{item.label}</span>
123
+ <button onClick={() => select(item.id, index)}>Select</button>
124
+ </li>
125
+ ))}
126
+ </ul>
127
+ ```
128
+
129
+ The compiler leaves the event prop on the React element and emits no native listener. Its stable
130
+ keyed proxy looks up the latest item and index when React dispatches the event, so a same-key item
131
+ replacement cannot leave a stale row closure. If the key order is unchanged, Farm patches the
132
+ prepared text, attribute, and style bindings without rerunning the component or map callback. An
133
+ insert, removal, or reorder asks React to reconcile the rows, adopts the committed host elements,
134
+ reapplies current bindings, and then resumes direct same-key patches. This avoids creating eventful
135
+ DOM outside React's Fiber tree.
136
+
137
+ A keyed row may also contain dedicated conditional slots:
138
+
139
+ ```tsx
140
+ <ul>
141
+ {items.map((item) => (
142
+ <li key={item.id}>
143
+ <span>{item.label}</span>
144
+ <div className="status-slot">
145
+ {item.done ? <strong>{item.label} complete</strong> : <span>In progress</span>}
146
+ </div>
147
+ <section className="details-slot">{item.expanded && <p>{item.description}</p>}</section>
148
+ </li>
149
+ ))}
150
+ </ul>
151
+ ```
152
+
153
+ Each conditional must be the only meaningful child of its persistent lowercase host container.
154
+ The compiler records its test and branch bindings per row key. A same-key collection update compares
155
+ those prepared snapshots and asks React to render only the conditional boundaries whose selected
156
+ branch or branch values changed. Other conditional rows and the outer map stay untouched. Inserts,
157
+ removals, and reorders remain React structural commits for these rows, after which Farm re-adopts
158
+ the host skeleton and resumes row-local updates. This keeps empty branches, SSR, hydration,
159
+ recoverable hydration errors, error boundaries, and Fast Refresh inside React's ownership.
160
+
161
+ A keyed collection may use derived locals or an inline chain of `filter`, `slice`, `toSorted`, and
162
+ `toReversed`:
163
+
164
+ ```tsx
165
+ const visible = items.filter((item) => item.visible && item.rank >= minimumRank);
166
+ const page = visible
167
+ .toSorted((left, right) => left.rank - right.rank)
168
+ .slice(offset, offset + pageSize)
169
+ .toReversed();
170
+
171
+ return (
172
+ <ul>
173
+ {page.map((item) => (
174
+ <li key={item.id}>{item.label}</li>
175
+ ))}
176
+ </ul>
177
+ );
178
+ ```
179
+
180
+ The compiler records dependencies used by the source collection, inline predicate, inline
181
+ comparator, and slice arguments. It reruns the pipeline only when one of those compiler cells
182
+ changes, then gives the result to the existing keyed-row and LIS runtime. The required filtering or
183
+ sorting still runs; unrelated local updates avoid that work and do not rerun the outer component.
184
+ Callbacks must be synchronous, inline, and contain one compiler-safe returned expression. Mutating
185
+ `sort`, `reverse`, and `splice`, external or async callbacks, Hooks, assignments, spread arguments,
186
+ and unproven calls use the original React fallback.
187
+
188
+ `toSorted` and `toReversed` are emitted as standard runtime calls rather than polyfilled. Configure
189
+ the TypeScript `lib` with ES2023 and target a runtime that supports them when using those methods.
190
+
191
+ React remains the fallback and compatibility boundary. An interactive map beside static children,
192
+ a non-host sibling between ranges, a row with a non-inline or async handler, a file input or dynamic
193
+ form control shape, a fragment, a ref, a custom component, or a conditional mixed directly beside
194
+ other children in the same host slot uses the existing React-owned keyed boundary. The outer
195
+ compiled component can still be skipped, but React reconciles that list's rows and owns their
196
+ events, lifecycle, and state.
197
+
198
+ Non-interactive host maps can share a nested or component-root container with static host siblings:
199
+
200
+ ```tsx
201
+ <ul>
202
+ <li>Primary</li>
203
+ {primary.map((item) => (
204
+ <li key={item.id}>{item.label}</li>
205
+ ))}
206
+ <li>Secondary</li>
207
+ {secondary.map((item) => (
208
+ <li key={item.id}>{item.label}</li>
209
+ ))}
210
+ <li>{primary.length + secondary.length} total</li>
211
+ </ul>
212
+ ```
213
+
214
+ The compiler emits one range block for that host container. React renders and hydrates the original
215
+ markup. After mount, Farm records the static element segments and reconciles each keyed range with
216
+ its own row table and LIS pass. It does not add wrappers or hydration markers. When the container is
217
+ the component root, the block forwards that exact host element to the outer binding runtime, so
218
+ root attributes and styles keep updating normally. A root with one eligible map or `List` also
219
+ works without static siblings. Direct children must be host elements or eligible ranges; events,
220
+ controlled rows, row conditionals, components, fragments, and nested dynamic structures keep the
221
+ React boundary. Duplicate keys, an adoption shape mismatch, or a parent-driven static markup change
222
+ remounts the complete container through React.
223
+
224
+ Controlled host fields can use the hybrid keyed-row path:
225
+
226
+ ```tsx
227
+ <ul>
228
+ {items.map((item) => (
229
+ <li key={item.id}>
230
+ <input
231
+ value={item.label}
232
+ onChange={(event) =>
233
+ setItems((current) =>
234
+ current.map((row) =>
235
+ row.id === item.id ? { ...row, label: event.currentTarget.value } : row,
236
+ ),
237
+ )
238
+ }
239
+ />
240
+ <input type="checkbox" checked={item.done} onChange={/* keyed update */} />
241
+ </li>
242
+ ))}
243
+ </ul>
244
+ ```
245
+
246
+ React owns the controls, events, hydration, and every structural commit. Farm patches `value`,
247
+ `checked`, selected options, and dependent row bindings by stable key. It preserves focused text
248
+ selection and leaves composition events on React. When `event.currentTarget` is referenced inside
249
+ a queued functional compiler-state updater, generated code snapshots the referenced property while
250
+ the handler is active. Static input types and static option attributes are required. File inputs,
251
+ dynamic types or options, `contentEditable`, refs, custom controls, textarea children beside
252
+ `value`, and async or non-inline handlers keep the React fallback.
253
+
254
+ For custom rows or an explicit key selector, use the public component:
255
+
256
+ ```tsx
257
+ import { List } from "@farm.js/react/list";
258
+
259
+ <div>
260
+ <List each={items} by={(item) => item.id}>
261
+ {(item) => <StatefulRow item={item} />}
262
+ </List>
263
+ </div>;
264
+ ```
265
+
266
+ `List` also works with the compiler disabled. `each` accepts an iterable, `null`, or `undefined`;
267
+ `by` supplies the React key; and the child function returns one React element. An inline host-only
268
+ row inside a dedicated container can use compiler-owned keyed rows. A custom row such as
269
+ `StatefulRow` remains a React-owned keyed boundary, which is necessary for its Hooks, lifecycle,
270
+ and Fiber state. Put Hooks inside the row component, not directly inside the iteration
271
+ callback. The optimized explicit shape also requires inline `by` and child functions, a safe
272
+ `each` expression, an item-derived key, and a statically known location. Other shapes keep normal
273
+ React behavior.
274
+
275
+ A normal child component can become an automatic React-owned island:
276
+
277
+ ```tsx
278
+ <Header title="Dashboard" />
279
+ <Chart value={count} />
280
+ ```
281
+
282
+ If `count` changes, the compiler leaves the outer component and `Header` alone and asks React to
283
+ render only the `Chart` boundary. `Chart` remains ordinary React and may use Hooks, context, local
284
+ state, effects, lifecycle, and error boundaries. The first contract accepts stable imported or
285
+ module-level identifiers with explicit compiler-safe props. Component children, spreads, `ref`,
286
+ `key`, member-expression or prop-selected component types, and identity-bearing inline prop values
287
+ fall back to the complete React component.
288
+
289
+ Generated callback refs give directly patched host elements stable private target identities. A
290
+ React-owned component may therefore return `null`, a fragment, or multiple host nodes without
291
+ shifting an unrelated compiler binding. These refs do not add attributes to SSR output.
292
+
293
+ Conditional blocks have two safe ownership levels. A dedicated container with exactly one proven
294
+ host-only conditional child can use compiler-owned branch instances. Events, keys, custom
295
+ components, fragments, refs, SVG, attribute spreads, `dangerouslySetInnerHTML`, nested dynamic
296
+ blocks, and static siblings in that container keep React ownership. The more general React-owned
297
+ conditional path still accepts supported nested conditionals, keyed lists, and component islands.
298
+ An empty ternary branch may be `null` or `false`. The inactive branch is described at build time but
299
+ is never pre-mounted or cached. If a logical `&&` evaluates to a number such as `0`, the runtime also
300
+ falls back to React so JavaScript and React rendering semantics remain exact.
301
+
302
+ All component-level boundary types share one block graph and one ID sequence. A nested
303
+ binding records its nearest conditional parent. If one state flush affects both an outer
304
+ conditional and its descendants, the runtime refreshes the mounted outer boundary once and skips
305
+ the redundant descendant refreshes. React unmounts inner boundaries normally, their subscriptions
306
+ are removed, and a later remount reads the latest compiler-cell values. Host-only keyed rows have
307
+ separate runtime instances per key. Row-local conditional IDs are scoped to that instance and
308
+ paired with its stable key, so two rows can use the same build-time slot ID without sharing data or
309
+ subscriptions. Unsupported row subtrees stay complete React-owned keyed boundaries.
65
310
 
66
311
  Unsupported components fall back to React by default. Use `onUnsupported: "warn"` for diagnostics
67
312
  or `onUnsupported: "error"` while tightening an annotated migration.
68
313
 
314
+ Enable a deterministic production-build coverage report to see which components compiled and why
315
+ the rest stayed on React:
316
+
317
+ ```ts
318
+ compiler: {
319
+ report: true,
320
+ // reportFile: "artifacts/react-compiler.json",
321
+ }
322
+ ```
323
+
324
+ The default path is `.farm/react-compiler.json`. The report covers the production browser graph and
325
+ contains project-relative module paths, compiled component names, fallback details, and fallback
326
+ reasons aggregated by count. A custom project-relative `reportFile` also enables reporting.
327
+
69
328
  The runtime test compares the same counter interaction on both paths: ordinary React performs a
70
329
  second component render and commit, while the compiled component remains at one render and one
71
330
  commit and updates its two bindings directly. This is a deterministic structural performance
72
331
  assertion; it is not presented as a cross-machine timing benchmark.
73
332
 
74
- Keyed list lowering, structural conditionals, effects, and more advanced hook support belong to
75
- later compiler groups and intentionally stay on React in this release.
333
+ Application and prototype calls, dynamic style objects, handlers outside JSX events, nested,
334
+ computed, and rest props patterns, async handlers, unkeyed or index-keyed lists, chained maps,
335
+ unsupported conditional roots, effects, and more advanced hook support intentionally stay on React
336
+ in this release. Compiler-owned keyed rows support either one dedicated host-only map/`List` or
337
+ one or more non-interactive ranges in a nested or component-root host container. Inline synchronous
338
+ events and dedicated row-local host conditional slots can use the single-list hybrid path, but
339
+ branch events, interactive ranges beside siblings, nested dynamic blocks, conditionals mixed with
340
+ siblings in one slot, non-inline or async row handlers, unsupported form shapes, custom components,
341
+ fragments, refs, SVG, and duplicate runtime keys keep or switch to React ownership.