@abide/abide 0.105.1 → 0.106.0

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/bin/abide.ts +4 -1
  3. package/docs/REGISTRY.md +156 -44
  4. package/docs/RULEBOOK.md +375 -87
  5. package/docs/abide.md +7243 -10256
  6. package/package.json +4 -1
  7. package/scaffold/AGENTS.md +2 -0
  8. package/scaffold/src/getGreeting.ts +6 -0
  9. package/scaffold/src/index.abide +3 -1
  10. package/scaffold/src/tests/getGreeting.test.ts +6 -0
  11. package/scaffold/src/tests/index.test.ts +18 -0
  12. package/src/compiler/DOCS_SITE.ts +2 -0
  13. package/src/compiler/Lowering.ts +234 -14
  14. package/src/compiler/PositionTable.ts +14 -7
  15. package/src/compiler/PositionedRefusal.ts +13 -0
  16. package/src/compiler/Refusals.ts +18 -11
  17. package/src/compiler/SHARED_NAMES.ts +2 -0
  18. package/src/compiler/ShadowMap.ts +37 -0
  19. package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
  20. package/src/compiler/TypeSession.ts +410 -31
  21. package/src/compiler/appLayout.ts +18 -2
  22. package/src/compiler/argsSchemaOf.ts +55 -9
  23. package/src/compiler/checkApp.ts +33 -0
  24. package/src/compiler/checkedShadow.ts +36 -22
  25. package/src/compiler/citedClauses.ts +14 -0
  26. package/src/compiler/classifyRead.ts +6 -0
  27. package/src/compiler/classifyTemplate.ts +2 -1
  28. package/src/compiler/compileApp.ts +2 -2
  29. package/src/compiler/declaredExports.ts +27 -6
  30. package/src/compiler/emitClient.ts +304 -103
  31. package/src/compiler/handlersUnder.ts +4 -0
  32. package/src/compiler/invocationProps.ts +1 -1
  33. package/src/compiler/isIgnored.ts +27 -0
  34. package/src/compiler/parseAbide.ts +176 -76
  35. package/src/compiler/projectShadow.ts +30 -6
  36. package/src/compiler/readSources.ts +1 -1
  37. package/src/compiler/readsIn.ts +100 -1
  38. package/src/compiler/renderMarkup.ts +36 -5
  39. package/src/compiler/scaffold.ts +19 -6
  40. package/src/compiler/serveLanguage.ts +156 -21
  41. package/src/compiler/templateContext.ts +150 -0
  42. package/src/compiler/tokenize.ts +6 -0
  43. package/src/server/BRIDGE_VERSION.ts +1 -1
  44. package/src/server/Inbox.ts +8 -0
  45. package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
  46. package/src/server/MOUNTABLE.ts +6 -0
  47. package/src/server/abideModules.ts +8 -1
  48. package/src/server/answerRequest.ts +3 -1
  49. package/src/server/answerRpc.ts +26 -12
  50. package/src/server/answerSocket.ts +26 -1
  51. package/src/server/argsFor.ts +4 -3
  52. package/src/server/assistant.ts +19 -13
  53. package/src/server/bridgeRelay.ts +108 -20
  54. package/src/server/claudeApi.ts +7 -7
  55. package/src/server/claudeCode.ts +11 -11
  56. package/src/server/commands.ts +73 -9
  57. package/src/server/createApp.ts +41 -7
  58. package/src/server/devWorker.ts +3 -3
  59. package/src/server/generatedSurfaces.ts +37 -4
  60. package/src/server/muxUpgrade.ts +47 -0
  61. package/src/server/preload.ts +13 -2
  62. package/src/server/render.ts +4 -1
  63. package/src/server/runCompiled.ts +1 -0
  64. package/src/server/seedBuffer.ts +31 -1
  65. package/src/server/serveDev.ts +83 -72
  66. package/src/server/surfaceOf.ts +3 -0
  67. package/src/server/testMount.ts +53 -0
  68. package/src/shared/ADDRESSES.ts +1 -1
  69. package/src/shared/ERROR_META.ts +3 -0
  70. package/src/shared/Schema.ts +1 -1
  71. package/src/shared/WORK.ts +14 -12
  72. package/src/shared/applyPatch.ts +1 -1
  73. package/src/shared/boundValue.ts +81 -0
  74. package/src/shared/index.ts +7 -2
  75. package/src/shared/jsonSchema.ts +25 -2
  76. package/src/shared/log.ts +11 -1
  77. package/src/shared/reactive/Keyed.ts +30 -6
  78. package/src/shared/reactive/PATCHES.ts +1 -1
  79. package/src/shared/reactive/Reactive.ts +118 -60
  80. package/src/shared/reactive/canonicalKey.ts +1 -1
  81. package/src/shared/reactive/index.ts +13 -5
  82. package/src/shared/reactive/recordWrites.ts +1 -1
  83. package/src/shared/reactive/watch.ts +26 -4
  84. package/src/shared/rpc.ts +6 -1
  85. package/src/shared/url.ts +7 -3
  86. package/src/shared/validateJson.ts +1 -1
  87. package/src/ui/ArmRegion.ts +2 -2
  88. package/src/ui/List.ts +9 -9
  89. package/src/ui/RawRun.ts +1 -1
  90. package/src/ui/Router.ts +194 -36
  91. package/src/ui/Scope.ts +29 -0
  92. package/src/ui/adoptWalk.ts +3 -3
  93. package/src/ui/applyHead.ts +25 -3
  94. package/src/ui/bindProperty.ts +70 -22
  95. package/src/ui/index.ts +1 -1
  96. package/src/ui/listenForRoutes.ts +14 -3
  97. package/src/ui/reconcile.ts +23 -28
  98. package/src/ui/regions.ts +9 -2
  99. package/src/ui/rows.ts +108 -28
@@ -27,7 +27,12 @@ import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
27
27
  import { moduleImports } from './emitComponent.ts'
28
28
  import { invocationProps, passedName } from './invocationProps.ts'
29
29
  import { isNamedAlone } from './isNamedAlone.ts'
30
- import { type LoweredChunk, Lowering, PROPS } from './Lowering.ts'
30
+ import {
31
+ type ItemUses,
32
+ type LoweredChunk,
33
+ Lowering,
34
+ PROPS,
35
+ } from './Lowering.ts'
31
36
  import type { AbideFile } from './parseAbide.ts'
32
37
  import {
33
38
  CLIENT_LANE,
@@ -551,41 +556,14 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
551
556
  live.add(at)
552
557
  }
553
558
 
554
- // BACKWARDS, so everything after a position — its later siblings and its own descendants — is
555
- // decided before it is. `followed` holds the frames, keyed by the element they stand inside,
556
- // that have a walked position later than the one being decided.
557
- const walked = new Set<PlacedNode>()
558
- const followed = new Set<PlacedNode | null>()
559
- for (let index = positions.length - 1; index >= 0; index -= 1) {
560
- const at = positions[index] as PlacedNode
561
- if (!live.has(at) && !followed.has(at) && !followed.has(at.within))
562
- continue
563
- walked.add(at)
564
- followed.add(at.within)
565
- }
566
-
567
- for (const at of positions) {
568
- if (!walked.has(at)) continue
569
- if (at.kind === 'region')
570
- chain.push(
571
- `const ${at.name} = ${lowering.use('regionHere')}(${at.from})`,
572
- )
573
- // A RUN THAT CAN HOLD MARKUP (32.26) is stepped over to its closing anchor where it does,
574
- // and a blank one is put back where it rendered nothing — the same step (`runHere`). A run
575
- // of proven parts is text for good and is its node as found.
576
- else if (
577
- at.kind === 'text' &&
578
- (at.blank || lowering.holdsMarkup(at.run))
579
- )
580
- chain.push(
581
- `const ${at.name} = ${lowering.use('runHere')}(${at.from}, ${at.blank ? at.parent : 'null'})`,
582
- )
583
- else chain.push(`const ${at.name} = ${at.from}`)
584
- }
559
+ for (const line of walkTo(positions, live, lowering)) chain.push(line)
585
560
 
586
561
  // THE UNITS, in the order a page always installed them — text, listeners, attributes, the node
587
562
  // handed over, then the regions. The walk does not depend on it; a `bind:element` callback has
588
563
  // always run with every other binding already in place, and that is kept.
564
+ // 32.67 — A ROW'S ITEM WATCHES FIRST, one after another, which is how its scope finds them
565
+ // (`Scope.itemWatch`).
566
+ const items: string[] = []
589
567
  const texts: string[] = []
590
568
  const handlers: string[] = []
591
569
  const attributes: string[] = []
@@ -599,43 +577,9 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
599
577
  }
600
578
  const slot = at.name.slice('__c'.length)
601
579
  if (at.kind === 'text') {
602
- const values: string[] = []
603
- const brands: string[] = []
604
- const text: string[] = []
605
- const markup: string[] = []
606
- for (let piece = 0; piece < at.run.length; piece += 1) {
607
- const node = at.run[piece] as TemplateNode
608
- if (node.kind === 'text') {
609
- markup.push(JSON.stringify(node.text))
610
- // 32.27 — static text is markup, and `data` is not a parse: a piece with an
611
- // entity in it goes in as the characters it stands for, decoded by the build.
612
- text.push(
613
- JSON.stringify(
614
- decodeCharacterReferences(node.text, 'text'),
615
- ),
616
- )
617
- continue
618
- }
619
- const value = `__v${slot}_${piece}`
620
- values.push(`${value} = ${lowering.expression(node)}`)
621
- brands.push(`${lowering.use('isRaw')}(${value})`)
622
- text.push(`${lowering.use('escapeless')}(${value})`)
623
- markup.push(`${lowering.use('escapeText')}(${value})`)
624
- }
625
- // RESOLVED ONCE, and the binding closes over what it resolved to. The node a position
626
- // lands on is the same one on every run while the run holds text — the walk put a blank
627
- // run's back and `setText` never removes it — so re-finding it per write is the same
628
- // answer bought again: measured in Chromium at 0.47 µs a write against 0.22 µs with the
629
- // node held. 32.26 is the one write that changes it: a `Raw` part turns the run into an
630
- // anchored one (`setMarkup`) and a later text value turns it back, so each write hands
631
- // back what the run now is and the binding holds that.
632
- texts.push(
633
- `let __t${slot} = ${lowering.use('textHere')}(${at.name})
634
- ${SCOPE}.watch(() => {
635
- const ${values.join(', ')}
636
- __t${slot} = ${brands.join(' || ')} ? ${lowering.use('setMarkup')}(__t${slot}, ${markup.join(' + ')}) : ${lowering.use('setText')}(__t${slot}, ${text.join(' + ')})
637
- })`,
638
- )
580
+ if (readsOwnItem(at.run, lowering))
581
+ items.push(textUnit(at, slot, lowering, 'itemWatch'))
582
+ else texts.push(textUnit(at, slot, lowering, 'watch'))
639
583
  continue
640
584
  }
641
585
  const units = elementUnits(
@@ -644,33 +588,146 @@ ${SCOPE}.watch(() => {
644
588
  slot,
645
589
  lowering,
646
590
  )
591
+ for (const unit of units.items) items.push(unit)
647
592
  for (const unit of units.handlers) handlers.push(unit)
648
593
  for (const unit of units.attributes) attributes.push(unit)
649
594
  for (const unit of units.elements) elements.push(unit)
650
595
  }
596
+ for (const unit of items) lines.push(unit)
651
597
  for (const unit of texts) lines.push(unit)
652
598
  for (const unit of handlers) lines.push(unit)
653
599
  for (const unit of attributes) lines.push(unit)
654
600
  for (const unit of elements) lines.push(unit)
655
601
 
656
- for (const at of regions) {
657
- const holder = at.holder
658
- if (holder.kind === 'block') {
659
- if (holder.block === 'for') emitList(at, holder, region)
660
- else if (holder.block === 'try') emitTry(at, holder, region)
661
- else emitBlock(at, holder, region)
662
- } else if (holder.kind === 'element') {
663
- if (isSlot(holder)) emitSlot(at, region)
664
- else emitInvocation(at, holder, region)
602
+ // 32.67 — WHAT A ROW'S REGIONS AND COMPONENTS BIND reads the row's item through its cell: the
603
+ // row's `update` reaches its own top level and no further.
604
+ lowering.nestItems(() => {
605
+ for (const at of regions) {
606
+ const holder = at.holder
607
+ if (holder.kind === 'block') {
608
+ if (holder.block === 'for') emitList(at, holder, region)
609
+ else if (holder.block === 'try') emitTry(at, holder, region)
610
+ else emitBlock(at, holder, region)
611
+ } else if (holder.kind === 'element') {
612
+ if (isSlot(holder)) emitSlot(at, region)
613
+ else emitInvocation(at, holder, region)
614
+ }
665
615
  }
666
- }
667
- // 32.43 — A REGION THAT MOUNTS NOTHING INVOKES NOTHING, so what it declares is reached from
668
- // nowhere and is not declared: a row declaring a component it never invokes stays a row whose
669
- // mount is the shared empty one.
670
- if (lines.length > 0) region.defs = definitions(sink, region)
616
+ // 32.43 — A REGION THAT MOUNTS NOTHING INVOKES NOTHING, so what it declares is reached
617
+ // from nowhere and is not declared: a row declaring a component it never invokes stays a
618
+ // row whose mount is the shared empty one.
619
+ if (lines.length > 0) region.defs = definitions(sink, region)
620
+ })
671
621
  return region
672
622
  }
673
623
 
624
+ /**
625
+ * The walk to every position in `live`: one `const` per position it needs, each found from the one
626
+ * before it — the region's `chain` (`emitRegion`), and a row's `update` (32.67).
627
+ *
628
+ * THE WALK IS PRUNED. A position is walked when something at or after it in its parent needs it —
629
+ * a binding, or a later position found through it — so a static subtree and a static tail are
630
+ * never visited. A blank run standing before something walked is put back as the walk passes
631
+ * (`runHere`), because what follows it is found from it.
632
+ */
633
+ function walkTo(
634
+ positions: readonly PlacedNode[],
635
+ live: ReadonlySet<PlacedNode>,
636
+ lowering: Lowering,
637
+ ): string[] {
638
+ const chain: string[] = []
639
+ // BACKWARDS, so everything after a position — its later siblings and its own descendants — is
640
+ // decided before it is. `followed` holds the frames, keyed by the element they stand inside,
641
+ // that have a walked position later than the one being decided.
642
+ const walked = new Set<PlacedNode>()
643
+ const followed = new Set<PlacedNode | null>()
644
+ for (let index = positions.length - 1; index >= 0; index -= 1) {
645
+ const at = positions[index] as PlacedNode
646
+ if (!live.has(at) && !followed.has(at) && !followed.has(at.within))
647
+ continue
648
+ walked.add(at)
649
+ followed.add(at.within)
650
+ }
651
+
652
+ for (const at of positions) {
653
+ if (!walked.has(at)) continue
654
+ if (at.kind === 'region')
655
+ chain.push(
656
+ `const ${at.name} = ${lowering.use('regionHere')}(${at.from})`,
657
+ )
658
+ // A RUN THAT CAN HOLD MARKUP (32.26) is stepped over to its closing anchor where it does,
659
+ // and a blank one is put back where it rendered nothing — the same step (`runHere`). A run
660
+ // of proven parts is text for good and is its node as found.
661
+ else if (
662
+ at.kind === 'text' &&
663
+ (at.blank || lowering.holdsMarkup(at.run))
664
+ )
665
+ chain.push(
666
+ `const ${at.name} = ${lowering.use('runHere')}(${at.from}, ${at.blank ? at.parent : 'null'})`,
667
+ )
668
+ else chain.push(`const ${at.name} = ${at.from}`)
669
+ }
670
+
671
+ return chain
672
+ }
673
+
674
+ /** 32.67 — whether a text run reads a row's item at that row's own top level (`readsOwnItem`). */
675
+ function readsOwnItem(
676
+ run: readonly TemplateNode[],
677
+ lowering: Lowering,
678
+ ): boolean {
679
+ for (const piece of run)
680
+ if (piece.kind === 'expression' && lowering.readsOwnItem(piece))
681
+ return true
682
+ return false
683
+ }
684
+
685
+ /**
686
+ * One text run's write: the run recomputed whole, as markup where a part is `Raw` (32.26) and as
687
+ * text otherwise — kept written by a watch where `watched`, and written once for a row's `update`.
688
+ *
689
+ * RESOLVED ONCE, and the binding closes over what it resolved to. The node a position lands on is
690
+ * the same one on every run while the run holds text — the walk put a blank run's back and
691
+ * `setText` never removes it — so re-finding it per write is the same answer bought again: measured
692
+ * in Chromium at 0.47 µs a write against 0.22 µs with the node held. 32.26 is the one write that
693
+ * changes it: a `Raw` part turns the run into an anchored one (`setMarkup`) and a later text value
694
+ * turns it back, so each write hands back what the run now is and the binding holds that.
695
+ */
696
+ function textUnit(
697
+ at: PlacedNode,
698
+ slot: string,
699
+ lowering: Lowering,
700
+ watched: 'watch' | 'itemWatch' | null,
701
+ ): string {
702
+ const values: string[] = []
703
+ const brands: string[] = []
704
+ const text: string[] = []
705
+ const markup: string[] = []
706
+ for (let piece = 0; piece < at.run.length; piece += 1) {
707
+ const node = at.run[piece] as TemplateNode
708
+ if (node.kind === 'text') {
709
+ markup.push(JSON.stringify(node.text))
710
+ // 32.27 — static text is markup, and `data` is not a parse: a piece with an entity in
711
+ // it goes in as the characters it stands for, decoded by the build.
712
+ text.push(
713
+ JSON.stringify(decodeCharacterReferences(node.text, 'text')),
714
+ )
715
+ continue
716
+ }
717
+ const value = `__v${slot}_${piece}`
718
+ values.push(`${value} = ${lowering.binding(node)}`)
719
+ brands.push(`${lowering.use('isRaw')}(${value})`)
720
+ text.push(`${lowering.use('escapeless')}(${value})`)
721
+ markup.push(`${lowering.use('escapeText')}(${value})`)
722
+ }
723
+ const write = ` const ${values.join(', ')}
724
+ __t${slot} = ${brands.join(' || ')} ? ${lowering.use('setMarkup')}(__t${slot}, ${markup.join(' + ')}) : ${lowering.use('setText')}(__t${slot}, ${text.join(' + ')})`
725
+ const found = `let __t${slot} = ${lowering.use('textHere')}(${at.name})`
726
+ return watched === null
727
+ ? `{\n${found}\n${write}\n}`
728
+ : `${found}\n${SCOPE}.${watched}(() => {\n${write}\n})`
729
+ }
730
+
674
731
  /**
675
732
  * One bound element's units — its listeners, its attribute bindings and the node it hands over —
676
733
  * against the element `element` evaluates to.
@@ -686,18 +743,25 @@ function elementUnits(
686
743
  classified: ClassifiedAttribute[],
687
744
  slot: string,
688
745
  lowering: Lowering,
689
- ): { handlers: string[]; attributes: string[]; elements: string[] } {
746
+ ): {
747
+ items: string[]
748
+ handlers: string[]
749
+ attributes: string[]
750
+ elements: string[]
751
+ } {
752
+ const items: string[] = []
690
753
  const handlers: string[] = []
691
754
  const attributes: string[] = []
692
755
  const elements: string[] = []
693
756
  const writes: string[] = []
757
+ const itemWrites: string[] = []
694
758
  const binds: string[] = []
695
759
  const spreads: string[] = []
696
760
  for (const one of classified) {
697
761
  if (!isBound(one)) continue
698
762
  if (one.kind === 'handler')
699
763
  handlers.push(
700
- `${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.expression(one.attribute)})`,
764
+ `${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.listener(one.attribute)})`,
701
765
  )
702
766
  // 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
703
767
  // region's scope writes the property, and a listener writes it back.
@@ -732,16 +796,25 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
732
796
  write: 'attribute' as const,
733
797
  name: one.attribute.name,
734
798
  }
735
- writes.push(
736
- `${SCOPE}.watch(() => ${lowering.use(WRITES[write])}(__a${slot}, ${JSON.stringify(name)}, ${lowering.expression(one.attribute)}))`,
737
- )
799
+ // 32.67 — one reading a row's item is one of the row's item watches, on a name of its
800
+ // own because it is installed apart from the element's other attributes.
801
+ if (lowering.readsOwnItem(one.attribute))
802
+ itemWrites.push(
803
+ `${SCOPE}.itemWatch(() => ${lowering.use(WRITES[write])}(__i${slot}, ${JSON.stringify(name)}, ${lowering.binding(one.attribute)}))`,
804
+ )
805
+ else
806
+ writes.push(
807
+ `${SCOPE}.watch(() => ${lowering.use(WRITES[write])}(__a${slot}, ${JSON.stringify(name)}, ${lowering.binding(one.attribute)}))`,
808
+ )
738
809
  }
739
810
  }
811
+ if (itemWrites.length > 0)
812
+ items.push(`const __i${slot} = ${element}\n${itemWrites.join('\n')}`)
740
813
  if (writes.length > 0)
741
814
  attributes.push(`const __a${slot} = ${element}\n${writes.join('\n')}`)
742
815
  for (const unit of binds) attributes.push(unit)
743
816
  for (const unit of spreads) attributes.push(unit)
744
- return { handlers, attributes, elements }
817
+ return { items, handlers, attributes, elements }
745
818
  }
746
819
 
747
820
  // A region's bindings go through its SCOPE, so what replaced the region can stop them (see
@@ -1331,11 +1404,38 @@ function emitList(
1331
1404
  : written
1332
1405
 
1333
1406
  const single = isSingleRooted(block.children)
1407
+ // 32.67 — the row's RECORD, under a name of the list's own, for a listener reading the item off
1408
+ // it: a row nested in this one takes `ROW_ROOT` for its own record.
1409
+ const record = `${name}_row`
1334
1410
 
1335
- const rowRegion = emitRegion(row, region.nested())
1411
+ let rowRegion = region.nested()
1412
+ const mounted = lowering.readItemsAs(block, 'own', record, () => {
1413
+ rowRegion = emitRegion(row, region.nested())
1414
+ })
1415
+ let local = ''
1416
+ const setUp =
1417
+ block.setup === null
1418
+ ? { cell: 0, record: 0 }
1419
+ : lowering.readItemsAs(block, 'nested', record, () => {
1420
+ local = lowering.setup(block.setup as Setup)
1421
+ })
1336
1422
 
1337
- // `List` hands the row its document — see `emitMaker` for which rows are parsed instead.
1338
- const make = `const ${name}_make = (__document, ${item}, ${at}) => {\n ${emitMaker(row, lowering, () => rowMarkup(row, !single, lowering, CLIENT_LANE), !single)}\n}`
1423
+ // MADE FROM THE ITEM'S VALUE: every slot reading the item alone is written before the row is
1424
+ // inserted, so making a row writes the document once per node (32.67).
1425
+ const valued = `const ${item} = ${ROW_ROOT}.data`
1426
+ const make = `const ${name}_make = (__document, ${ROW_ROOT}, ${at}) => {\n ${valued}\n ${emitMaker(row, lowering, () => rowMarkup(row, !single, lowering, CLIENT_LANE), !single)}\n}`
1427
+ const walk = rowRegion.body()
1428
+ // A PATTERN READ OFF THE RECORD is read through one picker a name, the list's rather than the
1429
+ // row's, so a row allocates nothing for it.
1430
+ if (mounted.record > 0 && !isNamedAlone(item))
1431
+ for (const read of lowering.itemNames(block))
1432
+ region.helpers.push(
1433
+ `const ${record}_${read} = (__value) => { const ${item} = __value; return ${read} }`,
1434
+ )
1435
+ const opened =
1436
+ walk.length === 0
1437
+ ? walk
1438
+ : [...itemBinding(block, item, mounted, record, lowering), ...walk]
1339
1439
 
1340
1440
  // A row that cannot be hydrated is REBUILT by the same `make`, in place, and the row's record
1341
1441
  // takes its new first node.
@@ -1343,16 +1443,17 @@ function emitList(
1343
1443
  make,
1344
1444
  mountFunction(
1345
1445
  name,
1346
- `${ROW_ROOT}, ${item}, ${at}`,
1446
+ `${ROW_ROOT}, ${at}`,
1347
1447
  // The row's walk, INSIDE the mount's one `try` rather than above it, because a row
1348
1448
  // rebuilds whole: a node that refused is the row disagreeing, and re-making it is the
1349
1449
  // answer the page does not have.
1350
- rowRegion.body(),
1351
- `${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make(${ROW_ROOT}.node.ownerDocument, ${item}, ${at}))`,
1450
+ opened,
1451
+ `${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make(${ROW_ROOT}.node.ownerDocument, ${ROW_ROOT}, ${at}))`,
1352
1452
  lowering,
1353
1453
  row.defersSetup,
1354
1454
  block.setup !== null,
1355
1455
  ),
1456
+ rowUpdate(name, row, block, valued, at, lowering),
1356
1457
  ].join('\n')
1357
1458
  // THE LIST CALLS `_setUp` ONCE PER ROW (`List.setUp`). 14.8 — a row with a branch-local
1358
1459
  // `<script>` runs it there, and the row's `make` and `mount` are declared after it, so both close
@@ -1365,13 +1466,17 @@ function emitList(
1365
1466
  block.setup !== null
1366
1467
  ? rowSetUp(
1367
1468
  name,
1368
- item,
1469
+ [
1470
+ ...itemBinding(block, item, setUp, record, lowering),
1471
+ // A setup reading nothing of the item still sees its value.
1472
+ ...(setUp.cell === 0 ? [valued] : []),
1473
+ ].join('\n'),
1369
1474
  at,
1370
- lowering.setup(block.setup),
1475
+ local,
1371
1476
  builders,
1372
1477
  lowering,
1373
1478
  )
1374
- : `${builders}\nconst ${name}_builders = { make: ${name}_make, mount: ${name}_mount }\nconst ${name}_setUp = () => ${name}_builders`,
1479
+ : `${builders}\nconst ${name}_builders = { make: ${name}_make, mount: ${name}_mount, update: ${name}_update }\nconst ${name}_setUp = () => ${name}_builders`,
1375
1480
  ].join('\n'),
1376
1481
  )
1377
1482
 
@@ -1386,6 +1491,101 @@ ${SCOPE}.watch(() => ${lowering.use('updateList')}(${name}_list, ${name}_subject
1386
1491
  )
1387
1492
  }
1388
1493
 
1494
+ /**
1495
+ * Binds a row function's item names for how `uses` says they were read (32.67): its record under
1496
+ * `record` for a listener, and the row's cell for everything else — the name itself where the item
1497
+ * is one name, and an accessor per name destructuring the cell's value where it is a pattern.
1498
+ */
1499
+ function itemBinding(
1500
+ block: object,
1501
+ item: string,
1502
+ uses: ItemUses,
1503
+ record: string,
1504
+ lowering: Lowering,
1505
+ ): string[] {
1506
+ const lines: string[] = []
1507
+ if (uses.record > 0) lines.push(`const ${record} = ${ROW_ROOT}`)
1508
+ if (uses.cell === 0) return lines
1509
+ const cell = `${lowering.use('rowCell')}(${ROW_ROOT})`
1510
+ if (isNamedAlone(item)) {
1511
+ lines.push(`const ${item} = ${cell}`)
1512
+ return lines
1513
+ }
1514
+ lines.push(`const __cell = ${cell}`)
1515
+ for (const name of lowering.itemNames(block))
1516
+ lines.push(
1517
+ `const ${name} = () => { const ${item} = __cell(); return ${name} }`,
1518
+ )
1519
+ return lines
1520
+ }
1521
+
1522
+ /**
1523
+ * A row's `update` (32.67): the walk to each text and attribute slot of the row's own top level that
1524
+ * reads the item and nothing that moves, and one write of each from the record's value.
1525
+ *
1526
+ * WHAT `make` WROTE FROM THE ITEM, written again — and nothing a binding keeps written. A bound
1527
+ * slot is its watch's alone (`Scope.itemWatch`), rerun when the row takes a new item (`updateRow`):
1528
+ * a run's node can change kind under a write (32.26), and what the slot reads can change with the
1529
+ * item, so the node and the subscriptions are each held by one writer. A region of the row binds
1530
+ * its own. A row with no such slot shares one `update` that does nothing.
1531
+ */
1532
+ function rowUpdate(
1533
+ name: string,
1534
+ row: Cursor,
1535
+ block: object,
1536
+ valued: string,
1537
+ at: string,
1538
+ lowering: Lowering,
1539
+ ): string {
1540
+ const live = new Set<PlacedNode>()
1541
+ const writes: string[] = []
1542
+ for (const position of row.positions) {
1543
+ if (position.kind === 'region') continue
1544
+ const slot = position.name.slice('__c'.length)
1545
+ if (position.kind === 'text') {
1546
+ let reads = false
1547
+ for (const piece of position.run)
1548
+ if (piece.kind === 'expression')
1549
+ reads ||= lowering.readsItem(piece, block)
1550
+ if (!reads || lowering.isLiveRun(position.run)) continue
1551
+ live.add(position)
1552
+ writes.push(textUnit(position, slot, lowering, null))
1553
+ continue
1554
+ }
1555
+ const element = `${lowering.use('elementHere')}(${position.name}, ${JSON.stringify(position.tag)})`
1556
+ const own: string[] = []
1557
+ for (const one of classifyAttributes(
1558
+ position.holder as ElementNode,
1559
+ lowering,
1560
+ )) {
1561
+ if (!one.attribute.expression || one.live) continue
1562
+ if (!lowering.readsItem(one.attribute, block)) continue
1563
+ const toggles =
1564
+ one.kind === 'merged'
1565
+ ? one.toggles
1566
+ : one.kind === 'dynamic'
1567
+ ? {
1568
+ write: 'attribute' as const,
1569
+ name: one.attribute.name,
1570
+ }
1571
+ : undefined
1572
+ if (toggles === undefined) continue
1573
+ own.push(
1574
+ `${lowering.use(WRITES[toggles.write])}(__a${slot}, ${JSON.stringify(toggles.name)}, ${lowering.expression(one.attribute)})`,
1575
+ )
1576
+ }
1577
+ if (own.length === 0) continue
1578
+ live.add(position)
1579
+ writes.push(`{\nconst __a${slot} = ${element}\n${own.join('\n')}\n}`)
1580
+ }
1581
+ if (writes.length === 0)
1582
+ return `const ${name}_update = ${lowering.use('noUpdate')}`
1583
+ return `const ${name}_update = (${ROW_ROOT}, ${at}) => {
1584
+ ${valued}
1585
+ ${[...walkTo(row.positions, live, lowering), ...writes].join('\n')}
1586
+ }`
1587
+ }
1588
+
1389
1589
  /**
1390
1590
  * A row's `_setUp` where the row has a branch-local `<script>` (14.8): runs it once for the row,
1391
1591
  * and answers the builders that close over its names.
@@ -1394,18 +1594,19 @@ ${SCOPE}.watch(() => ${lowering.use('updateList')}(${name}_list, ${name}_subject
1394
1594
  */
1395
1595
  function rowSetUp(
1396
1596
  name: string,
1397
- item: string,
1597
+ bound: string,
1398
1598
  at: string,
1399
1599
  local: string,
1400
1600
  builders: string,
1401
1601
  lowering: Lowering,
1402
1602
  ): string {
1403
1603
  const owning = scopedSetup(local, lowering, true)
1404
- // The mount takes the setUp's own item and index again, shadowing them with the same values, so
1405
- // the row record `List` hands in is the only thing that is new.
1406
- return `const ${name}_setUp = (${item}, ${at}) => {
1604
+ // The mount takes the setUp's own record and index again, so the row record `List` hands in is
1605
+ // the only thing that is new.
1606
+ return `const ${name}_setUp = (${ROW_ROOT}, ${at}) => {
1607
+ ${bound}
1407
1608
  ${owning.opened}${local}
1408
1609
  ${owning.handed}${builders}
1409
- return { make: ${name}_make, mount: ${name}_mount }${owning.closed}
1610
+ return { make: ${name}_make, mount: ${name}_mount, update: ${name}_update }${owning.closed}
1410
1611
  }`
1411
1612
  }
@@ -58,6 +58,8 @@ export async function handlersUnder(
58
58
  paths.push(file.path)
59
59
  named.push(file.relative)
60
60
  }
61
+ // The project is asked of below, so the session keeps its snapshot until then.
62
+ using _held = session.hold()
61
63
  const project = await session.projectWith(paths)
62
64
  for (const source of serving) {
63
65
  paths.push(session.projectionOf(source.path))
@@ -125,6 +127,7 @@ export async function handlersUnder(
125
127
  copy: handler.copy,
126
128
  defaults: handler.defaults,
127
129
  args: handler.args,
130
+ result: handler.result,
128
131
  })
129
132
  warnings.push(...handler.unread)
130
133
  } catch (thrown) {
@@ -155,6 +158,7 @@ export async function handlersUnder(
155
158
  copy: each.copy,
156
159
  defaults: each.defaults,
157
160
  args: each.args,
161
+ result: each.result,
158
162
  })
159
163
  warnings.push(...each.unread)
160
164
  }
@@ -31,7 +31,7 @@ export function invocationProps(
31
31
  const key = propKeyOf(attribute)
32
32
  if (key === undefined)
33
33
  throw new Error(
34
- `abide: ${attribute.name} on <${node.tag}> is not a prop a component takes — \`bind:\` hands over the \`Reactive\` in its braces (32.6), and \`class:\` and \`style:\` set an element's own class and style. RULEBOOK 32.10.`,
34
+ `abide: ${attribute.name} on <${node.tag}> is not a prop a component takes — \`bind:\` hands over the \`Reactive\` in its braces (RULEBOOK 32.6), and \`class:\` and \`style:\` set an element's own class and style. RULEBOOK 32.10.`,
35
35
  )
36
36
  // A STATIC PROP IS STILL AN ACCESSOR. One shape per prop whatever the call site spelled
37
37
  // (33.10), so the child reads every key the same way and a literal is not a second case.
@@ -0,0 +1,27 @@
1
+ import { citedClauses } from './citedClauses.ts'
2
+
3
+ /**
4
+ * 14.19 — whether the line above `offset` in `text` silences the warning `message`: it holds
5
+ * `abide-ignore` followed by the number of a clause the message cites, in a script comment or a
6
+ * markup one alike. A directive naming another clause silences nothing, so one silence never hides
7
+ * a second warning on the same read.
8
+ */
9
+ export function isIgnored(
10
+ text: string,
11
+ offset: number,
12
+ message: string,
13
+ ): boolean {
14
+ const cited = citedClauses(message)
15
+ if (cited.length === 0) return false
16
+ const lineStart = text.lastIndexOf('\n', offset - 1)
17
+ if (lineStart <= 0) return false
18
+ const above = text.slice(
19
+ text.lastIndexOf('\n', lineStart - 1) + 1,
20
+ lineStart,
21
+ )
22
+ const directive = /abide-ignore((?:[ \t]+\d+\.\d+)+)/.exec(above)
23
+ if (directive === null) return false
24
+ for (const clause of (directive[1] as string).trim().split(/\s+/))
25
+ if (cited.includes(clause)) return true
26
+ return false
27
+ }