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

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
@@ -62,7 +62,8 @@ The current compiler handles components that it can prove have:
62
62
  - optional synchronous `const` or function-declaration handlers used by JSX events;
63
63
  - state-driven text, attributes, per-property inline styles, and controlled form properties;
64
64
  - host-rooted `condition && <element>` and `condition ? <element> : <element>` child blocks at
65
- statically known locations, including supported nested boundaries;
65
+ statically known locations, including direct ranges among static siblings and at the component
66
+ root;
66
67
  - item-keyed `collection.map(...)` children and explicit `List` boundaries at statically known
67
68
  container locations, including supported non-mutating collection pipelines;
68
69
  - inline synchronous events inside otherwise eligible host-only keyed rows;
@@ -73,8 +74,8 @@ The current compiler handles components that it can prove have:
73
74
 
74
75
  The generated component preserves React ownership of initial placement, props, events, SSR, and
75
76
  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
77
+ targets. Proven host containers can transfer child ownership after mount for host-only conditional
78
+ branches and ranges, plus non-interactive host-only keyed rows. An interactive row or a row with
78
79
  local conditional slots uses a hybrid boundary instead: React retains its events, conditional
79
80
  Fibers, and structural reconciliation, while Farm patches same-key bindings and refreshes only
80
81
  changed row-local branches. React still creates or hydrates all initial DOM. Anything outside those
@@ -96,6 +97,26 @@ or hydration, a same-branch update patches those bindings without replacing the
96
97
  change creates, removes, or replaces only the selected branch. No marker node is added to SSR output.
97
98
  More complex conditional shapes keep the existing React-owned conditional boundary.
98
99
 
100
+ Eligible direct conditionals can also share a host container with static host siblings:
101
+
102
+ ```tsx
103
+ <article data-update={updates}>
104
+ <header>Status</header>
105
+ {loading && <p>Loading {updates}</p>}
106
+ <section>Stable content</section>
107
+ {enabled ? <strong>Enabled</strong> : <span>Disabled</span>}
108
+ <footer>Actions</footer>
109
+ </article>
110
+ ```
111
+
112
+ That `article` may be the component's exact returned root. Farm adopts the original container and
113
+ counts its direct static elements as anchors for the conditional ranges. Same-branch bindings patch
114
+ in place; branch changes insert, remove, or replace only their range. Multiple changed ranges use
115
+ one state snapshot and reconcile from right to left, including when adjacent ranges are empty. The
116
+ root and every unchanged static sibling keep their DOM identity, and no wrapper or marker is added.
117
+ Branch events, components, fragments, refs, SVG, spreads, nested dynamic blocks, or another
118
+ unsupported direct child keep the container on the React-owned path.
119
+
99
120
  For a common keyed map, no new API is required:
100
121
 
101
122
  ```tsx
@@ -290,14 +311,16 @@ Generated callback refs give directly patched host elements stable private targe
290
311
  React-owned component may therefore return `null`, a fragment, or multiple host nodes without
291
312
  shifting an unrelated compiler binding. These refs do not add attributes to SSR output.
292
313
 
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.
314
+ Conditional blocks have three safe ownership levels. One or more proven host-only direct branches
315
+ may become compiler-owned ranges among static host siblings, including at the exact component root.
316
+ A dedicated container with exactly one conditional child uses the smaller compiler-owned branch
317
+ boundary. The more general React-owned conditional path accepts supported events, nested host
318
+ conditionals, keyed lists, and component islands. Keys, custom components, fragments, refs, SVG,
319
+ attribute spreads, `dangerouslySetInnerHTML`, nested dynamic blocks, and branch events keep React
320
+ ownership. An empty ternary branch may be `null` or `false`. The inactive branch is described at
321
+ build time but is never pre-mounted or cached. If a logical `&&` evaluates to a number such as `0`,
322
+ or adopted DOM no longer matches its descriptor, the affected container remounts through React so
323
+ JavaScript, hydration, and React rendering semantics remain exact.
301
324
 
302
325
  All component-level boundary types share one block graph and one ID sequence. A nested
303
326
  binding records its nearest conditional parent. If one state flush affects both an outer
@@ -510,6 +510,181 @@ function createHostConditionalBlockComponent(owner) {
510
510
  FarmHostConditionalBlock.displayName = "FarmCompiledHostConditional";
511
511
  return FarmHostConditionalBlock;
512
512
  }
513
+ function createConditionalRangesBlockComponent(owner) {
514
+ class FarmConditionalRangesBlock extends React__default.default.Component {
515
+ constructor() {
516
+ super(...arguments);
517
+ this.state = { fallback: false };
518
+ this.root = null;
519
+ this.mounted = false;
520
+ this.fallbackRequested = false;
521
+ this.fallbackVersion = 0;
522
+ this.propFallbackQueued = false;
523
+ this.currentProps = this.props;
524
+ this.rangeInstances = [];
525
+ this.staticSegments = [];
526
+ this.captureRoot = (root) => {
527
+ this.root = root;
528
+ this.currentProps.rootRef?.(root);
529
+ };
530
+ this.refresh = (afterCommit) => {
531
+ this.reconcile(afterCommit);
532
+ };
533
+ }
534
+ readSelections(props) {
535
+ const selections = [];
536
+ for (const range of props.ranges) {
537
+ const selection = hostConditionalSelection(range);
538
+ if (selection.kind === "fallback") return null;
539
+ selections.push(selection);
540
+ }
541
+ return selections;
542
+ }
543
+ adopt(props = this.currentProps) {
544
+ if (!this.root || !Number.isSafeInteger(props.trailing) || props.trailing < 0) return false;
545
+ const selections = this.readSelections(props);
546
+ if (!selections) return false;
547
+ const elements = [...this.root.children];
548
+ const staticSegments = [];
549
+ const instances = [];
550
+ let cursor = 0;
551
+ for (let rangeIndex = 0; rangeIndex < props.ranges.length; rangeIndex += 1) {
552
+ const range = props.ranges[rangeIndex];
553
+ if (!Number.isSafeInteger(range.before) || range.before < 0) return false;
554
+ const staticEnd = cursor + range.before;
555
+ if (staticEnd > elements.length) return false;
556
+ staticSegments.push(elements.slice(cursor, staticEnd));
557
+ cursor = staticEnd;
558
+ const selection = selections[rangeIndex];
559
+ if (selection.kind === "empty") {
560
+ instances.push(null);
561
+ continue;
562
+ }
563
+ const element = elements[cursor];
564
+ if (!element) return false;
565
+ const descriptor = selection.branch.create();
566
+ if (!matchesCompilerHostElement(element, descriptor)) return false;
567
+ instances.push({
568
+ key: selection.key,
569
+ host: {
570
+ element,
571
+ values: readHostConditionalBindingValues(selection.branch)
572
+ }
573
+ });
574
+ cursor += 1;
575
+ }
576
+ if (cursor + props.trailing !== elements.length) return false;
577
+ staticSegments.push(elements.slice(cursor));
578
+ this.staticSegments = staticSegments;
579
+ this.rangeInstances = instances;
580
+ return true;
581
+ }
582
+ anchorAfter(rangeIndex) {
583
+ for (let next = rangeIndex + 1; next < this.rangeInstances.length; next += 1) {
584
+ const staticAnchor = this.staticSegments[next]?.[0];
585
+ if (staticAnchor) return staticAnchor;
586
+ const rangeAnchor = this.rangeInstances[next]?.host.element;
587
+ if (rangeAnchor) return rangeAnchor;
588
+ }
589
+ return this.staticSegments[this.rangeInstances.length]?.[0] || null;
590
+ }
591
+ reconcileRange(rangeIndex, selection) {
592
+ if (!this.root) return;
593
+ const previous = this.rangeInstances[rangeIndex];
594
+ if (selection.kind === "empty") {
595
+ previous?.host.element.remove();
596
+ this.rangeInstances[rangeIndex] = null;
597
+ return;
598
+ }
599
+ if (previous?.key === selection.key) {
600
+ applyHostConditionalBindings(selection.branch, previous.host);
601
+ return;
602
+ }
603
+ const element = createCompilerHostElement(this.root.ownerDocument, selection.branch.create());
604
+ if (previous) previous.host.element.replaceWith(element);
605
+ else this.root.insertBefore(element, this.anchorAfter(rangeIndex));
606
+ this.rangeInstances[rangeIndex] = {
607
+ key: selection.key,
608
+ host: {
609
+ element,
610
+ values: readHostConditionalBindingValues(selection.branch)
611
+ }
612
+ };
613
+ }
614
+ reconcile(afterCommit) {
615
+ if (!this.mounted || !this.root) {
616
+ afterCommit?.();
617
+ return;
618
+ }
619
+ if (this.state.fallback) {
620
+ this.fallbackVersion += 1;
621
+ this.forceUpdate(afterCommit);
622
+ return;
623
+ }
624
+ if (this.currentProps.ranges.length !== this.rangeInstances.length) {
625
+ this.activateFallback(afterCommit);
626
+ return;
627
+ }
628
+ const selections = this.readSelections(this.currentProps);
629
+ if (!selections) {
630
+ this.activateFallback(afterCommit);
631
+ return;
632
+ }
633
+ for (let rangeIndex = this.rangeInstances.length - 1; rangeIndex >= 0; rangeIndex -= 1) {
634
+ this.reconcileRange(rangeIndex, selections[rangeIndex]);
635
+ }
636
+ afterCommit?.();
637
+ }
638
+ activateFallback(afterCommit) {
639
+ if (!this.mounted || this.state.fallback || this.fallbackRequested) {
640
+ afterCommit?.();
641
+ return;
642
+ }
643
+ this.fallbackRequested = true;
644
+ this.setState({ fallback: true }, afterCommit);
645
+ }
646
+ schedulePropFallback() {
647
+ if (this.propFallbackQueued) return;
648
+ this.propFallbackQueued = true;
649
+ queueMicrotask(() => {
650
+ this.propFallbackQueued = false;
651
+ if (this.mounted && !this.state.fallback) this.activateFallback();
652
+ });
653
+ }
654
+ shouldComponentUpdate(nextProps, nextState) {
655
+ this.currentProps = nextProps;
656
+ if (nextState.fallback || this.state.fallback) return true;
657
+ this.schedulePropFallback();
658
+ return false;
659
+ }
660
+ componentDidMount() {
661
+ this.mounted = true;
662
+ this.unsubscribe = owner.subscribe(this.props.id, this.refresh);
663
+ if (!this.adopt()) this.activateFallback();
664
+ }
665
+ componentWillUnmount() {
666
+ this.mounted = false;
667
+ this.unsubscribe?.();
668
+ this.root = null;
669
+ this.rangeInstances = [];
670
+ this.staticSegments = [];
671
+ }
672
+ render() {
673
+ const container = this.currentProps.render();
674
+ if (!React__default.default.isValidElement(container) || typeof container.type !== "string") {
675
+ throw new TypeError(
676
+ `Compiled conditional ranges ${this.currentProps.id} must own one host container.`
677
+ );
678
+ }
679
+ return React__default.default.cloneElement(container, {
680
+ key: this.state.fallback ? `react-${this.fallbackVersion}` : "compiled",
681
+ ref: this.captureRoot
682
+ });
683
+ }
684
+ }
685
+ FarmConditionalRangesBlock.displayName = "FarmCompiledConditionalRanges";
686
+ return FarmConditionalRangesBlock;
687
+ }
513
688
  function longestIncreasingSubsequencePositions(sequence) {
514
689
  const tails = [];
515
690
  const previous = Array.from({ length: sequence.length }, () => -1);
@@ -1209,6 +1384,9 @@ function createCompiledComponent(definition) {
1209
1384
  HostConditional: createHostConditionalBlockComponent({
1210
1385
  subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
1211
1386
  }),
1387
+ ConditionalRanges: createConditionalRangesBlockComponent({
1388
+ subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
1389
+ }),
1212
1390
  KeyedList: createKeyedListBlockComponent({
1213
1391
  subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
1214
1392
  }),
@@ -1385,6 +1563,12 @@ function createCompiledComponent(definition) {
1385
1563
  rootRef: this.captureRoot
1386
1564
  });
1387
1565
  }
1566
+ if (element.type === this.blockRuntime.ConditionalRanges) {
1567
+ return React__default.default.cloneElement(
1568
+ element,
1569
+ { rootRef: this.captureRoot }
1570
+ );
1571
+ }
1388
1572
  if (typeof element.type !== "string") {
1389
1573
  throw new TypeError(
1390
1574
  `Compiled component ${currentDefinition.displayName} must return one host element.`