@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
@@ -0,0 +1,96 @@
1
+ // 38.108 — THE APP'S MAP: every address it answers, the file answering each, and where every hook is
2
+ // filed. What `abide routes` prints, off the build's own reading of the tree — the routes and the
3
+ // handler mounts `compileApp` answers with — so the map and what the server answers are one walk.
4
+ //
5
+ // THE HOOKS ARE READ, NOT RUN, as the handlers are: a call of one of abide's hook names in a server
6
+ // block, a `<script>` or a handler file, at the route 37.1 files it at.
7
+
8
+ import { readFileSync } from 'node:fs' // bun exposes no sync read of its own
9
+ import { appFiles } from '#server/appTree.ts'
10
+ import { scopeOf } from '#shared/routePattern.ts'
11
+ import { compileApp } from './compileApp.ts'
12
+ import { hooksCalled } from './hooksCalled.ts'
13
+ import { parseAbide } from './parseAbide.ts'
14
+
15
+ /** One address the app answers, and the file answering it. */
16
+ type MappedRoute = {
17
+ method: string
18
+ address: string
19
+ file: string
20
+ kind: 'page' | 'handler' | 'private'
21
+ }
22
+
23
+ /** One hook call, the route it is filed at, and the file making it. */
24
+ type MappedHook = { hook: string; route: string; file: string }
25
+
26
+ /** Every address the app at `root` answers, and every hook it files. */
27
+ export async function appMap(
28
+ root: string,
29
+ ): Promise<{ routes: MappedRoute[]; hooks: MappedHook[] }> {
30
+ const app = await compileApp(root)
31
+ const routes: MappedRoute[] = []
32
+ for (const route of app.routes)
33
+ routes.push({
34
+ method: 'GET',
35
+ address: route.pattern,
36
+ file: route.source,
37
+ kind: 'page',
38
+ })
39
+ for (const mount of app.mounts)
40
+ routes.push({
41
+ method: mount.method,
42
+ address: mount.address,
43
+ file: mount.source,
44
+ kind: mount.private ? 'private' : 'handler',
45
+ })
46
+ routes.sort((one, two) =>
47
+ one.address === two.address
48
+ ? one.method < two.method
49
+ ? -1
50
+ : 1
51
+ : one.address < two.address
52
+ ? -1
53
+ : 1,
54
+ )
55
+ const hooks: MappedHook[] = []
56
+ const { abide, handlers, modules } = appFiles(root, () => {})
57
+ for (const file of abide) {
58
+ const parsed = parseAbide(readFileSync(`${root}/${file}`, 'utf8'))
59
+ // Block by block: one block's last token would decide how the next one's first reads.
60
+ for (const script of [...parsed.servers, ...parsed.scripts])
61
+ for (const hook of hooksCalled(script))
62
+ hooks.push({ hook, route: scopeOf(file), file })
63
+ }
64
+ for (const file of handlers)
65
+ for (const hook of hooksCalled(readFileSync(`${root}/${file}`, 'utf8')))
66
+ hooks.push({ hook, route: scopeOf(file), file })
67
+ // 37.1 — A MODULE THAT IS NEITHER files at the root: it runs as it is imported, before the
68
+ // importing file's route is set.
69
+ for (const file of modules)
70
+ for (const hook of hooksCalled(readFileSync(`${root}/${file}`, 'utf8')))
71
+ hooks.push({ hook, route: '/', file })
72
+ return { routes, hooks }
73
+ }
74
+
75
+ /** The map as `abide routes` prints it: one line an address, then one a hook. */
76
+ export function mapText(map: Awaited<ReturnType<typeof appMap>>): string {
77
+ const lines: string[] = []
78
+ const width = (values: string[]): number =>
79
+ values.reduce((most, value) => Math.max(most, value.length), 0)
80
+ const methods = width(map.routes.map((route) => route.method))
81
+ const addresses = width(map.routes.map((route) => route.address))
82
+ for (const route of map.routes)
83
+ lines.push(
84
+ `${route.method.padEnd(methods)} ${route.address.padEnd(addresses)} ${route.file}${route.kind === 'private' ? ' (private)' : ''}`,
85
+ )
86
+ if (map.hooks.length > 0) {
87
+ lines.push('', 'hooks')
88
+ const hooks = width(map.hooks.map((hook) => hook.hook))
89
+ const routes = width(map.hooks.map((hook) => hook.route))
90
+ for (const hook of map.hooks)
91
+ lines.push(
92
+ ` ${hook.hook.padEnd(hooks)} ${hook.route.padEnd(routes)} ${hook.file}`,
93
+ )
94
+ }
95
+ return lines.join('\n')
96
+ }
@@ -134,7 +134,7 @@ async function objectOf(
134
134
  }
135
135
  const schema: Schema = { type: 'object', properties }
136
136
  if (required.length > 0) schema.required = required
137
- // 19.20 — A MEMBER THE TYPE DOES NOT DECLARE IS REFUSED, where no index signature declares the
137
+ // 19.18 — A MEMBER THE TYPE DOES NOT DECLARE IS REFUSED, where no index signature declares the
138
138
  // rest: a caller outside the process sends whatever it likes, and a body writing its args to a
139
139
  // row would write that too. An author who wants the rest declares a schema of their own.
140
140
  schema.additionalProperties = false
@@ -1,13 +1,16 @@
1
1
  import { relative } from 'node:path' // bun exposes no path helpers of its own
2
- import { buildDirectory } from '#server/buildDirectory.ts'
2
+ import { appFiles } from '#server/appTree.ts'
3
+ import { placeOf } from '#shared/routePattern.ts'
3
4
  import {
4
5
  type AppChecker,
5
6
  emitApp,
6
7
  resolverIn,
7
8
  sourcesUnder,
8
9
  withChecker,
10
+ withDependencies,
9
11
  } from './compileApp.ts'
10
12
  import { abideImportsOf } from './componentGraph.ts'
13
+ import { projectShadow } from './projectShadow.ts'
11
14
  import {
12
15
  parseSources,
13
16
  type ReadSource,
@@ -36,10 +39,34 @@ export async function checkApp(
36
39
  const { root, sources } = await sourcesUnder(appRoot)
37
40
  return withChecker(root, kept, async (session) => {
38
41
  const refused: Diagnosis[] = []
39
- const parsed = parseSources(sources, refused)
42
+ const own = parseSources(sources, refused)
43
+ // 23.16-23.18 — what the build's walk would refuse of the tree, at each file it names.
44
+ const { handlers } = appFiles(root, (path, message) => {
45
+ refused.push(refusal(`${root}/${path}`, message))
46
+ })
47
+ const resolve = resolverIn()
48
+ const { sources: everyParsed, handlers: handlerFiles } =
49
+ await withDependencies(root, own, handlers, resolve, refused)
50
+ // 38.33 — A FILE WHOSE PROJECTION IS REFUSED (34.3, 34.17) is a finding at its file, found
51
+ // here because every later step projects it and would throw the refusal for the whole app.
52
+ // AFTER THE IMPORTS ARE FOLLOWED, so a refused file another one imports is found too.
53
+ const parsed: typeof everyParsed = []
54
+ for (const source of everyParsed)
55
+ try {
56
+ projectShadow(source.file)
57
+ parsed.push(source)
58
+ } catch (thrown) {
59
+ refused.push(refusal(source.path, thrown))
60
+ }
40
61
  const { read, warnings } = await readSources(session, parsed, refused)
62
+ // THE APP'S OWN FILES ARE REPORTED ON: a dependency's is compiled with them (34.23), and
63
+ // what is wrong inside a package is the package's to report.
64
+ const reported: ReadSource[] = []
65
+ for (const source of read)
66
+ if (placeOf(relative(root, source.path)) !== 'dependency')
67
+ reported.push(source)
41
68
  const { diagnoses: found, refused: loweringRefused } =
42
- await session.diagnose(read, root)
69
+ await session.diagnose(reported, root)
43
70
  const { ambient, unkeyed } = await session.lints(root, parsed)
44
71
  // A FILE WHOSE LOWERING REFUSED is `found` already, at its line — emitted too, it throws the
45
72
  // same refusal at the root and takes the rest of the app's findings with it.
@@ -54,7 +81,13 @@ export async function checkApp(
54
81
  root,
55
82
  session,
56
83
  emittable,
57
- relative(root, buildDirectory(root)),
84
+ resolve,
85
+ handlerFiles,
86
+ // 38.33 — each at its file, and every one rather than the first.
87
+ (relative, thrown) =>
88
+ refused.push(
89
+ refusal(`${root}/${relative}`, thrown),
90
+ ),
58
91
  )
59
92
  ).warnings,
60
93
  )
@@ -65,10 +98,12 @@ export async function checkApp(
65
98
  })
66
99
  }
67
100
 
101
+ const FILES_CONFIG = /^\s*onConfig\s*\(/m
102
+
68
103
  /**
69
104
  * The findings for one file as an editor holds it, saved or not.
70
105
  *
71
- * 38.12 — ONE FILE AS AN EDITOR HOLDS IT, saved or not: the same findings `checkApp` has for it, for
106
+ * 38.11 — ONE FILE AS AN EDITOR HOLDS IT, saved or not: the same findings `checkApp` has for it, for
72
107
  * `abide lsp` to publish. The rest of the app is read from disk, as it stands — save a file `held`
73
108
  * has, which is one the editor holds too.
74
109
  *
@@ -78,12 +113,11 @@ export async function checkApp(
78
113
  */
79
114
  export async function checkSource(
80
115
  checker: AppChecker,
81
- root: string,
82
116
  path: string,
83
117
  text: string,
84
118
  held: (path: string) => string | undefined = () => undefined,
85
119
  ): Promise<Diagnosis[]> {
86
- const session = checker.session
120
+ const { session, root } = checker
87
121
  const refused: Diagnosis[] = []
88
122
  const [page] = parseSources([{ path, text }], refused)
89
123
  if (page === undefined) return refused
@@ -91,7 +125,7 @@ export async function checkSource(
91
125
  for (const imported of abideImportsOf(
92
126
  path,
93
127
  page.file,
94
- resolverIn(root),
128
+ resolverIn(),
95
129
  ).values())
96
130
  try {
97
131
  imports.push({
@@ -102,6 +136,17 @@ export async function checkSource(
102
136
  } catch {
103
137
  // A component that cannot be read takes anything here.
104
138
  }
139
+ // 27.13 — AND EVERY FILE FILING `onConfig`, quietly, so a page that only reads `config()` is
140
+ // typed by defaults it never imported — as `checkApp`'s whole-app program types it. The scan
141
+ // only names candidates (a statement opening on the call); the checker decides which file.
142
+ const lowered = new Set(imports.map((one) => one.path))
143
+ const { sources } = await checker.sources()
144
+ for (const other of sources) {
145
+ if (other.path === path || lowered.has(other.path)) continue
146
+ const text = held(other.path) ?? other.text
147
+ if (text.includes('onConfig') && FILES_CONFIG.test(text))
148
+ imports.push({ path: other.path, text, quiet: true })
149
+ }
105
150
  // A component that does not parse, or that the checker refuses, is its own file's finding and
106
151
  // takes anything here: a quiet source's refusal is reported by nobody.
107
152
  const { read, warnings } = await readSources(
@@ -1,6 +1,13 @@
1
+ import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
1
2
  import type { CheckerAnswer } from './CheckerAnswer.ts'
2
3
  import type { Finding } from './Finding.ts'
3
- import { CHECKED_VALUES, CHECKED_WRITE, Lowering } from './Lowering.ts'
4
+ import {
5
+ CHECKED_CONFIG,
6
+ CHECKED_CONFIG_OF,
7
+ CHECKED_VALUES,
8
+ CHECKED_WRITE,
9
+ Lowering,
10
+ } from './Lowering.ts'
4
11
  import { holdsReactive } from './PositionTable.ts'
5
12
  import type { AbideFile } from './parseAbide.ts'
6
13
  import { NO_PROPS, type Origin, type Span } from './projectShadow.ts'
@@ -18,7 +25,7 @@ import { abideImports } from './splitImports.ts'
18
25
  //
19
26
  // A `.abide` IMPORT NAMES ITS CHECKED MODULE, so a component a page imports resolves to something
20
27
  // the checker can read rather than to a file it cannot parse. What a checked module exports as its
21
- // default is a component taking what its `props` call declares, so a tag is held to it (38.41).
28
+ // default is a component taking what its `props` call declares, so a tag is held to it (38.40).
22
29
 
23
30
  /** A `.abide` lowered as the program it runs as, with its runs mapped back to the projection. */
24
31
  export type CheckedShadow = {
@@ -35,15 +42,17 @@ export type CheckedShadow = {
35
42
  export const COMPONENT_STAND_IN = '__component'
36
43
 
37
44
  /**
38
- * The whole of what a checked module exports, and so all a module importing one sees of it: a
39
- * component, called with the record its tag passes (`projectShadow`'s `invocation`).
45
+ * The component a checked module exports as its default: called with the record its tag passes
46
+ * (`projectShadow`'s `invocation`), and answering the `Component` a server module's call of it
47
+ * renders (34.16). What its blocks export by name is beside it.
40
48
  */
41
49
  function componentModule(props: string, generics?: string): string {
42
50
  // 34.13 — generic over what the file's `<script>` declares, so a caller's props infer it, the
43
- // signature's own parameters shadowing the opaque stand-ins the body was checked against.
51
+ // signature's own parameters shadowing the opaque stand-ins the body was checked against. 34.16 —
52
+ // a component no prop is required of is called with none, as the runtime's bound face takes it.
44
53
  const parameters = generics === undefined ? '' : `<${generics}>`
45
54
  return `
46
- declare const ${COMPONENT_STAND_IN}: ${parameters}(record: ${props}) => void
55
+ declare const ${COMPONENT_STAND_IN}: ${parameters}(...record: {} extends ${props} ? [record?: ${props}] : [record: ${props}]) => import('${SERVER_SPECIFIER}').Component
47
56
  export default ${COMPONENT_STAND_IN}
48
57
  `
49
58
  }
@@ -61,6 +70,7 @@ export function checkedShadow(
61
70
  // Every span the emitters lower, with whether it holds the `Reactive` rather than reading it.
62
71
  const units: { span: Span; holds: boolean }[] = [
63
72
  { span: projection.modules, holds: false },
73
+ { span: projection.servers, holds: false },
64
74
  { span: projection.scripts, holds: false },
65
75
  ]
66
76
  for (const span of projection.slots.values())
@@ -101,6 +111,7 @@ export function checkedShadow(
101
111
  }\n`
102
112
  text += `declare const ${CHECKED_VALUES}: {\n${values}}\n`
103
113
  text += `declare function ${CHECKED_WRITE}<Cell extends { (): unknown; set(value: never): unknown }, Value extends Parameters<Cell['set']>[0]>(cell: Cell, value: Value): Value extends ReturnType<Cell> ? Value : ReturnType<Cell>\n`
114
+ text += configDeclarations(lowering)
104
115
  return {
105
116
  ...pointAtCheckedModules(text, origins, scriptsEnd),
106
117
  refusals: lowering.refusals,
@@ -123,6 +134,62 @@ function propsOf(lowering: Lowering): string {
123
134
  .replace(/\(\s*\)$/, '')}>`
124
135
  }
125
136
 
137
+ /**
138
+ * 27.13 — WHAT EACH `onConfig` DEFAULTS IS DECLARED INTO `Config`, so `config().RETRIES` is the
139
+ * `number` its defaults return rather than the index's `unknown`. Each call is bound by the lowering
140
+ * (`CHECKED_CONFIG`) and typed by `CHECKED_CONFIG_OF` as what its defaults return; an interface can
141
+ * only extend a name, so each is aliased, and a union is narrowed to the keys every member has.
142
+ * Nothing where the file files none.
143
+ */
144
+ function configDeclarations(lowering: Lowering): string {
145
+ const bound: string[] = []
146
+ for (const read of lowering.answer.reads)
147
+ if (read.shape === 'config')
148
+ bound.push(`${CHECKED_CONFIG}${read.start}`)
149
+ return configInterface(bound)
150
+ }
151
+
152
+ /** `Config` extended by what each of `bound` holds — the binding of one `onConfig` call each. */
153
+ function configInterface(bound: readonly string[]): string {
154
+ if (bound.length === 0) return ''
155
+ let aliases = ''
156
+ let names = ''
157
+ for (const name of bound) {
158
+ aliases += `type ${name}Type = typeof ${name}\n`
159
+ names += names === '' ? `${name}Type` : `, ${name}Type`
160
+ }
161
+ return `declare function ${CHECKED_CONFIG_OF}<Options>(on: (defaults: never, options?: Options) => () => void): <Defaults>(defaults: ((env: import('${SERVER_SPECIFIER}').Env) => Defaults) | null, options?: Options) => [Defaults] extends [object] ? Pick<Defaults, keyof Defaults> : {}
162
+ ${aliases}declare module '${SERVER_SPECIFIER}' {
163
+ interface Config extends ${names} {}
164
+ }
165
+ `
166
+ }
167
+
168
+ // A call standing as a statement at a module's top level, as a formatted module writes one.
169
+ const TOP_LEVEL_CONFIG = /^onConfig\s*\(/gm
170
+
171
+ /**
172
+ * 27.13 FOR A MODULE THAT IS NO `.abide` — a handler file, or one under a `server` directory: its
173
+ * text with each `onConfig(…)` at its top level bound as the lowering binds a server block's, and
174
+ * `Config` extended by each, or `undefined` where it files none.
175
+ *
176
+ * A MODULE OF ITS OWN BESIDE THE FILE, which the checker reads and nothing reports on: the file's own
177
+ * text is the one its diagnostics are placed in, and binding a call shifts every column after it.
178
+ */
179
+ export function moduleConfigShadow(text: string): string | undefined {
180
+ const bound: string[] = []
181
+ let out = ''
182
+ let from = 0
183
+ for (const found of text.matchAll(TOP_LEVEL_CONFIG)) {
184
+ const name = `${CHECKED_CONFIG}${bound.length}`
185
+ bound.push(name)
186
+ out += `${text.slice(from, found.index)}const ${name} = ${CHECKED_CONFIG_OF}(onConfig)(`
187
+ from = found.index + found[0].length
188
+ }
189
+ if (bound.length === 0) return undefined
190
+ return `${out}${text.slice(from)}\n${configInterface(bound)}`
191
+ }
192
+
126
193
  /**
127
194
  * Points each `.abide` import in the script blocks at its checked module, and moves `origins` to
128
195
  * match.
@@ -27,10 +27,11 @@ import { REACTIVE_MEMBERS } from './REACTIVE_MEMBERS.ts'
27
27
  * statement ends and whether `{ n }` is a literal are TypeScript's answers rather than a token
28
28
  * walk's guess at them. Every offset is into the file's projection (`projectShadow`).
29
29
  *
30
- * ONE SHAPE IS NOT A REACTIVE NAME: `raw` is the callee of a template expression's bare `raw(…)`
31
- * (32.25), which lowers to the framework's own `raw`; a `<script>`'s is left as written. It rides
32
- * this list because it is a rewrite at an occurrence, which is what the splice walks; what the
33
- * checker proved about a whole position is `CheckerAnswer`'s.
30
+ * TWO SHAPES ARE NOT A REACTIVE NAME: `raw` is the callee of a template expression's bare `raw(…)`
31
+ * (32.25), which lowers to the framework's own `raw`; a `<script>`'s is left as written. `config`
32
+ * is the callee of an `onConfig(…)` statement in a `<script server>` (27.13), which only a checked
33
+ * lowering rewrites. They ride this list because each is a rewrite at an occurrence, which is what
34
+ * the splice walks; what the checker proved about a whole position is `CheckerAnswer`'s.
34
35
  *
35
36
  * THE SHAPE IS FIXED: every field on every read, so `Lowering`'s splice sees one hidden class.
36
37
  */
@@ -69,6 +70,7 @@ export type Read = {
69
70
  | 'restMember'
70
71
  | 'restWhole'
71
72
  | 'raw'
73
+ | 'config'
72
74
  /** What the rewrite replaces: the name, or a whole write or update from its first token. */
73
75
  from: number
74
76
  to: number
@@ -132,7 +132,7 @@ export function classifyAttributes(
132
132
  // 32.10, 32.19 — THE PLAIN `style={expr}` OR `class={expr}` BESIDE A DIRECTIVE IS LEFT
133
133
  // UNBOUND. `setAttribute` replaces the whole merged value and takes the directives'
134
134
  // halves with it, and the two bindings then overwrite each other in whichever order
135
- // they re-ran.
135
+ // they reran.
136
136
  //
137
137
  // ALLOWED, rather than refused. The page still RENDERS right — the server writes both
138
138
  // halves into the one merged attribute — so the only thing a build error would buy is