@abide/abide 0.105.0 → 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.
- package/CHANGELOG.md +24 -0
- package/bin/abide.ts +4 -1
- package/docs/REGISTRY.md +156 -44
- package/docs/RULEBOOK.md +375 -87
- package/docs/abide.md +7243 -10256
- package/package.json +4 -1
- package/scaffold/AGENTS.md +2 -0
- package/scaffold/src/getGreeting.ts +6 -0
- package/scaffold/src/index.abide +3 -1
- package/scaffold/src/tests/getGreeting.test.ts +6 -0
- package/scaffold/src/tests/index.test.ts +18 -0
- package/src/compiler/DOCS_SITE.ts +2 -0
- package/src/compiler/Lowering.ts +234 -14
- package/src/compiler/PositionTable.ts +14 -7
- package/src/compiler/PositionedRefusal.ts +13 -0
- package/src/compiler/Refusals.ts +18 -11
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowMap.ts +37 -0
- package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
- package/src/compiler/TypeSession.ts +410 -31
- package/src/compiler/appLayout.ts +18 -2
- package/src/compiler/argsSchemaOf.ts +55 -9
- package/src/compiler/checkApp.ts +33 -0
- package/src/compiler/checkedShadow.ts +36 -22
- package/src/compiler/citedClauses.ts +14 -0
- package/src/compiler/classifyRead.ts +6 -0
- package/src/compiler/classifyTemplate.ts +2 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +27 -6
- package/src/compiler/emitClient.ts +304 -103
- package/src/compiler/handlersUnder.ts +4 -0
- package/src/compiler/invocationProps.ts +1 -1
- package/src/compiler/isIgnored.ts +27 -0
- package/src/compiler/parseAbide.ts +176 -76
- package/src/compiler/projectShadow.ts +30 -6
- package/src/compiler/readSources.ts +1 -1
- package/src/compiler/readsIn.ts +100 -1
- package/src/compiler/renderMarkup.ts +36 -5
- package/src/compiler/scaffold.ts +19 -6
- package/src/compiler/serveLanguage.ts +156 -21
- package/src/compiler/templateContext.ts +150 -0
- package/src/compiler/tokenize.ts +6 -0
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/Inbox.ts +8 -0
- package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
- package/src/server/MOUNTABLE.ts +6 -0
- package/src/server/abideModules.ts +8 -1
- package/src/server/answerRequest.ts +3 -1
- package/src/server/answerRpc.ts +26 -12
- package/src/server/answerSocket.ts +26 -1
- package/src/server/argsFor.ts +4 -3
- package/src/server/assistant.ts +19 -13
- package/src/server/bridgeRelay.ts +108 -20
- package/src/server/claudeApi.ts +7 -7
- package/src/server/claudeCode.ts +11 -11
- package/src/server/commands.ts +73 -9
- package/src/server/createApp.ts +41 -7
- package/src/server/devWorker.ts +3 -3
- package/src/server/generatedSurfaces.ts +37 -4
- package/src/server/muxUpgrade.ts +47 -0
- package/src/server/preload.ts +13 -2
- package/src/server/render.ts +4 -1
- package/src/server/runCompiled.ts +1 -0
- package/src/server/seedBuffer.ts +31 -1
- package/src/server/serveDev.ts +83 -72
- package/src/server/surfaceOf.ts +3 -0
- package/src/server/testMount.ts +53 -0
- package/src/shared/ADDRESSES.ts +1 -1
- package/src/shared/ERROR_META.ts +3 -0
- package/src/shared/Schema.ts +1 -1
- package/src/shared/WORK.ts +14 -12
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/boundValue.ts +81 -0
- package/src/shared/index.ts +7 -2
- package/src/shared/jsonSchema.ts +25 -2
- package/src/shared/log.ts +11 -1
- package/src/shared/reactive/Keyed.ts +30 -6
- package/src/shared/reactive/PATCHES.ts +1 -1
- package/src/shared/reactive/Reactive.ts +118 -60
- package/src/shared/reactive/canonicalKey.ts +1 -1
- package/src/shared/reactive/index.ts +13 -5
- package/src/shared/reactive/recordWrites.ts +1 -1
- package/src/shared/reactive/watch.ts +26 -4
- package/src/shared/rpc.ts +6 -1
- package/src/shared/url.ts +7 -3
- package/src/shared/validateJson.ts +1 -1
- package/src/ui/ArmRegion.ts +2 -2
- package/src/ui/List.ts +9 -9
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +194 -36
- package/src/ui/Scope.ts +29 -0
- package/src/ui/adoptWalk.ts +3 -3
- package/src/ui/applyHead.ts +25 -3
- package/src/ui/bindProperty.ts +70 -22
- package/src/ui/index.ts +1 -1
- package/src/ui/leave.ts +5 -0
- package/src/ui/listenForRoutes.ts +14 -3
- package/src/ui/reconcile.ts +48 -28
- package/src/ui/regions.ts +36 -4
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
): {
|
|
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.
|
|
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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1338
|
-
|
|
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}, ${
|
|
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
|
-
|
|
1351
|
-
`${lowering.use('replaceRow')}(${ROW_ROOT}, ${name}_make(${ROW_ROOT}.node.ownerDocument, ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1405
|
-
// the
|
|
1406
|
-
return `const ${name}_setUp = (${
|
|
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
|
+
}
|