@abide/abide 0.100.0 → 0.102.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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/bin/abide.ts +227 -163
  3. package/docs/REGISTRY.md +726 -0
  4. package/docs/RULEBOOK.md +2753 -0
  5. package/docs/abide.md +12878 -0
  6. package/package.json +10 -3
  7. package/scaffold/AGENTS.md +31 -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 +262 -67
  13. package/src/compiler/argsSchemaOf.ts +1 -1
  14. package/src/compiler/checkApp.ts +43 -15
  15. package/src/compiler/checkedShadow.ts +39 -5
  16. package/src/compiler/classifyRead.ts +6 -4
  17. package/src/compiler/classifyTemplate.ts +1 -1
  18. package/src/compiler/compileApp.ts +433 -193
  19. package/src/compiler/compiledEntry.ts +43 -39
  20. package/src/compiler/componentGraph.ts +58 -71
  21. package/src/compiler/declaredExports.ts +24 -7
  22. package/src/compiler/emitClient.ts +141 -71
  23. package/src/compiler/emitComponent.ts +37 -8
  24. package/src/compiler/emitSource.ts +106 -13
  25. package/src/compiler/handlersUnder.ts +106 -56
  26. package/src/compiler/index.ts +7 -0
  27. package/src/compiler/mountsOf.ts +138 -0
  28. package/src/compiler/nearest.ts +14 -0
  29. package/src/compiler/parseAbide.ts +157 -20
  30. package/src/compiler/projectShadow.ts +213 -7
  31. package/src/compiler/reactiveTypes.ts +20 -7
  32. package/src/compiler/readSources.ts +15 -2
  33. package/src/compiler/readsIn.ts +61 -7
  34. package/src/compiler/renderMarkup.ts +3 -3
  35. package/src/compiler/scaffold.ts +30 -11
  36. package/src/compiler/serveLanguage.ts +125 -34
  37. package/src/compiler/sourceMapOf.ts +193 -0
  38. package/src/compiler/splitImports.ts +205 -83
  39. package/src/compiler/syncTypes.ts +32 -0
  40. package/src/compiler/tokenize.ts +14 -0
  41. package/src/compiler/writeDefaultProject.ts +1 -1
  42. package/src/compiler/writeTypes.ts +82 -0
  43. package/src/server/CLIENT_BUILD.ts +11 -13
  44. package/src/server/DEFAULT_PORT.ts +1 -1
  45. package/src/server/LAUNCHER_FILES.ts +9 -0
  46. package/src/server/MODULE_QUERIES.ts +18 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +391 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +235 -106
  52. package/src/server/answerRpc.ts +124 -39
  53. package/src/server/answerSocket.ts +141 -48
  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 +179 -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 +37 -37
  65. package/src/server/componentModules.d.ts +3 -1
  66. package/src/server/compressAnswer.ts +48 -14
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +59 -73
  70. package/src/server/devWorker.ts +144 -183
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +37 -35
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +124 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +11 -7
  77. package/src/server/installSourceMaps.ts +88 -0
  78. package/src/server/launcherOptions.ts +34 -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 +5 -5
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/principal.ts +10 -5
  85. package/src/server/realOrAsGiven.ts +10 -0
  86. package/src/server/refuseAddressCollision.ts +21 -14
  87. package/src/server/remote.ts +49 -52
  88. package/src/server/render.ts +65 -94
  89. package/src/server/renderComponent.ts +5 -0
  90. package/src/server/reportHealth.ts +2 -2
  91. package/src/server/requestScope.ts +12 -1
  92. package/src/server/runBuild.ts +24 -0
  93. package/src/server/runCompiled.ts +5 -5
  94. package/src/server/runDesktop.ts +52 -27
  95. package/src/server/serveBuild.ts +43 -52
  96. package/src/server/serveDev.ts +81 -39
  97. package/src/server/shell.ts +7 -22
  98. package/src/server/sourceMappings.ts +139 -0
  99. package/src/server/surfaceOf.ts +18 -3
  100. package/src/server/testBuild.ts +1 -19
  101. package/src/server/withAbideSources.ts +117 -0
  102. package/src/shared/ADDRESSES.ts +7 -18
  103. package/src/shared/APP_META.ts +7 -0
  104. package/src/shared/CopyOptions.ts +1 -1
  105. package/src/shared/DEBUG_META.ts +1 -1
  106. package/src/shared/GLOBAL_GETS.ts +9 -3
  107. package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
  108. package/src/shared/RPC_SERVER.ts +2 -2
  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 +1 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/shared/menuItem.ts +3 -3
  119. package/src/shared/reactive/Cursor.ts +3 -9
  120. package/src/shared/reactive/Keyed.ts +4 -3
  121. package/src/shared/reactive/NotLanded.ts +1 -1
  122. package/src/shared/reactive/Reactive.ts +81 -60
  123. package/src/shared/reactive/argsContract.ts +4 -4
  124. package/src/shared/reactive/canonicalKey.ts +4 -4
  125. package/src/shared/reactive/chunksOfStream.ts +6 -0
  126. package/src/shared/reactive/index.ts +1 -1
  127. package/src/shared/reactive/recordWrites.ts +103 -5
  128. package/src/shared/reactive/structural.ts +1 -1
  129. package/src/shared/reactive/tracking.ts +155 -21
  130. package/src/shared/reactive/watch.ts +10 -6
  131. package/src/shared/refuse.ts +1 -1
  132. package/src/shared/routePattern.ts +82 -46
  133. package/src/shared/rpc.ts +47 -13
  134. package/src/shared/rpcRequest.ts +27 -3
  135. package/src/shared/url.ts +4 -44
  136. package/src/shared/writePath.ts +1 -1
  137. package/src/ui/LAYER_NAMES.ts +1 -1
  138. package/src/ui/List.ts +3 -3
  139. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  140. package/src/ui/Mismatch.ts +1 -1
  141. package/src/ui/RawRun.ts +1 -1
  142. package/src/ui/Router.ts +38 -31
  143. package/src/ui/Scope.ts +1 -1
  144. package/src/ui/TryRegion.ts +2 -2
  145. package/src/ui/applyHead.ts +33 -0
  146. package/src/ui/index.ts +1 -1
  147. package/src/ui/menuBridge.ts +1 -1
  148. package/src/ui/moveNode.ts +1 -1
  149. package/src/ui/reconcile.ts +2 -2
  150. package/src/ui/reportMenu.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 → page.abide} +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.20, 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.34 — 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.34.',
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.20 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.33 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.33 — 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.33.',
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.20.',
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.34).
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.35 — 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.34 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.35.',
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,
@@ -8,6 +8,14 @@ import {
8
8
  PositionTable,
9
9
  } from './PositionTable.ts'
10
10
  import type { AbideFile } from './parseAbide.ts'
11
+ import {
12
+ attributedImports,
13
+ IMPORT_ATTRIBUTES,
14
+ type ImportBinding,
15
+ importStatements,
16
+ newlinesIn,
17
+ SOURCE_SUFFIX,
18
+ } from './splitImports.ts'
11
19
  import type {
12
20
  Body,
13
21
  Branch,
@@ -37,6 +45,8 @@ export type Span = { start: number; end: number }
37
45
  export type Projection = {
38
46
  text: string
39
47
  modules: Span
48
+ /** 34.20 — the `<script server>` bodies, module scope like `modules` and the server's alone. */
49
+ servers: Span
40
50
  scripts: Span
41
51
  /** Every template position, ascending, each with its kind — what the checker's tree is asked. */
42
52
  positions: PositionTable
@@ -83,6 +93,45 @@ export function projectShadow(file: AbideFile): Projection {
83
93
  return projection
84
94
  }
85
95
 
96
+ type ImportStatement = ReturnType<typeof importStatements>[number]
97
+
98
+ /** 34.26 — a binding as two blocks importing the same thing agree on it. */
99
+ function bindingKey(
100
+ statement: ImportStatement,
101
+ binding: ImportBinding,
102
+ ): string {
103
+ return `${statement.specifier}|${statement.attributes}|${binding.imported}|${binding.local}`
104
+ }
105
+
106
+ /**
107
+ * 34.26 — what is left of an import once the bindings `drops` answers for are taken out:
108
+ * `undefined` where none is, `''` where every one is, and the statement over the rest otherwise,
109
+ * spelled from its tokens.
110
+ */
111
+ function keptImport(
112
+ statement: ImportStatement,
113
+ drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
114
+ ): string | undefined {
115
+ const parts: string[] = []
116
+ const named: string[] = []
117
+ for (const binding of statement.bindings) {
118
+ if (drops(statement, binding)) continue
119
+ // The default's own slot and a namespace stand outside the braces; `{ default as x }` in.
120
+ if (
121
+ (binding.imported === 'default' &&
122
+ binding.written === binding.local) ||
123
+ binding.imported === '*'
124
+ )
125
+ parts.push(binding.written)
126
+ else named.push(binding.written)
127
+ }
128
+ const kept = parts.length + named.length
129
+ if (kept === statement.bindings.length) return undefined
130
+ if (kept === 0) return ''
131
+ if (named.length > 0) parts.push(`{ ${named.join(', ')} }`)
132
+ return `import ${statement.typeOnly ? 'type ' : ''}${parts.join(', ')} from '${statement.specifier}'${statement.attributes === '' ? '' : ` ${statement.attributes}`}`
133
+ }
134
+
86
135
  function project(file: AbideFile): Projection {
87
136
  const origins: Origin[] = []
88
137
  // 34.13 — scaffolding ahead of every block, so each one and the template are in their scope.
@@ -102,17 +151,115 @@ function project(file: AbideFile): Projection {
102
151
  }
103
152
 
104
153
  // Blocks of one kind, joined as the file wrote them, each run placed where it stood.
105
- const blocks = (bodies: string[], starts: number[]): Span => {
154
+ const blocks = (
155
+ bodies: string[],
156
+ starts: number[],
157
+ drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
158
+ ): Span => {
106
159
  const start = text.length
107
160
  for (let at = 0; at < bodies.length; at += 1) {
108
161
  if (at > 0) text += '\n'
109
- copied(bodies[at] as string, starts[at])
162
+ script(bodies[at] as string, starts[at], drops)
110
163
  }
111
164
  return { start, end: text.length }
112
165
  }
113
- const modules = blocks(file.modules, file.origins.modules)
166
+ // 34.25 — a block copied as written, save that a `.abide` imported as text names the module
167
+ // holding its text, and loses the attribute bun would otherwise read that module as text by.
168
+ //
169
+ // 34.26 — and a binding a `<script server>` and the file's `<script module>` or `<script>` both
170
+ // import is written once: the server's module holds the other blocks' imports too, and written
171
+ // twice it was a duplicate binding. `drops` says which copy goes. A blank line per line it
172
+ // spanned keeps the block line for line with the file.
173
+ const script = (
174
+ body: string,
175
+ origin: number | undefined,
176
+ drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
177
+ ): void => {
178
+ const edits: { start: number; end: number; written: string }[] = []
179
+ for (const one of attributedImports(body, 'text'))
180
+ edits.push({
181
+ start: one.literalStart,
182
+ end: one.end,
183
+ written: `'${one.specifier}${SOURCE_SUFFIX}'`,
184
+ })
185
+ // 23.19 — a mount is the build's to read, and no module of the checker's or bun's.
186
+ for (const one of attributedImports(body, 'routes'))
187
+ edits.push({
188
+ start: one.start,
189
+ end: one.end,
190
+ written: `;${'\n'.repeat(newlinesIn(body, one.start, one.end))}`,
191
+ })
192
+ for (const one of importStatements(body)) {
193
+ const kept = keptImport(one, drops)
194
+ if (kept === undefined) continue
195
+ edits.push({
196
+ start: one.start,
197
+ end: one.end,
198
+ written: `${kept === '' ? ';' : kept}${'\n'.repeat(newlinesIn(body, one.start, one.end))}`,
199
+ })
200
+ }
201
+ edits.sort((one, two) => one.start - two.start)
202
+ let from = 0
203
+ for (const edit of edits) {
204
+ if (edit.start < from) continue
205
+ copied(
206
+ body.slice(from, edit.start),
207
+ origin === undefined ? undefined : origin + from,
208
+ )
209
+ place(origin === undefined ? undefined : origin + edit.start, 0)
210
+ text += edit.written
211
+ from = edit.end
212
+ }
213
+ copied(
214
+ body.slice(from),
215
+ origin === undefined ? undefined : origin + from,
216
+ )
217
+ }
218
+ // 34.26 — per binding the blocks both sides run import, whether one of them imports a value;
219
+ // and the bindings the server block imports as values.
220
+ const shared = new Map<string, boolean>()
221
+ for (const body of [...file.modules, ...file.scripts])
222
+ for (const one of importStatements(body))
223
+ for (const binding of one.bindings) {
224
+ const key = bindingKey(one, binding)
225
+ shared.set(key, shared.get(key) === true || !binding.typeOnly)
226
+ }
227
+ const serverValues = new Set<string>()
228
+ for (const body of file.servers)
229
+ for (const one of importStatements(body))
230
+ for (const binding of one.bindings)
231
+ if (!binding.typeOnly)
232
+ serverValues.add(bindingKey(one, binding))
233
+ // A TYPE-ONLY IMPORT NEVER STANDS FOR A VALUE ONE: erased, it binds nothing at run time, so where
234
+ // the server block imports the value, it is the other block's type-only copy that goes.
235
+ const typeOnlyOfServerValue = (
236
+ statement: ImportStatement,
237
+ binding: ImportBinding,
238
+ ): boolean =>
239
+ binding.typeOnly && serverValues.has(bindingKey(statement, binding))
240
+ const modules = blocks(
241
+ file.modules,
242
+ file.origins.modules,
243
+ typeOnlyOfServerValue,
244
+ )
245
+ text += '\n;\n'
246
+ const servers = blocks(
247
+ file.servers,
248
+ file.origins.servers,
249
+ (statement, binding) => {
250
+ const value = shared.get(bindingKey(statement, binding))
251
+ if (value === undefined || (value === false && !binding.typeOnly))
252
+ return false
253
+ if (value && !binding.typeOnly) refuseTwoFaces(statement, binding)
254
+ return true
255
+ },
256
+ )
114
257
  text += '\n;\n'
115
- const scripts = blocks(file.scripts, file.origins.scripts)
258
+ const scripts = blocks(
259
+ file.scripts,
260
+ file.origins.scripts,
261
+ typeOnlyOfServerValue,
262
+ )
116
263
  const scriptsText = text.slice(scripts.start, scripts.end)
117
264
  text += '\n'
118
265
  const positions: Position[] = []
@@ -161,7 +308,7 @@ function project(file: AbideFile): Projection {
161
308
  // 32.17 — A COMPONENT TAG IS A CALL, AND IT IS PROJECTED AS ONE: the name it invokes, handed the
162
309
  // record the build hands it (`invocationProps`), so the checker holds each value to the prop the
163
310
  // child declares (33.4), reports a name nobody imported, and answers a hover or a jump on the
164
- // tag with the import's (38.40). Spreads first and explicit keys after, which is the order 33.12
311
+ // tag with the import's (38.39). Spreads first and explicit keys after, which is the order 33.12
165
312
  // resolves them in.
166
313
  const invocation = (node: ElementNode): void => {
167
314
  // 32.36 — CALLED OPTIONALLY, so a value that may be absent is checked as one that may
@@ -386,8 +533,17 @@ function project(file: AbideFile): Projection {
386
533
  ),
387
534
  )
388
535
  text += ')\n'
389
- text += `for (const ${node.item} of ${ROWS_OF}(${subject})) {\n`
390
- if (node.index !== '') text += `const ${node.index} = 0\n`
536
+ // 38.39 — the item and the index copied from where they stand, so a hover on either is
537
+ // the binding's. The index is a `number`: `= 0` alone types it the literal `0`.
538
+ const bound = file.origins.bindings.get(node)
539
+ text += 'for (const '
540
+ copied(node.item, bound?.item)
541
+ text += ` of ${ROWS_OF}(${subject})) {\n`
542
+ if (node.index !== '') {
543
+ text += 'const '
544
+ copied(node.index, bound?.index)
545
+ text += ': number = 0\n'
546
+ }
391
547
  // `inBody` but for the key, which stands after the row's setup and before its nodes.
392
548
  setup(node.setup)
393
549
  declare(node.components)
@@ -402,6 +558,9 @@ function project(file: AbideFile): Projection {
402
558
  }
403
559
  declare(file.components)
404
560
  walk(file.head)
561
+ // 35.17 — the document's attributes are read as any element's are.
562
+ if (file.document !== undefined)
563
+ walk([file.document.html, file.document.body])
405
564
  walk(file.template)
406
565
  if (subjects > 0) text += ROWS_OF_DECLARATION
407
566
  if (inlineProps) text += INLINE_PROPS_IMPORT
@@ -410,11 +569,14 @@ function project(file: AbideFile): Projection {
410
569
  // does not parse; a `<script module>` is module scope, where it would, and holds the import of
411
570
  // every file that reaches it until it settles — the render waiting on it in all but name.
412
571
  refuseTopLevelAwait(file.modules.join('\n'), '<script module>')
572
+ refuseTopLevelAwait(file.servers.join('\n'), '<script server>')
413
573
  refuseTopLevelAwait(scriptsText, '<script>')
414
574
  refuseInstanceExport(scriptsText)
575
+ refuseServerDefault(file.servers.join('\n'))
415
576
  return {
416
577
  text,
417
578
  modules,
579
+ servers,
418
580
  scripts,
419
581
  positions: new PositionTable(positions),
420
582
  slots,
@@ -573,6 +735,50 @@ function refuseInstanceExport(source: string): void {
573
735
  }
574
736
  }
575
737
 
738
+ /**
739
+ * 34.22 — a `<script server>` exporting `default`, refused: the module's default is its component,
740
+ * and a handler is addressed by its name (16.2).
741
+ */
742
+ /**
743
+ * 34.26 — throws where the binding both blocks import is a `.abide`'s default or namespace: the
744
+ * server block's is the function binding its props (34.16) and the other block's is the module it
745
+ * compiles to (32.17), and one name in one module scope cannot hold both.
746
+ */
747
+ function refuseTwoFaces(
748
+ statement: ImportStatement,
749
+ binding: ImportBinding,
750
+ ): void {
751
+ if (
752
+ statement.specifier.endsWith('.abide') &&
753
+ !IMPORT_ATTRIBUTES.text.test(statement.attributes) &&
754
+ (binding.imported === 'default' || binding.imported === '*')
755
+ )
756
+ throw new Error(
757
+ `abide: <script server> imports ${binding.local} from '${statement.specifier}' as another script block of its file does — a server block's .abide is the function binding its props (34.16), the other block's is its module (32.17), and one name cannot be both. Import it under another name in the <script server>. RULEBOOK 34.26.`,
758
+ )
759
+ }
760
+
761
+ function refuseServerDefault(source: string): void {
762
+ if (!source.includes(EXPORT)) return
763
+ let depth = 0
764
+ let previous = ''
765
+ for (const token of tokenize(source)) {
766
+ const text = token.text
767
+ if (text === '{' || text === '(' || text === '[') depth += 1
768
+ else if (text === '}' || text === ')' || text === ']') depth -= 1
769
+ // `export { x as default }` names it too.
770
+ else if (
771
+ text === 'default' &&
772
+ ((depth === 0 && previous === EXPORT) ||
773
+ (depth === 1 && previous === 'as'))
774
+ )
775
+ throw new Error(
776
+ "abide: a <script server> cannot export `default` — the module's default is its component. Export the handler under a name; the name is its address's last segment. RULEBOOK 34.22.",
777
+ )
778
+ previous = text
779
+ }
780
+ }
781
+
576
782
  /**
577
783
  * 34.13 — each type parameter a component declares, as a type its body cannot see inside.
578
784
  *