@abide/abide 0.101.0 → 0.103.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 (161) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/abide.ts +226 -164
  3. package/docs/REGISTRY.md +730 -0
  4. package/docs/RULEBOOK.md +2768 -0
  5. package/docs/abide.md +13127 -0
  6. package/package.json +6 -3
  7. package/scaffold/AGENTS.md +38 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +341 -71
  13. package/src/compiler/appMap.ts +96 -0
  14. package/src/compiler/argsSchemaOf.ts +1 -1
  15. package/src/compiler/checkApp.ts +53 -8
  16. package/src/compiler/checkedShadow.ts +73 -6
  17. package/src/compiler/classifyRead.ts +6 -4
  18. package/src/compiler/classifyTemplate.ts +1 -1
  19. package/src/compiler/compileApp.ts +598 -200
  20. package/src/compiler/compiledEntry.ts +48 -40
  21. package/src/compiler/componentGraph.ts +57 -71
  22. package/src/compiler/declaredExports.ts +26 -9
  23. package/src/compiler/emitClient.ts +142 -72
  24. package/src/compiler/emitComponent.ts +41 -12
  25. package/src/compiler/emitSource.ts +102 -13
  26. package/src/compiler/handlersUnder.ts +164 -76
  27. package/src/compiler/hooksCalled.ts +35 -0
  28. package/src/compiler/index.ts +7 -0
  29. package/src/compiler/mountsOf.ts +150 -0
  30. package/src/compiler/nearest.ts +14 -0
  31. package/src/compiler/parseAbide.ts +157 -20
  32. package/src/compiler/projectShadow.ts +250 -19
  33. package/src/compiler/reactiveTypes.ts +20 -7
  34. package/src/compiler/readSources.ts +15 -2
  35. package/src/compiler/readsIn.ts +62 -8
  36. package/src/compiler/renderMarkup.ts +3 -3
  37. package/src/compiler/scaffold.ts +37 -15
  38. package/src/compiler/serveLanguage.ts +101 -34
  39. package/src/compiler/sharedModuleWrites.ts +2 -2
  40. package/src/compiler/sourceMapOf.ts +193 -0
  41. package/src/compiler/splitImports.ts +205 -83
  42. package/src/compiler/tokenize.ts +14 -0
  43. package/src/compiler/writeDefaultProject.ts +29 -2
  44. package/src/server/CLIENT_BUILD.ts +11 -13
  45. package/src/server/DEFAULT_PORT.ts +1 -1
  46. package/src/server/MODULE_QUERIES.ts +27 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +457 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +241 -107
  52. package/src/server/answerRpc.ts +107 -33
  53. package/src/server/answerSocket.ts +65 -20
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +238 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +38 -37
  65. package/src/server/componentModules.d.ts +2 -1
  66. package/src/server/compressAnswer.ts +4 -4
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +77 -79
  70. package/src/server/devWorker.ts +118 -181
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +57 -32
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +137 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +12 -7
  77. package/src/server/installSourceMaps.ts +93 -0
  78. package/src/server/launcherOptions.ts +33 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +6 -6
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/preload.ts +13 -0
  85. package/src/server/principal.ts +10 -5
  86. package/src/server/realOrAsGiven.ts +10 -0
  87. package/src/server/refuseAddressCollision.ts +21 -14
  88. package/src/server/remote.ts +49 -52
  89. package/src/server/render.ts +65 -94
  90. package/src/server/renderComponent.ts +5 -0
  91. package/src/server/reportHealth.ts +2 -2
  92. package/src/server/requestScope.ts +12 -1
  93. package/src/server/runBuild.ts +24 -0
  94. package/src/server/runCompiled.ts +5 -5
  95. package/src/server/runDesktop.ts +48 -28
  96. package/src/server/serveBuild.ts +43 -52
  97. package/src/server/serveDev.ts +79 -37
  98. package/src/server/shell.ts +7 -22
  99. package/src/server/sourceMappings.ts +139 -0
  100. package/src/server/surfaceOf.ts +20 -3
  101. package/src/server/testBuild.ts +1 -19
  102. package/src/server/withAbideSources.ts +117 -0
  103. package/src/shared/ADDRESSES.ts +10 -18
  104. package/src/shared/APP_META.ts +7 -0
  105. package/src/shared/CopyOptions.ts +1 -1
  106. package/src/shared/DEBUG_META.ts +1 -1
  107. package/src/shared/LAUNCHER_FILES.ts +9 -0
  108. package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
  109. package/src/shared/cookies.ts +2 -2
  110. package/src/shared/csp.ts +1 -1
  111. package/src/shared/escapeText.ts +1 -1
  112. package/src/shared/filledPath.ts +85 -0
  113. package/src/shared/framing.ts +79 -6
  114. package/src/shared/headKeyOf.ts +2 -0
  115. package/src/shared/index.ts +2 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/{ui → shared}/menuBridge.ts +2 -2
  119. package/src/shared/menuItem.ts +4 -4
  120. package/src/shared/onMenu.ts +58 -0
  121. package/src/shared/reactive/Keyed.ts +2 -2
  122. package/src/shared/reactive/NotLanded.ts +1 -1
  123. package/src/shared/reactive/Reactive.ts +43 -43
  124. package/src/shared/reactive/argsContract.ts +4 -4
  125. package/src/shared/reactive/canonicalKey.ts +4 -4
  126. package/src/shared/reactive/chunksOfStream.ts +6 -0
  127. package/src/shared/reactive/index.ts +1 -1
  128. package/src/shared/reactive/recordWrites.ts +103 -5
  129. package/src/shared/reactive/structural.ts +1 -1
  130. package/src/shared/reactive/tracking.ts +1 -1
  131. package/src/shared/reactive/watch.ts +3 -3
  132. package/src/shared/refuse.ts +1 -1
  133. package/src/{ui → shared}/reportMenu.ts +8 -10
  134. package/src/shared/routePattern.ts +122 -48
  135. package/src/shared/rpc.ts +37 -8
  136. package/src/shared/rpcRequest.ts +27 -3
  137. package/src/shared/url.ts +4 -44
  138. package/src/shared/writePath.ts +1 -1
  139. package/src/ui/LAYER_NAMES.ts +1 -1
  140. package/src/ui/List.ts +3 -3
  141. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  142. package/src/ui/Mismatch.ts +1 -1
  143. package/src/ui/RawRun.ts +1 -1
  144. package/src/ui/Router.ts +37 -36
  145. package/src/ui/Scope.ts +1 -1
  146. package/src/ui/TryRegion.ts +2 -2
  147. package/src/ui/applyHead.ts +33 -0
  148. package/src/ui/index.ts +1 -1
  149. package/src/ui/moveNode.ts +1 -1
  150. package/src/ui/reconcile.ts +2 -2
  151. package/src/ui/routing.ts +2 -2
  152. package/src/ui/setAttribute.ts +1 -1
  153. package/src/ui/setText.ts +1 -1
  154. package/src/ui/socketWire.ts +9 -7
  155. package/src/ui/wire.ts +1 -1
  156. package/scaffold/src/server/app.ts +0 -6
  157. package/scaffold/src/shared/.gitkeep +0 -0
  158. package/scaffold/src/ui/app.html +0 -8
  159. package/src/server/componentImports.ts +0 -56
  160. /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
  161. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
@@ -8,6 +8,7 @@ import {
8
8
  type Attribute,
9
9
  type Branch,
10
10
  bodiesOf,
11
+ type ElementNode,
11
12
  type ForBlock,
12
13
  type InlineComponent,
13
14
  type Setup,
@@ -18,6 +19,7 @@ import {
18
19
  nesting,
19
20
  skipQuoted,
20
21
  slotEnd,
22
+ startsToken,
21
23
  type Token,
22
24
  tokenize,
23
25
  } from './tokenize.ts'
@@ -37,6 +39,11 @@ export type AbideFile = {
37
39
  * `scripts` because the two run a different number of times, which is the whole of the clause.
38
40
  */
39
41
  modules: string[]
42
+ /**
43
+ * `<script server>` bodies in source order — 34.19, module scope on a server alone. Apart from
44
+ * `modules` because the browser's module never carries them.
45
+ */
46
+ servers: string[]
40
47
  /**
41
48
  * `<style>` bodies in source order, outside any block body — each scoped to the component (34.4). A
42
49
  * body's own is on its block (34.11).
@@ -47,6 +54,12 @@ export type AbideFile = {
47
54
  * template here so no walk over the body steps around it. In tree order, which is 39.5.
48
55
  */
49
56
  head: TemplateNode[]
57
+ /**
58
+ * 35.15 — the document's `<html>` and `<body>`, where the file is written inside them: each with
59
+ * its attributes and none of its children, which are the head and the template. `undefined`
60
+ * where the template is not.
61
+ */
62
+ document: { html: ElementNode; body: ElementNode } | undefined
50
63
  template: TemplateNode[]
51
64
  /** 32.43 — the inline components the template's own body declares, in source order. */
52
65
  components: InlineComponent[]
@@ -68,11 +81,19 @@ export type AbideFile = {
68
81
  origins: Origins
69
82
  }
70
83
 
84
+ /** Every script body a file holds — module, server and per-instance — for a walk over its imports. */
85
+ export function scriptBodies(file: AbideFile): string[] {
86
+ return [...file.modules, ...file.servers, ...file.scripts]
87
+ }
88
+
71
89
  type Origins = {
72
90
  modules: number[]
91
+ servers: number[]
73
92
  scripts: number[]
74
93
  holders: Map<object, number>
75
94
  keys: Map<object, number>
95
+ /** 38.39 — per `{#for}` block, where its item and its index start (`-1` where it has none). */
96
+ bindings: Map<object, { item: number; index: number }>
76
97
  }
77
98
 
78
99
  /**
@@ -111,13 +132,16 @@ function unwindTo(
111
132
  export function parseAbide(source: string): AbideFile {
112
133
  const scripts: string[] = []
113
134
  const modules: string[] = []
135
+ const servers: string[] = []
114
136
  const styles: string[] = []
115
137
  let generics: string | undefined
116
138
  const origins: Origins = {
117
139
  modules: [],
140
+ servers: [],
118
141
  scripts: [],
119
142
  holders: new Map(),
120
143
  keys: new Map(),
144
+ bindings: new Map(),
121
145
  }
122
146
  const root: TemplateNode[] = []
123
147
  // The open elements, innermost last. `root` is the floor and is never popped.
@@ -152,11 +176,16 @@ export function parseAbide(source: string): AbideFile {
152
176
  // the div — while the browser re-parsing that markup closes at `</DIV>`, so every
153
177
  // position the client walks to after it is a different node and the page hydrates
154
178
  // against the wrong tree.
179
+ const closing = source.slice(at + 2, close).trim()
180
+ // 34.29 — a `<script>` is raw text whose body takes its close tag with it, so one met
181
+ // here is what is left of a block that ended early: a `</script>` in a comment or a
182
+ // string in it, and the source after it would be rendered as the page.
183
+ if (sameTag(closing, 'script'))
184
+ throw new Error(
185
+ 'abide: a </script> that closes no <script> — a block ends at the first `</script>` in it, even one in a comment or a string. Write it `<\\/script>` there. RULEBOOK 34.29.',
186
+ )
155
187
  const innermost = open[open.length - 1]
156
- if (
157
- typeof innermost === 'string' &&
158
- sameTag(innermost, source.slice(at + 2, close).trim())
159
- ) {
188
+ if (typeof innermost === 'string' && sameTag(innermost, closing)) {
160
189
  stack.pop()
161
190
  open.pop()
162
191
  }
@@ -213,7 +242,7 @@ export function parseAbide(source: string): AbideFile {
213
242
  lowerTag === 'style'
214
243
  ? styles
215
244
  : lowerTag === 'script'
216
- ? scriptBlock(tag.attributes, scripts, modules)
245
+ ? scriptBlock(tag.attributes, scripts, modules, servers)
217
246
  : undefined
218
247
  const declared =
219
248
  block === undefined || block === styles
@@ -224,7 +253,7 @@ export function parseAbide(source: string): AbideFile {
224
253
  // setup have no call to be generic over.
225
254
  if (block !== scripts || blocks.length > 0)
226
255
  throw new Error(
227
- 'abide: generics="…" belongs on the component\'s own <script>, and this one is a <script module> or a block body\'s. RULEBOOK 34.13.',
256
+ 'abide: generics="…" belongs on the component\'s own <script>, and this one is a <script module>, a <script server> or a block body\'s. RULEBOOK 34.13.',
228
257
  )
229
258
  if (generics !== undefined)
230
259
  throw new Error(
@@ -233,7 +262,12 @@ export function parseAbide(source: string): AbideFile {
233
262
  generics = declared
234
263
  }
235
264
  if (lowerTag === 'script' || lowerTag === 'style') {
236
- const raw = rawBody(source, lowerTag, tag)
265
+ const raw = rawBody(
266
+ source,
267
+ lowerTag,
268
+ tag,
269
+ block !== undefined && block !== styles,
270
+ )
237
271
  // 14.8 — A `<script>` INSIDE A BLOCK IS THAT BODY'S, set up once each time the body is
238
272
  // built, and not the component's: lifted, it ran once for the page and a row's own
239
273
  // names (`{#for item …}`) were not in scope.
@@ -272,6 +306,7 @@ export function parseAbide(source: string): AbideFile {
272
306
  if (block !== undefined) {
273
307
  if (block === scripts) origins.scripts.push(tag.end)
274
308
  else if (block === modules) origins.modules.push(tag.end)
309
+ else if (block === servers) origins.servers.push(tag.end)
275
310
  block.push(raw.body)
276
311
  at = raw.end
277
312
  continue
@@ -508,11 +543,16 @@ export function parseAbide(source: string): AbideFile {
508
543
  }
509
544
 
510
545
  const components = fileComponents(root, declared)
511
- refuseImported(components, [...modules, ...scripts])
546
+ refuseImported(components, [...modules, ...servers, ...scripts])
512
547
  collapseWhitespace(root)
513
548
  const head: TemplateNode[] = []
514
549
  const template: TemplateNode[] = []
550
+ let document: AbideFile['document']
515
551
  for (const node of root) {
552
+ if (node.kind === 'element' && node.lowerTag === 'html') {
553
+ document = documentOf(node, head, template)
554
+ continue
555
+ }
516
556
  if (node.kind === 'element' && node.lowerTag === 'head') {
517
557
  assertNoNestedHead(node.children, '<head>')
518
558
  for (const child of node.children) head.push(child)
@@ -521,11 +561,23 @@ export function parseAbide(source: string): AbideFile {
521
561
  assertNoNestedHead([node], '')
522
562
  template.push(node)
523
563
  }
564
+ // 35.16 — the document is written once, around everything else.
565
+ if (document !== undefined)
566
+ for (const node of root)
567
+ if (
568
+ !(node.kind === 'element' && node.lowerTag === 'html') &&
569
+ !(node.kind === 'text' && node.text.trim() === '')
570
+ )
571
+ throw new Error(
572
+ `abide: ${node.kind === 'element' ? `<${node.tag}>` : 'markup'} stands beside <html>, and a file written inside <html> has nothing outside it. RULEBOOK 35.16.`,
573
+ )
524
574
  return {
525
575
  scripts,
526
576
  modules,
577
+ servers,
527
578
  styles,
528
579
  head,
580
+ document,
529
581
  template,
530
582
  components,
531
583
  generics,
@@ -578,7 +630,7 @@ function inlineComponent(header: {
578
630
  }
579
631
  if (close !== tokens.length - 1)
580
632
  throw new Error(
581
- `abide: {#component ${header.text}} is not a name and one parenthesised props pattern. Write {#component ${name.text}(props)}. RULEBOOK 32.39.`,
633
+ `abide: {#component ${header.text}} is not a name and one parenthesized props pattern. Write {#component ${name.text}(props)}. RULEBOOK 32.39.`,
582
634
  )
583
635
  const inside = (from: number, to: number): { text: string; at: number } =>
584
636
  trimmed(header.text, from, to)
@@ -695,6 +747,43 @@ function refuseImported(components: InlineComponent[], blocks: string[]): void {
695
747
  )
696
748
  }
697
749
 
750
+ /**
751
+ * 35.15 — the `<html>` a file is written inside: its `<head>`'s children added to `head`, its
752
+ * `<body>`'s to `template`, and the two elements handed back with their attributes alone.
753
+ */
754
+ function documentOf(
755
+ html: ElementNode,
756
+ head: TemplateNode[],
757
+ template: TemplateNode[],
758
+ ): { html: ElementNode; body: ElementNode } {
759
+ let body: ElementNode | undefined
760
+ for (const child of html.children) {
761
+ if (child.kind === 'text' && child.text.trim() === '') continue
762
+ if (child.kind === 'element' && child.lowerTag === 'head') {
763
+ assertNoNestedHead(child.children, '<head>')
764
+ for (const node of child.children) head.push(node)
765
+ continue
766
+ }
767
+ if (child.kind === 'element' && child.lowerTag === 'body' && !body) {
768
+ body = child
769
+ assertNoNestedHead(child.children, '<body>')
770
+ for (const node of child.children) template.push(node)
771
+ continue
772
+ }
773
+ throw new Error(
774
+ `abide: <html> holds a <head> and a <body> and nothing else, and this one holds ${child.kind === 'element' ? `<${child.tag}>` : 'more'}. RULEBOOK 35.16.`,
775
+ )
776
+ }
777
+ if (body === undefined)
778
+ throw new Error(
779
+ 'abide: <html> holds no <body>, so the page has nowhere to go. RULEBOOK 35.16.',
780
+ )
781
+ // The elements themselves, emptied, so where each attribute was written is still known.
782
+ html.children = []
783
+ body.children = []
784
+ return { html, body }
785
+ }
786
+
698
787
  /**
699
788
  * Throws where a `<head>` stands anywhere but the top level of the file.
700
789
  *
@@ -712,6 +801,14 @@ function assertNoNestedHead(nodes: TemplateNode[], within: string): void {
712
801
  throw new Error(
713
802
  `abide: a <head> must be top level in a .abide file, and this one is inside ${within}. RULEBOOK 39.2.`,
714
803
  )
804
+ // 35.16 — the document's own elements are written once, at the top, around the rest.
805
+ if (
806
+ node.kind === 'element' &&
807
+ (node.lowerTag === 'html' || node.lowerTag === 'body')
808
+ )
809
+ throw new Error(
810
+ `abide: <${node.tag}> ${within === '' ? 'stands among the markup' : `is inside ${within}`}, and the document's <html> and <body> are written once, at the top of the root layout.abide. RULEBOOK 35.16.`,
811
+ )
715
812
  const label =
716
813
  node.kind === 'element'
717
814
  ? `<${node.tag}>`
@@ -783,29 +880,49 @@ function sameTag(left: string, right: string): boolean {
783
880
  /**
784
881
  * The list a `<script>`'s body belongs in, or `undefined` where it is markup.
785
882
  *
786
- * 34.1 is the bare tag and 34.2 is the bare `module` attribute; anything else on the tag — `src`, `type`, `defer`, `nonce` —
883
+ * 34.1 is the bare tag, 34.2 the bare `module` attribute and 34.19 the bare `server` one; anything
884
+ * else on the tag — `src`, `type`, `defer`, `nonce` —
787
885
  * is a `<script>` the author wrote for the DOCUMENT, and lifting it into component setup moves it
788
- * out of the markup and throws its attributes away.
886
+ * out of the markup and throws its attributes away — save beside `server`, which 34.28 refuses.
789
887
  *
790
- * 34.12, 34.13 — BUT `lang="ts"` AND `generics="…"` ARE THE COMPONENT'S. Every block is TypeScript,
791
- * so `lang="ts"` says nothing, and an editor's convention read as markup took the author's whole
792
- * setup out of the component without a word.
888
+ * 34.13 — BUT `generics="…"` IS THE COMPONENT'S. 34.12 — `lang` is refused on any `<script>`: every
889
+ * block is TypeScript, so it says nothing, and read as markup it took the author's whole setup out of
890
+ * the component without a word.
793
891
  */
794
892
  function scriptBlock(
795
893
  attributes: Attribute[],
796
894
  scripts: string[],
797
895
  modules: string[],
896
+ servers: string[],
798
897
  ): string[] | undefined {
799
898
  let module = false
899
+ let server = false
900
+ let markup = false
800
901
  for (const attribute of attributes) {
801
902
  // `<script module>` carries the marker and no value. `module="x"` names something else.
802
903
  if (attribute.lowerName === 'module' && attribute.value === '')
803
904
  module = true
804
- else if (attribute.lowerName === 'lang' && attribute.value === 'ts')
805
- continue
806
- else if (attribute.lowerName !== 'generics') return undefined
905
+ else if (attribute.lowerName === 'server') {
906
+ server = true
907
+ if (attribute.value !== '') markup = true
908
+ } else if (attribute.lowerName === 'lang')
909
+ throw new Error(
910
+ 'abide: every <script> in a .abide file is TypeScript, so `lang` says nothing — remove it. RULEBOOK 34.12.',
911
+ )
912
+ else if (attribute.lowerName !== 'generics') markup = true
807
913
  }
808
- return module ? modules : scripts
914
+ // 34.28 — a `server` attribute on a tag that would be markup ships the server's source to the
915
+ // document and the browser's module, so it is refused rather than demoted.
916
+ if (server && markup)
917
+ throw new Error(
918
+ 'abide: a <script> carrying `server` is a server block, written `<script server>` with nothing beside it. RULEBOOK 34.28.',
919
+ )
920
+ if (markup) return undefined
921
+ if (module && server)
922
+ throw new Error(
923
+ 'abide: a <script> is `module` or `server`, not both — a server block is module scope already. RULEBOOK 34.19.',
924
+ )
925
+ return server ? servers : module ? modules : scripts
809
926
  }
810
927
 
811
928
  /**
@@ -826,13 +943,22 @@ function rawBody(
826
943
  source: string,
827
944
  lowerTag: string,
828
945
  tag: { selfClosing: boolean; end: number },
946
+ code: boolean,
829
947
  ): { body: string; end: number } {
830
948
  if (tag.selfClosing) return { body: '', end: tag.end }
831
- // The body ends at the first literal close tag, so a `</script>` inside a string in a script
832
- // block ends it early — the case is real and is not handled yet.
949
+ // The body ends at the first literal close tag, as a browser's does, so a `</script>` inside
950
+ // a string or a comment in a script block ends it early — and the real close tag after it
951
+ // is refused where the template meets it (34.29).
833
952
  const close = closeTagAt(source, lowerTag, tag.end)
834
953
  if (close === null)
835
954
  return { body: source.slice(tag.end), end: source.length }
955
+ // 34.30 — a component block's early end is refused AT the early end, because what is left
956
+ // over can open a `<script>` of its own that takes the real close tag, and then 34.29 meets
957
+ // nothing to refuse while the block's source between them renders as the page.
958
+ if (code && !startsToken(source, tag.end, close.start))
959
+ throw new Error(
960
+ 'abide: a </script> inside a string, a comment or a regex ends the <script> block there, as a browser would. Write it `<\\/script>`. RULEBOOK 34.30.',
961
+ )
836
962
  return { body: source.slice(tag.end, close.start), end: close.end }
837
963
  }
838
964
 
@@ -1005,9 +1131,20 @@ function forBlock(
1005
1131
  }
1006
1132
  origins.holders.set(block, subject.at)
1007
1133
  if (key.text !== '') origins.keys.set(block, key.at)
1134
+ origins.bindings.set(block, {
1135
+ item: header.at + leadingSpace(source),
1136
+ index:
1137
+ comma === undefined
1138
+ ? -1
1139
+ : header.at + comma.end + leadingSpace(source.slice(comma.end)),
1140
+ })
1008
1141
  return block
1009
1142
  }
1010
1143
 
1144
+ function leadingSpace(text: string): number {
1145
+ return text.length - text.trimStart().length
1146
+ }
1147
+
1011
1148
  function readTag(
1012
1149
  source: string,
1013
1150
  open: number,