@abide/abide 0.105.1 → 0.107.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 (100) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bin/abide.ts +4 -1
  3. package/docs/REGISTRY.md +156 -44
  4. package/docs/RULEBOOK.md +392 -88
  5. package/docs/abide.md +7245 -10248
  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 +7 -4
  28. package/src/compiler/compileApp.ts +2 -2
  29. package/src/compiler/declaredExports.ts +27 -6
  30. package/src/compiler/emitClient.ts +306 -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 +21 -14
  53. package/src/server/bridgeRelay.ts +108 -20
  54. package/src/server/claudeApi.ts +19 -11
  55. package/src/server/claudeCode.ts +19 -14
  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/bindElement.ts +4 -0
  95. package/src/ui/bindProperty.ts +70 -22
  96. package/src/ui/index.ts +1 -1
  97. package/src/ui/listenForRoutes.ts +14 -3
  98. package/src/ui/reconcile.ts +23 -28
  99. package/src/ui/regions.ts +9 -2
  100. 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,6 +556,88 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
551
556
  live.add(at)
552
557
  }
553
558
 
559
+ for (const line of walkTo(positions, live, lowering)) chain.push(line)
560
+
561
+ // THE UNITS — text, listeners, attributes, the regions, then the node handed over. The walk
562
+ // does not depend on the order.
563
+ // 32.68 — A `bind:element` CALLBACK RUNS LAST, so the node it is handed is whole: a child
564
+ // component, an arm or a list inside it has built, and `form.querySelector('input')` finds an
565
+ // `<input>` an `<Input>` makes.
566
+ // 32.67 — A ROW'S ITEM WATCHES FIRST, one after another, which is how its scope finds them
567
+ // (`Scope.itemWatch`).
568
+ const items: string[] = []
569
+ const texts: string[] = []
570
+ const handlers: string[] = []
571
+ const attributes: string[] = []
572
+ const elements: string[] = []
573
+ const regions: PlacedNode[] = []
574
+ for (const at of positions) {
575
+ if (!live.has(at)) continue
576
+ if (at.kind === 'region') {
577
+ regions.push(at)
578
+ continue
579
+ }
580
+ const slot = at.name.slice('__c'.length)
581
+ if (at.kind === 'text') {
582
+ if (readsOwnItem(at.run, lowering))
583
+ items.push(textUnit(at, slot, lowering, 'itemWatch'))
584
+ else texts.push(textUnit(at, slot, lowering, 'watch'))
585
+ continue
586
+ }
587
+ const units = elementUnits(
588
+ `${lowering.use('elementHere')}(${at.name}, ${JSON.stringify(at.tag)})`,
589
+ bound.get(at) as ClassifiedAttribute[],
590
+ slot,
591
+ lowering,
592
+ )
593
+ for (const unit of units.items) items.push(unit)
594
+ for (const unit of units.handlers) handlers.push(unit)
595
+ for (const unit of units.attributes) attributes.push(unit)
596
+ for (const unit of units.elements) elements.push(unit)
597
+ }
598
+ for (const unit of items) lines.push(unit)
599
+ for (const unit of texts) lines.push(unit)
600
+ for (const unit of handlers) lines.push(unit)
601
+ for (const unit of attributes) lines.push(unit)
602
+
603
+ // 32.67 — WHAT A ROW'S REGIONS AND COMPONENTS BIND reads the row's item through its cell: the
604
+ // row's `update` reaches its own top level and no further.
605
+ lowering.nestItems(() => {
606
+ for (const at of regions) {
607
+ const holder = at.holder
608
+ if (holder.kind === 'block') {
609
+ if (holder.block === 'for') emitList(at, holder, region)
610
+ else if (holder.block === 'try') emitTry(at, holder, region)
611
+ else emitBlock(at, holder, region)
612
+ } else if (holder.kind === 'element') {
613
+ if (isSlot(holder)) emitSlot(at, region)
614
+ else emitInvocation(at, holder, region)
615
+ }
616
+ }
617
+ for (const unit of elements) lines.push(unit)
618
+ // 32.43 — A REGION THAT MOUNTS NOTHING INVOKES NOTHING, so what it declares is reached
619
+ // from nowhere and is not declared: a row declaring a component it never invokes stays a
620
+ // row whose mount is the shared empty one.
621
+ if (lines.length > 0) region.defs = definitions(sink, region)
622
+ })
623
+ return region
624
+ }
625
+
626
+ /**
627
+ * The walk to every position in `live`: one `const` per position it needs, each found from the one
628
+ * before it — the region's `chain` (`emitRegion`), and a row's `update` (32.67).
629
+ *
630
+ * THE WALK IS PRUNED. A position is walked when something at or after it in its parent needs it —
631
+ * a binding, or a later position found through it — so a static subtree and a static tail are
632
+ * never visited. A blank run standing before something walked is put back as the walk passes
633
+ * (`runHere`), because what follows it is found from it.
634
+ */
635
+ function walkTo(
636
+ positions: readonly PlacedNode[],
637
+ live: ReadonlySet<PlacedNode>,
638
+ lowering: Lowering,
639
+ ): string[] {
640
+ const chain: string[] = []
554
641
  // BACKWARDS, so everything after a position — its later siblings and its own descendants — is
555
642
  // decided before it is. `followed` holds the frames, keyed by the element they stand inside,
556
643
  // that have a walked position later than the one being decided.
@@ -583,92 +670,64 @@ function emitRegion(sink: Cursor, region: RegionEmission): RegionEmission {
583
670
  else chain.push(`const ${at.name} = ${at.from}`)
584
671
  }
585
672
 
586
- // THE UNITS, in the order a page always installed them — text, listeners, attributes, the node
587
- // handed over, then the regions. The walk does not depend on it; a `bind:element` callback has
588
- // always run with every other binding already in place, and that is kept.
589
- const texts: string[] = []
590
- const handlers: string[] = []
591
- const attributes: string[] = []
592
- const elements: string[] = []
593
- const regions: PlacedNode[] = []
594
- for (const at of positions) {
595
- if (!live.has(at)) continue
596
- if (at.kind === 'region') {
597
- regions.push(at)
598
- continue
599
- }
600
- const slot = at.name.slice('__c'.length)
601
- 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
- })`,
673
+ return chain
674
+ }
675
+
676
+ /** 32.67 — whether a text run reads a row's item at that row's own top level (`readsOwnItem`). */
677
+ function readsOwnItem(
678
+ run: readonly TemplateNode[],
679
+ lowering: Lowering,
680
+ ): boolean {
681
+ for (const piece of run)
682
+ if (piece.kind === 'expression' && lowering.readsOwnItem(piece))
683
+ return true
684
+ return false
685
+ }
686
+
687
+ /**
688
+ * One text run's write: the run recomputed whole, as markup where a part is `Raw` (32.26) and as
689
+ * text otherwise — kept written by a watch where `watched`, and written once for a row's `update`.
690
+ *
691
+ * RESOLVED ONCE, and the binding closes over what it resolved to. The node a position lands on is
692
+ * the same one on every run while the run holds text — the walk put a blank run's back and
693
+ * `setText` never removes it — so re-finding it per write is the same answer bought again: measured
694
+ * in Chromium at 0.47 µs a write against 0.22 µs with the node held. 32.26 is the one write that
695
+ * changes it: a `Raw` part turns the run into an anchored one (`setMarkup`) and a later text value
696
+ * turns it back, so each write hands back what the run now is and the binding holds that.
697
+ */
698
+ function textUnit(
699
+ at: PlacedNode,
700
+ slot: string,
701
+ lowering: Lowering,
702
+ watched: 'watch' | 'itemWatch' | null,
703
+ ): string {
704
+ const values: string[] = []
705
+ const brands: string[] = []
706
+ const text: string[] = []
707
+ const markup: string[] = []
708
+ for (let piece = 0; piece < at.run.length; piece += 1) {
709
+ const node = at.run[piece] as TemplateNode
710
+ if (node.kind === 'text') {
711
+ markup.push(JSON.stringify(node.text))
712
+ // 32.27 — static text is markup, and `data` is not a parse: a piece with an entity in
713
+ // it goes in as the characters it stands for, decoded by the build.
714
+ text.push(
715
+ JSON.stringify(decodeCharacterReferences(node.text, 'text')),
638
716
  )
639
717
  continue
640
718
  }
641
- const units = elementUnits(
642
- `${lowering.use('elementHere')}(${at.name}, ${JSON.stringify(at.tag)})`,
643
- bound.get(at) as ClassifiedAttribute[],
644
- slot,
645
- lowering,
646
- )
647
- for (const unit of units.handlers) handlers.push(unit)
648
- for (const unit of units.attributes) attributes.push(unit)
649
- for (const unit of units.elements) elements.push(unit)
650
- }
651
- for (const unit of texts) lines.push(unit)
652
- for (const unit of handlers) lines.push(unit)
653
- for (const unit of attributes) lines.push(unit)
654
- for (const unit of elements) lines.push(unit)
655
-
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)
665
- }
719
+ const value = `__v${slot}_${piece}`
720
+ values.push(`${value} = ${lowering.binding(node)}`)
721
+ brands.push(`${lowering.use('isRaw')}(${value})`)
722
+ text.push(`${lowering.use('escapeless')}(${value})`)
723
+ markup.push(`${lowering.use('escapeText')}(${value})`)
666
724
  }
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)
671
- return region
725
+ const write = ` const ${values.join(', ')}
726
+ __t${slot} = ${brands.join(' || ')} ? ${lowering.use('setMarkup')}(__t${slot}, ${markup.join(' + ')}) : ${lowering.use('setText')}(__t${slot}, ${text.join(' + ')})`
727
+ const found = `let __t${slot} = ${lowering.use('textHere')}(${at.name})`
728
+ return watched === null
729
+ ? `{\n${found}\n${write}\n}`
730
+ : `${found}\n${SCOPE}.${watched}(() => {\n${write}\n})`
672
731
  }
673
732
 
674
733
  /**
@@ -686,18 +745,25 @@ function elementUnits(
686
745
  classified: ClassifiedAttribute[],
687
746
  slot: string,
688
747
  lowering: Lowering,
689
- ): { handlers: string[]; attributes: string[]; elements: string[] } {
748
+ ): {
749
+ items: string[]
750
+ handlers: string[]
751
+ attributes: string[]
752
+ elements: string[]
753
+ } {
754
+ const items: string[] = []
690
755
  const handlers: string[] = []
691
756
  const attributes: string[] = []
692
757
  const elements: string[] = []
693
758
  const writes: string[] = []
759
+ const itemWrites: string[] = []
694
760
  const binds: string[] = []
695
761
  const spreads: string[] = []
696
762
  for (const one of classified) {
697
763
  if (!isBound(one)) continue
698
764
  if (one.kind === 'handler')
699
765
  handlers.push(
700
- `${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.expression(one.attribute)})`,
766
+ `${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.listener(one.attribute)})`,
701
767
  )
702
768
  // 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
703
769
  // region's scope writes the property, and a listener writes it back.
@@ -732,16 +798,25 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
732
798
  write: 'attribute' as const,
733
799
  name: one.attribute.name,
734
800
  }
735
- writes.push(
736
- `${SCOPE}.watch(() => ${lowering.use(WRITES[write])}(__a${slot}, ${JSON.stringify(name)}, ${lowering.expression(one.attribute)}))`,
737
- )
801
+ // 32.67 — one reading a row's item is one of the row's item watches, on a name of its
802
+ // own because it is installed apart from the element's other attributes.
803
+ if (lowering.readsOwnItem(one.attribute))
804
+ itemWrites.push(
805
+ `${SCOPE}.itemWatch(() => ${lowering.use(WRITES[write])}(__i${slot}, ${JSON.stringify(name)}, ${lowering.binding(one.attribute)}))`,
806
+ )
807
+ else
808
+ writes.push(
809
+ `${SCOPE}.watch(() => ${lowering.use(WRITES[write])}(__a${slot}, ${JSON.stringify(name)}, ${lowering.binding(one.attribute)}))`,
810
+ )
738
811
  }
739
812
  }
813
+ if (itemWrites.length > 0)
814
+ items.push(`const __i${slot} = ${element}\n${itemWrites.join('\n')}`)
740
815
  if (writes.length > 0)
741
816
  attributes.push(`const __a${slot} = ${element}\n${writes.join('\n')}`)
742
817
  for (const unit of binds) attributes.push(unit)
743
818
  for (const unit of spreads) attributes.push(unit)
744
- return { handlers, attributes, elements }
819
+ return { items, handlers, attributes, elements }
745
820
  }
746
821
 
747
822
  // A region's bindings go through its SCOPE, so what replaced the region can stop them (see
@@ -1331,11 +1406,38 @@ function emitList(
1331
1406
  : written
1332
1407
 
1333
1408
  const single = isSingleRooted(block.children)
1409
+ // 32.67 — the row's RECORD, under a name of the list's own, for a listener reading the item off
1410
+ // it: a row nested in this one takes `ROW_ROOT` for its own record.
1411
+ const record = `${name}_row`
1334
1412
 
1335
- const rowRegion = emitRegion(row, region.nested())
1413
+ let rowRegion = region.nested()
1414
+ const mounted = lowering.readItemsAs(block, 'own', record, () => {
1415
+ rowRegion = emitRegion(row, region.nested())
1416
+ })
1417
+ let local = ''
1418
+ const setUp =
1419
+ block.setup === null
1420
+ ? { cell: 0, record: 0 }
1421
+ : lowering.readItemsAs(block, 'nested', record, () => {
1422
+ local = lowering.setup(block.setup as Setup)
1423
+ })
1336
1424
 
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}`
1425
+ // MADE FROM THE ITEM'S VALUE: every slot reading the item alone is written before the row is
1426
+ // inserted, so making a row writes the document once per node (32.67).
1427
+ const valued = `const ${item} = ${ROW_ROOT}.data`
1428
+ const make = `const ${name}_make = (__document, ${ROW_ROOT}, ${at}) => {\n ${valued}\n ${emitMaker(row, lowering, () => rowMarkup(row, !single, lowering, CLIENT_LANE), !single)}\n}`
1429
+ const walk = rowRegion.body()
1430
+ // A PATTERN READ OFF THE RECORD is read through one picker a name, the list's rather than the
1431
+ // row's, so a row allocates nothing for it.
1432
+ if (mounted.record > 0 && !isNamedAlone(item))
1433
+ for (const read of lowering.itemNames(block))
1434
+ region.helpers.push(
1435
+ `const ${record}_${read} = (__value) => { const ${item} = __value; return ${read} }`,
1436
+ )
1437
+ const opened =
1438
+ walk.length === 0
1439
+ ? walk
1440
+ : [...itemBinding(block, item, mounted, record, lowering), ...walk]
1339
1441
 
1340
1442
  // A row that cannot be hydrated is REBUILT by the same `make`, in place, and the row's record
1341
1443
  // takes its new first node.
@@ -1343,16 +1445,17 @@ function emitList(
1343
1445
  make,
1344
1446
  mountFunction(
1345
1447
  name,
1346
- `${ROW_ROOT}, ${item}, ${at}`,
1448
+ `${ROW_ROOT}, ${at}`,
1347
1449
  // The row's walk, INSIDE the mount's one `try` rather than above it, because a row
1348
1450
  // rebuilds whole: a node that refused is the row disagreeing, and re-making it is the
1349
1451
  // answer the page does not have.
1350
- rowRegion.body(),
1351
- `${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make(${ROW_ROOT}.node.ownerDocument, ${item}, ${at}))`,
1452
+ opened,
1453
+ `${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make(${ROW_ROOT}.node.ownerDocument, ${ROW_ROOT}, ${at}))`,
1352
1454
  lowering,
1353
1455
  row.defersSetup,
1354
1456
  block.setup !== null,
1355
1457
  ),
1458
+ rowUpdate(name, row, block, valued, at, lowering),
1356
1459
  ].join('\n')
1357
1460
  // THE LIST CALLS `_setUp` ONCE PER ROW (`List.setUp`). 14.8 — a row with a branch-local
1358
1461
  // `<script>` runs it there, and the row's `make` and `mount` are declared after it, so both close
@@ -1365,13 +1468,17 @@ function emitList(
1365
1468
  block.setup !== null
1366
1469
  ? rowSetUp(
1367
1470
  name,
1368
- item,
1471
+ [
1472
+ ...itemBinding(block, item, setUp, record, lowering),
1473
+ // A setup reading nothing of the item still sees its value.
1474
+ ...(setUp.cell === 0 ? [valued] : []),
1475
+ ].join('\n'),
1369
1476
  at,
1370
- lowering.setup(block.setup),
1477
+ local,
1371
1478
  builders,
1372
1479
  lowering,
1373
1480
  )
1374
- : `${builders}\nconst ${name}_builders = { make: ${name}_make, mount: ${name}_mount }\nconst ${name}_setUp = () => ${name}_builders`,
1481
+ : `${builders}\nconst ${name}_builders = { make: ${name}_make, mount: ${name}_mount, update: ${name}_update }\nconst ${name}_setUp = () => ${name}_builders`,
1375
1482
  ].join('\n'),
1376
1483
  )
1377
1484
 
@@ -1386,6 +1493,101 @@ ${SCOPE}.watch(() => ${lowering.use('updateList')}(${name}_list, ${name}_subject
1386
1493
  )
1387
1494
  }
1388
1495
 
1496
+ /**
1497
+ * Binds a row function's item names for how `uses` says they were read (32.67): its record under
1498
+ * `record` for a listener, and the row's cell for everything else — the name itself where the item
1499
+ * is one name, and an accessor per name destructuring the cell's value where it is a pattern.
1500
+ */
1501
+ function itemBinding(
1502
+ block: object,
1503
+ item: string,
1504
+ uses: ItemUses,
1505
+ record: string,
1506
+ lowering: Lowering,
1507
+ ): string[] {
1508
+ const lines: string[] = []
1509
+ if (uses.record > 0) lines.push(`const ${record} = ${ROW_ROOT}`)
1510
+ if (uses.cell === 0) return lines
1511
+ const cell = `${lowering.use('rowCell')}(${ROW_ROOT})`
1512
+ if (isNamedAlone(item)) {
1513
+ lines.push(`const ${item} = ${cell}`)
1514
+ return lines
1515
+ }
1516
+ lines.push(`const __cell = ${cell}`)
1517
+ for (const name of lowering.itemNames(block))
1518
+ lines.push(
1519
+ `const ${name} = () => { const ${item} = __cell(); return ${name} }`,
1520
+ )
1521
+ return lines
1522
+ }
1523
+
1524
+ /**
1525
+ * A row's `update` (32.67): the walk to each text and attribute slot of the row's own top level that
1526
+ * reads the item and nothing that moves, and one write of each from the record's value.
1527
+ *
1528
+ * WHAT `make` WROTE FROM THE ITEM, written again — and nothing a binding keeps written. A bound
1529
+ * slot is its watch's alone (`Scope.itemWatch`), rerun when the row takes a new item (`updateRow`):
1530
+ * a run's node can change kind under a write (32.26), and what the slot reads can change with the
1531
+ * item, so the node and the subscriptions are each held by one writer. A region of the row binds
1532
+ * its own. A row with no such slot shares one `update` that does nothing.
1533
+ */
1534
+ function rowUpdate(
1535
+ name: string,
1536
+ row: Cursor,
1537
+ block: object,
1538
+ valued: string,
1539
+ at: string,
1540
+ lowering: Lowering,
1541
+ ): string {
1542
+ const live = new Set<PlacedNode>()
1543
+ const writes: string[] = []
1544
+ for (const position of row.positions) {
1545
+ if (position.kind === 'region') continue
1546
+ const slot = position.name.slice('__c'.length)
1547
+ if (position.kind === 'text') {
1548
+ let reads = false
1549
+ for (const piece of position.run)
1550
+ if (piece.kind === 'expression')
1551
+ reads ||= lowering.readsItem(piece, block)
1552
+ if (!reads || lowering.isLiveRun(position.run)) continue
1553
+ live.add(position)
1554
+ writes.push(textUnit(position, slot, lowering, null))
1555
+ continue
1556
+ }
1557
+ const element = `${lowering.use('elementHere')}(${position.name}, ${JSON.stringify(position.tag)})`
1558
+ const own: string[] = []
1559
+ for (const one of classifyAttributes(
1560
+ position.holder as ElementNode,
1561
+ lowering,
1562
+ )) {
1563
+ if (!one.attribute.expression || one.live) continue
1564
+ if (!lowering.readsItem(one.attribute, block)) continue
1565
+ const toggles =
1566
+ one.kind === 'merged'
1567
+ ? one.toggles
1568
+ : one.kind === 'dynamic'
1569
+ ? {
1570
+ write: 'attribute' as const,
1571
+ name: one.attribute.name,
1572
+ }
1573
+ : undefined
1574
+ if (toggles === undefined) continue
1575
+ own.push(
1576
+ `${lowering.use(WRITES[toggles.write])}(__a${slot}, ${JSON.stringify(toggles.name)}, ${lowering.expression(one.attribute)})`,
1577
+ )
1578
+ }
1579
+ if (own.length === 0) continue
1580
+ live.add(position)
1581
+ writes.push(`{\nconst __a${slot} = ${element}\n${own.join('\n')}\n}`)
1582
+ }
1583
+ if (writes.length === 0)
1584
+ return `const ${name}_update = ${lowering.use('noUpdate')}`
1585
+ return `const ${name}_update = (${ROW_ROOT}, ${at}) => {
1586
+ ${valued}
1587
+ ${[...walkTo(row.positions, live, lowering), ...writes].join('\n')}
1588
+ }`
1589
+ }
1590
+
1389
1591
  /**
1390
1592
  * A row's `_setUp` where the row has a branch-local `<script>` (14.8): runs it once for the row,
1391
1593
  * and answers the builders that close over its names.
@@ -1394,18 +1596,19 @@ ${SCOPE}.watch(() => ${lowering.use('updateList')}(${name}_list, ${name}_subject
1394
1596
  */
1395
1597
  function rowSetUp(
1396
1598
  name: string,
1397
- item: string,
1599
+ bound: string,
1398
1600
  at: string,
1399
1601
  local: string,
1400
1602
  builders: string,
1401
1603
  lowering: Lowering,
1402
1604
  ): string {
1403
1605
  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}) => {
1606
+ // The mount takes the setUp's own record and index again, so the row record `List` hands in is
1607
+ // the only thing that is new.
1608
+ return `const ${name}_setUp = (${ROW_ROOT}, ${at}) => {
1609
+ ${bound}
1407
1610
  ${owning.opened}${local}
1408
1611
  ${owning.handed}${builders}
1409
- return { make: ${name}_make, mount: ${name}_mount }${owning.closed}
1612
+ return { make: ${name}_make, mount: ${name}_mount, update: ${name}_update }${owning.closed}
1410
1613
  }`
1411
1614
  }
@@ -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), \`class:\` and \`style:\` set an element's own class and style (RULEBOOK 32.10), and \`bind:element\` hands over an element — put it on one inside <${node.tag}>, or pass the callback as a prop (RULEBOOK 32.69).`,
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
+ }