@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,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,
@@ -16,7 +24,7 @@ import type {
16
24
  Setup,
17
25
  TemplateNode,
18
26
  } from './TemplateNode.ts'
19
- import { tokenize } from './tokenize.ts'
27
+ import { nesting, tokenize } from './tokenize.ts'
20
28
 
21
29
  export type Span = { start: number; end: number }
22
30
 
@@ -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.19 — 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.22 — 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.22 — 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.21 — 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.22 — 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.22 — 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
+ )
114
245
  text += '\n;\n'
115
- const scripts = blocks(file.scripts, file.origins.scripts)
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
+ )
257
+ text += '\n;\n'
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
@@ -409,12 +568,23 @@ function project(file: AbideFile): Projection {
409
568
  // 34.3 — BOTH BLOCKS. A `<script>` is a function body on both sides, where an `await` at its top
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
- refuseTopLevelAwait(file.modules.join('\n'), '<script module>')
571
+ const modulesText = file.modules.join('\n')
572
+ const serversText = file.servers.join('\n')
573
+ refuseTopLevelAwait(modulesText, '<script module>')
574
+ refuseTopLevelAwait(serversText, '<script server>')
413
575
  refuseTopLevelAwait(scriptsText, '<script>')
414
- refuseInstanceExport(scriptsText)
576
+ // A kind at a time: a token at the end of one kind's text would decide how the next one's first
577
+ // reads.
578
+ refuseExport(modulesText)
579
+ refuseExport(serversText)
580
+ refuseExport(scriptsText)
581
+ // 16.70 — what the server block binds, re-exported under a name no author writes, so the
582
+ // checker reads each one's declaring call as it reads a handler file's default.
583
+ text += privateExports(serversText)
415
584
  return {
416
585
  text,
417
586
  modules,
587
+ servers,
418
588
  scripts,
419
589
  positions: new PositionTable(positions),
420
590
  slots,
@@ -537,6 +707,7 @@ declare function ${ROWS_OF}<T>(subject: Iterable<T> | AsyncIterable<T> | null |
537
707
  */
538
708
  function refuseTopLevelAwait(source: string, block: string): void {
539
709
  if (!source.includes(AWAIT)) return
710
+ // Brackets alone, not `nesting`: a template's substitution is still the top level here.
540
711
  let depth = 0
541
712
  for (const token of tokenize(source)) {
542
713
  const text = token.text
@@ -550,29 +721,89 @@ function refuseTopLevelAwait(source: string, block: string): void {
550
721
  }
551
722
 
552
723
  /**
553
- * Throws where a `<script>` exports.
724
+ * Throws where a script block exports.
554
725
  *
555
- * 34.18 — AN EXPORT IS THE MODULE'S, and a `<script>` runs once per instance: there is no one
556
- * instance an importer could be reading from. Refused rather than emitted, as 34.3's `await` is —
557
- * the block is a function body on both sides, and the bundler's own error names neither the page
558
- * nor the fix.
726
+ * 34.17 — A `.abide`'S ONE EXPORT IS ITS COMPONENT. A handler another file calls is a handler file
727
+ * (16.2), a value two files share is a module under a lib directory, and what the framework reads
728
+ * off a file is a hook it calls (37.1). Refused rather than emitted, as 34.3's `await` is: the
729
+ * bundler's own error names neither the file nor the fix.
559
730
  */
560
- function refuseInstanceExport(source: string): void {
731
+ function refuseExport(source: string): void {
561
732
  if (!source.includes(EXPORT)) return
562
733
  let depth = 0
563
734
  let previous = ''
564
735
  for (const token of tokenize(source)) {
565
736
  const text = token.text
566
- if (text === '{' || text === '(' || text === '[') depth += 1
567
- else if (text === '}' || text === ')' || text === ']') depth -= 1
737
+ const moved = nesting(token.kind)
738
+ if (moved !== 0) depth += moved
568
739
  else if (depth === 0 && text === EXPORT && previous !== '.')
569
740
  throw new Error(
570
- "abide: a <script> cannot export — it runs once per instance, and an export is the module's. Move the declaration to <script module>. RULEBOOK 34.18.",
741
+ 'abide: a .abide cannot export — its component is its one export. A handler another file calls is a handler file (a .ts beside the page, `export default GET(…)`); a value files share is a module under a lib directory. RULEBOOK 34.17.',
571
742
  )
572
743
  previous = text
573
744
  }
574
745
  }
575
746
 
747
+ /** The prefix a server block's bindings are re-exported under, for the build alone (16.70). */
748
+ export const PRIVATE_EXPORT = '__handler_'
749
+
750
+ /**
751
+ * 16.70 — the `export { … }` re-exporting each name the server blocks bind under `PRIVATE_EXPORT`,
752
+ * or `''` where they bind none. One spelling for the checker's view and the emitted module, which
753
+ * the build's handler table reads a private handler out of by the same name.
754
+ */
755
+ export function privateExports(servers: string): string {
756
+ const listed: string[] = []
757
+ for (const name of serverBindings(servers))
758
+ listed.push(`${name} as ${PRIVATE_EXPORT}${name}`)
759
+ return listed.length === 0 ? '' : `export { ${listed.join(', ')} }\n`
760
+ }
761
+
762
+ /**
763
+ * The names a `<script server>` binds with `const`, `let` or `var` at its top level, a
764
+ * destructuring's aside: a handler is a call bound to one name.
765
+ */
766
+ function serverBindings(source: string): string[] {
767
+ const names: string[] = []
768
+ if (source === '') return names
769
+ let depth = 0
770
+ let declaring = false
771
+ for (const token of tokenize(source)) {
772
+ const text = token.text
773
+ const moved = nesting(token.kind)
774
+ if (moved !== 0) {
775
+ depth += moved
776
+ declaring = false
777
+ } else if (depth !== 0) continue
778
+ else if (text === 'const' || text === 'let' || text === 'var')
779
+ declaring = true
780
+ else if (declaring && token.kind === SyntaxKind.Identifier) {
781
+ names.push(text)
782
+ declaring = false
783
+ } else declaring = false
784
+ }
785
+ return names
786
+ }
787
+
788
+ /**
789
+ * 34.22 — throws where the binding both blocks import is a `.abide`'s default or namespace: the
790
+ * server block's is the function binding its props (34.16) and the other block's is the module it
791
+ * compiles to (32.17), and one name in one module scope cannot hold both.
792
+ */
793
+ function refuseTwoFaces(
794
+ statement: ImportStatement,
795
+ binding: ImportBinding,
796
+ ): void {
797
+ if (
798
+ statement.specifier.endsWith('.abide') &&
799
+ !IMPORT_ATTRIBUTES.text.test(statement.attributes) &&
800
+ (binding.imported === 'default' || binding.imported === '*')
801
+ )
802
+ throw new Error(
803
+ `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.22.`,
804
+ )
805
+ }
806
+
576
807
  /**
577
808
  * 34.13 — each type parameter a component declares, as a type its body cannot see inside.
578
809
  *
@@ -76,8 +76,8 @@ async function everyPresentMember(
76
76
  return present
77
77
  }
78
78
 
79
- // Where abide declares `Reactive`, `props`, `route` and `Selection`, found in the project the way
80
- // `METHODS_SOURCE` is.
79
+ // Where abide declares `Reactive`, `props`, `route`, `Selection` and `onConfig`, found in the project
80
+ // the way `METHODS_SOURCE` is.
81
81
  const REACTIVE_SOURCE = Bun.fileURLToPath(
82
82
  new URL('../shared/reactive/Reactive.ts', import.meta.url),
83
83
  )
@@ -90,16 +90,20 @@ const ROUTE_SOURCE = Bun.fileURLToPath(
90
90
  const SELECTION_SOURCE = Bun.fileURLToPath(
91
91
  new URL('../shared/reactive/index.ts', import.meta.url),
92
92
  )
93
+ const HOOKS_SOURCE = Bun.fileURLToPath(
94
+ new URL('../server/hooks.ts', import.meta.url),
95
+ )
93
96
 
94
97
  /**
95
- * Abide's own `Reactive`, `Selection`, `props` and `route` declarations, as this project's checker
96
- * names them.
98
+ * Abide's own `Reactive`, `Selection`, `props`, `route` and `onConfig` declarations, as this
99
+ * project's checker names them.
97
100
  *
98
101
  * 31.3 — A `Reactive` IS TOLD BY ITS DECLARATION, never by how it prints. Every instantiation of
99
102
  * abide's `Reactive` carries the symbol of the one type literal it is declared as, whatever alias
100
103
  * or re-export the app reached it through — so `type Counter = Reactive<number>` is one, and an
101
104
  * app's own type that happens to be spelled `Reactive` is not. `undefined` where the project never
102
- * reaches abide, and then nothing in it is one. `props` is abide's file as the checker names it.
105
+ * reaches abide, and then nothing in it is one. `props` and `onConfig` are abide's files as the
106
+ * checker names them.
103
107
  */
104
108
  export type Abide = {
105
109
  /** 23.9 — abide's `route`, as the file the checker names it. */
@@ -110,14 +114,17 @@ export type Abide = {
110
114
  */
111
115
  selection: CheckedSymbol | undefined
112
116
  props: string | undefined
117
+ /** 27.13 — abide's `onConfig`, as the file the checker names it. */
118
+ onConfig: string | undefined
113
119
  }
114
120
 
115
121
  export async function abideIn(project: Project): Promise<Abide> {
116
- const [declares, props, selects, route] = await Promise.all([
122
+ const [declares, props, selects, route, hooks] = await Promise.all([
117
123
  project.program.getSourceFile(REACTIVE_SOURCE),
118
124
  project.program.getSourceFile(PROPS_SOURCE),
119
125
  project.program.getSourceFile(SELECTION_SOURCE),
120
126
  project.program.getSourceFile(ROUTE_SOURCE),
127
+ project.program.getSourceFile(HOOKS_SOURCE),
121
128
  ])
122
129
  let selection: CheckedSymbol | undefined
123
130
  for (const statement of selects?.statements ?? [])
@@ -142,7 +149,13 @@ export async function abideIn(project: Project): Promise<Abide> {
142
149
  reactive = await literal?.getSymbol()
143
150
  break
144
151
  }
145
- return { route: route?.path, reactive, selection, props: props?.path }
152
+ return {
153
+ route: route?.path,
154
+ reactive,
155
+ selection,
156
+ props: props?.path,
157
+ onConfig: hooks?.path,
158
+ }
146
159
  }
147
160
 
148
161
  /**
@@ -13,6 +13,8 @@ export type ParsedSource = {
13
13
  text: string
14
14
  file: AbideFile
15
15
  quiet: boolean
16
+ /** 23.19 — where a mounted package's file stands in the app; `undefined` for one at its own path. */
17
+ position: string | undefined
16
18
  }
17
19
 
18
20
  /** One `.abide` as the emitters and the checked lowering take it: parsed, and answered. */
@@ -25,7 +27,12 @@ export type ReadSource = ParsedSource & { answer: CheckerAnswer }
25
27
  * parsed around it — a quiet one's is nobody's. Without it the refusal is thrown, as a build stops.
26
28
  */
27
29
  export function parseSources(
28
- sources: readonly { path: string; text: string; quiet?: boolean }[],
30
+ sources: readonly {
31
+ path: string
32
+ text: string
33
+ quiet?: boolean
34
+ position?: string
35
+ }[],
29
36
  refused?: Diagnosis[],
30
37
  ): ParsedSource[] {
31
38
  const parsed: ParsedSource[] = []
@@ -39,7 +46,13 @@ export function parseSources(
39
46
  if (!quiet) refused.push(refusal(source.path, thrown))
40
47
  continue
41
48
  }
42
- parsed.push({ path: source.path, text: source.text, file, quiet })
49
+ parsed.push({
50
+ path: source.path,
51
+ text: source.text,
52
+ file,
53
+ quiet,
54
+ position: source.position,
55
+ })
43
56
  }
44
57
  return parsed
45
58
  }
@@ -8,6 +8,7 @@ import {
8
8
  isCallExpression,
9
9
  isClassDeclaration,
10
10
  isEnumDeclaration,
11
+ isExpressionStatement,
11
12
  isFunctionDeclaration,
12
13
  isFunctionExpression,
13
14
  isIdentifier,
@@ -66,8 +67,9 @@ import { trackingWarnings } from './trackingWarnings.ts'
66
67
  export type ShadowSpans = {
67
68
  /** Where the shadow's scripts end and its template begins. */
68
69
  scriptsEnd: number
69
- /** The `<script module>` bodies, and the `<script>` bodies, in the shadow. */
70
+ /** The `<script module>`, `<script server>` and `<script>` bodies, in the shadow. */
70
71
  modules: Span
72
+ servers: Span
71
73
  scripts: Span
72
74
  positions: PositionTable
73
75
  }
@@ -123,7 +125,8 @@ export async function readsIn(
123
125
  sourceFile: SourceFile,
124
126
  shadowPath: string,
125
127
  spans: ShadowSpans,
126
- abidePath: string,
128
+ // The file below the app's root, which its route is read off (23.14).
129
+ routedPath: string,
127
130
  file: AbideFile,
128
131
  ): Promise<{ answer: CheckerAnswer; warnings: Finding[] }> {
129
132
  // A destructuring pattern binds names this does not see, and that is 31.12's case.
@@ -185,7 +188,14 @@ export async function readsIn(
185
188
  // Awaited after the round trips below, so a 33.14 refusal would reject with nothing
186
189
  // listening and be reported unhandled; `await props` still throws it.
187
190
  props.catch(() => {})
188
- const params = routeReads(checker, abide, sourceFile, abidePath, reads)
191
+ const params = routeReads(checker, abide, sourceFile, routedPath, reads)
192
+ const configs = configReads(
193
+ checker,
194
+ abide,
195
+ sourceFile,
196
+ spans.servers,
197
+ reads,
198
+ )
189
199
  // No name is no round trip to ask about none.
190
200
  const symbols =
191
201
  names.length === 0 ? [] : await checker.getSymbolAtLocation(names)
@@ -249,6 +259,7 @@ export async function readsIn(
249
259
  await Promise.all(holding)
250
260
  await props
251
261
  await params
262
+ await configs
252
263
  const statics = new Set<number>()
253
264
  const expressions = spans.positions.nodesIn(
254
265
  sourceFile,
@@ -271,7 +282,7 @@ export async function readsIn(
271
282
  await proveSlots(checker, expressions, reads, statics)
272
283
  reads.sort(byPosition)
273
284
  const warnings = await warningsFor(checker, located, spans.positions)
274
- // 34.19.
285
+ // 34.18.
275
286
  for (const warning of await sharedModuleWrites(
276
287
  checker,
277
288
  shadowPath,
@@ -683,7 +694,7 @@ const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'navigating'])
683
694
  * a name, and this read is spelled as a member.
684
695
  *
685
696
  * 23.9 — AND `route.params` IN A PAGE OR A LAYOUT IS WHAT THAT FILE'S OWN ROUTE PROMISES. The file
686
- * is the route: `ui/pages/media/[id]/page.abide` only ever renders where `id` matched, so its
697
+ * is the route: `media/[id].abide` only ever renders where `id` matched, so its
687
698
  * `route.params` has an `id`, and a layout's has its own segments and whatever a page below adds.
688
699
  * Only the checker reads that — the running module reads the one `route` every page shares.
689
700
  */
@@ -691,11 +702,11 @@ async function routeReads(
691
702
  checker: Checker,
692
703
  abide: Abide,
693
704
  sourceFile: Node,
694
- abidePath: string,
705
+ routedPath: string,
695
706
  reads: Read[],
696
707
  ): Promise<void> {
697
708
  if (abide.route === undefined) return
698
- const routed = fileKind(abidePath)
709
+ const routed = fileKind(routedPath)
699
710
  const kind = routed === 'page' || routed === 'layout' ? routed : undefined
700
711
  const accesses: Node[] = []
701
712
  const owners: Node[] = []
@@ -714,7 +725,7 @@ async function routeReads(
714
725
  sourceFile.forEachChild(visit)
715
726
  if (owners.length === 0) return
716
727
  const symbols = await checker.getSymbolAtLocation(owners)
717
- const pattern = routeOf(abidePath)
728
+ const pattern = routeOf(routedPath)
718
729
  for (let at = 0; at < accesses.length; at += 1) {
719
730
  const symbol = symbols[at]
720
731
  if (symbol === undefined) continue
@@ -777,6 +788,49 @@ function refuseDeclared(
777
788
  )
778
789
  }
779
790
 
791
+ /**
792
+ * 27.13 — each `onConfig(…)` standing as a statement at the top of a `<script server>`, as a read of
793
+ * its callee, pushed onto `reads`.
794
+ *
795
+ * The callee says which are abide's, as `props`'s does. A call anywhere else — inside a function,
796
+ * or handing its unfiling back to a name — is filed all the same, and declares nothing.
797
+ */
798
+ async function configReads(
799
+ checker: Checker,
800
+ abide: Abide,
801
+ sourceFile: Node & { statements: readonly Node[] },
802
+ servers: Span,
803
+ reads: Read[],
804
+ ): Promise<void> {
805
+ if (abide.onConfig === undefined) return
806
+ const callees: Node[] = []
807
+ for (const statement of sourceFile.statements) {
808
+ const start = statement.getStart()
809
+ if (start < servers.start) continue
810
+ if (start >= servers.end) break
811
+ if (
812
+ isExpressionStatement(statement) &&
813
+ isCallExpression(statement.expression) &&
814
+ isIdentifier(statement.expression.expression)
815
+ )
816
+ callees.push(statement.expression.expression)
817
+ }
818
+ if (callees.length === 0) return
819
+ const named = await checker.getSymbolAtLocation(callees)
820
+ for (let at = 0; at < callees.length; at += 1) {
821
+ const symbol = named[at]
822
+ if (symbol === undefined) continue
823
+ const callee = await aliasedSymbol(checker, symbol)
824
+ if (
825
+ callee.name !== 'onConfig' ||
826
+ callee.valueDeclaration?.path !== abide.onConfig
827
+ )
828
+ continue
829
+ const node = callees[at] as Node
830
+ reads.push(newRead(node.getStart(), node.end, 'config'))
831
+ }
832
+ }
833
+
780
834
  /**
781
835
  * The reads a `props<Props>()` destructuring contributes, pushed onto `reads`.
782
836
  *
@@ -108,7 +108,7 @@ export const HEADS = '__heads'
108
108
  * EVERY MARKUP POSITION THAT EVALUATES AN AUTHOR EXPRESSION, rather than the two that were found
109
109
  * first. A GUARD HERE IS HALF A MECHANISM — it trades a throw for a HOLE, and something on the
110
110
  * client has to fill that hole once the value lands: a text slot has `runHere` and `textHere`, a
111
- * `{#for}` has `List.hydrate`'s count check, an attribute has its own binding re-running on the
111
+ * `{#for}` has `List.hydrate`'s count check, an attribute has its own binding rerunning on the
112
112
  * value that landed (32.3), and the `{#if}` chain has `isEmptyRegion` — which is why that one is
113
113
  * guarded at the REGION rather than per subject, and says so at its own arm. The ESCAPER STAYS
114
114
  * OUTSIDE: absence is `undefined` here and what it means is already decided where it lands (see
@@ -296,7 +296,7 @@ function renderPosition(
296
296
  `abide: a ${at.kind} position holds a ${node.kind} node.`,
297
297
  )
298
298
 
299
- // 32.17 — A CAPITALISED TAG IS AN INVOCATION, and the capital is the whole of the
299
+ // 32.17 — A CAPITALIZED TAG IS AN INVOCATION, and the capital is the whole of the
300
300
  // distinction. It is emitted as a CALL, so the child's own module renders it.
301
301
  if (isComponentTag(node.tag)) return invocationMarkup(node, at, lowering)
302
302
 
@@ -536,7 +536,7 @@ function serverComponent(
536
536
  // standing side by side are two instances, each creating what no ancestor holds (10.6).
537
537
  const { opened, closed } = shareScope(lowering, '')
538
538
  return `function ${component.name}(${PROPS} = {}) {
539
- ${opened}${head}${setup}${definitions(sink, lowering)}return { head: '', body: ${renderMarkup(sink, lowering, SERVER_LANE)} }${closed}
539
+ ${opened}${head}${setup}${definitions(sink, lowering)}return { head: '', body: ${renderMarkup(sink, lowering, SERVER_LANE)}, document: undefined }${closed}
540
540
  }`
541
541
  }
542
542