@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
@@ -1,28 +1,53 @@
1
1
  import { existsSync, readFileSync, realpathSync } from 'node:fs' // bun exposes no sync existence check, read or realpath of its own
2
- import { dirname, posix, resolve } from 'node:path' // bun exposes no path helpers of its own
3
-
2
+ import { dirname, posix, relative as relativeTo } from 'node:path' // bun exposes no path helpers of its own
3
+ import {
4
+ type Compilation,
5
+ type CompiledFile,
6
+ resolvedFile,
7
+ } from '#server/abideModules.ts'
4
8
  import type { Mount } from '#server/answerRpc.ts'
5
- import { buildDirectory } from '#server/buildDirectory.ts'
6
- import { CLIENT_ENTRY, WIRE_DIRECTORY } from '#server/CLIENT_BUILD.ts'
7
- import { keyedHead, shellUnder } from '#server/render.ts'
9
+ import { abideUnder, appFiles } from '#server/appTree.ts'
10
+ import { CLIENT_ENTRY } from '#server/CLIENT_BUILD.ts'
11
+ import { COMPONENT_QUERY } from '#server/MODULE_QUERIES.ts'
12
+ import { ROUTES_TABLE, SERVER_TABLE } from '#server/TABLES.ts'
13
+ import { VIRTUAL_DIRECTORY } from '#server/VIRTUAL_DIRECTORY.ts'
8
14
  import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
9
- import { appGraph } from './componentGraph.ts'
15
+ import {
16
+ coversRoute,
17
+ parseRoutePattern,
18
+ placeOf,
19
+ routeOf,
20
+ type Segment,
21
+ } from '#shared/routePattern.ts'
22
+ import { abideImportsOf, appGraph } from './componentGraph.ts'
10
23
  import { emitSource } from './emitSource.ts'
11
24
  import { formatDiagnosis } from './formatDiagnosis.ts'
12
- import { declaresHandlers, handlersUnder } from './handlersUnder.ts'
13
- import { parseSources, type ReadSource, readSources } from './readSources.ts'
14
- import { ROUTING_SPECIFIER, WIRE_SPECIFIER } from './SPECIFIERS.ts'
25
+ import { handlersUnder } from './handlersUnder.ts'
26
+ import { mountsOf } from './mountsOf.ts'
27
+ import { nearest } from './nearest.ts'
28
+ import type { AbideFile } from './parseAbide.ts'
29
+ import {
30
+ type ParsedSource,
31
+ parseSources,
32
+ type ReadSource,
33
+ readSources,
34
+ refusal,
35
+ } from './readSources.ts'
36
+ import {
37
+ ROUTING_SPECIFIER,
38
+ SHARED_SPECIFIER,
39
+ WIRE_SPECIFIER,
40
+ } from './SPECIFIERS.ts'
41
+ import { respecified } from './splitImports.ts'
15
42
  import { type Diagnosis, TypeSession } from './TypeSession.ts'
16
43
  import { writeDefaultProject } from './writeDefaultProject.ts'
44
+ import { writeTypes } from './writeTypes.ts'
17
45
 
18
- // AN APP'S `.abide` SOURCES, COMPILED — every module the output tree holds, keyed by where it goes
19
- // in it. Nothing here touches the output directory: `abide build` writes what this hands back and
20
- // `abide dev` keeps it in memory (38.17), and the two commands serving different markup from the
21
- // same sources is exactly what one compile in two sinks makes impossible.
22
- //
23
- // THE KEYS ARE THE OUTPUT LAYOUT, which mirrors the source tree so a module's neighbours are where
24
- // its imports say they are — `appGraph` lands a `.abide` import on its module whatever the
25
- // spelling, and every other relative import is pointed back at the source it named (34.14).
46
+ // AN APP'S `.abide` SOURCES, COMPILED — every file keyed by its path below the root, and loaded
47
+ // there through its `?component` face (`abideModules`). Nothing here touches the output directory:
48
+ // `abide build` writes what this hands back and `abide dev` keeps it in memory (38.16), and the two
49
+ // commands serving different markup from the same sources is exactly what one compile in two sinks
50
+ // makes impossible.
26
51
 
27
52
  type AppRoute = {
28
53
  /** The address, in the bracket spelling `routePattern` parses. */
@@ -38,21 +63,17 @@ type AppRoute = {
38
63
  layouts: string[]
39
64
  }
40
65
 
41
- /** An app's compiled output: every module by output path, its routes, and its warnings. */
42
- export type CompiledApp = {
43
- /**
44
- * Module text by path below the output directory.
45
- *
46
- * `ui/pages/page.ts`, its `.client.ts`, and what the build writes rather than an author
47
- * (`routes.ts`, `server.ts`, `entry.ts`, `rpc.ts` and a wrapper per handler module under
48
- * `WIRE_DIRECTORY`).
49
- */
50
- modules: Map<string, string>
66
+ /** An app's compiled output: every file and table (`Compilation`), its routes, and its warnings. */
67
+ export type CompiledApp = Compilation & {
51
68
  routes: AppRoute[]
69
+ /** Every handler, where it is mounted. 16.2. */
70
+ mounts: Mount[]
52
71
  /** How many `.abide` files were read, which is what the commands report. */
53
72
  sources: number
54
73
  /** What the compiler has to say that does not stop the build, on the `.abide` line. */
55
74
  warnings: Diagnosis[]
75
+ /** Every public file's source path to its address, as `appFiles` read them. 36.11. */
76
+ published: Map<string, string>
56
77
  }
57
78
 
58
79
  /**
@@ -64,12 +85,19 @@ export type CompiledApp = {
64
85
  */
65
86
  export type AppChecker = {
66
87
  session: TypeSession
88
+ /** The app's root, real — every path a checker is asked about is spelled under it. */
89
+ root: string
67
90
  /**
68
91
  * Records a path a watcher saw move, or `undefined` for a move it could not place.
69
92
  *
70
93
  * Held until `current`.
71
94
  */
72
95
  saw: (path: string | undefined) => void
96
+ /**
97
+ * The app's own `.abide`, read (`sourcesUnder`) — kept until `saw` hears anything move, so a
98
+ * check asked again and again while an editor types re-reads nothing on disk.
99
+ */
100
+ sources: () => ReturnType<typeof sourcesUnder>
73
101
  /**
74
102
  * Whether the project it was opened on is still the app's; if so, flushes what `saw` held.
75
103
  *
@@ -82,9 +110,15 @@ export type AppChecker = {
82
110
  close: () => Promise<void>
83
111
  }
84
112
 
85
- /** Opens the checker for the app at `root`, on a default tsconfig where it has none (38.31). */
86
- export async function openChecker(root: string): Promise<AppChecker> {
87
- // Where the app has no tsconfig it is typed against a default one (38.31), so every app is
113
+ /**
114
+ * Opens the checker for the app at `appRoot`, on a default tsconfig where it has none (38.30).
115
+ *
116
+ * AT ITS REAL PATH, which is the one bun resolves every import to: an editor holding the app
117
+ * through a link (`/tmp` is one on macOS) is spelled back at its own edge (`serveLanguage`).
118
+ */
119
+ export async function openChecker(appRoot: string): Promise<AppChecker> {
120
+ const root = realpathSync(appRoot)
121
+ // Where the app has no tsconfig it is typed against a default one (38.30), so every app is
88
122
  // read by the same compiler.
89
123
  const found = nearest(root, 'tsconfig.json')
90
124
  const opened = projectFiles(root)
@@ -93,14 +127,25 @@ export async function openChecker(root: string): Promise<AppChecker> {
93
127
  ? await writeDefaultProject(root)
94
128
  : { tsconfig: found, remove: async (): Promise<void> => {} }
95
129
  const session = await TypeSession.open(project.tsconfig)
130
+ session.root = root
96
131
  const moved = new Set<string>()
97
132
  let movedSomewhere = false
133
+ let sources: ReturnType<typeof sourcesUnder> | undefined
98
134
  return {
99
135
  session,
136
+ root,
100
137
  saw: (path) => {
138
+ sources = undefined
101
139
  if (path === undefined) movedSomewhere = true
102
140
  else moved.add(path)
103
141
  },
142
+ sources: () => {
143
+ sources ??= sourcesUnder(root).catch((thrown: unknown) => {
144
+ sources = undefined
145
+ throw thrown
146
+ })
147
+ return sources
148
+ },
104
149
  current: () => {
105
150
  if (projectFiles(root) !== opened) return false
106
151
  if (movedSomewhere || moved.size > 0)
@@ -146,7 +191,7 @@ export async function withChecker<Result>(
146
191
  }
147
192
 
148
193
  /**
149
- * Every `.abide` under the app, read, in path order — and the root as bun resolves it.
194
+ * Every `.abide` of the app's own, read, in path order — and the root as bun resolves it.
150
195
  *
151
196
  * `Bun.resolveSync` answers a symlink-free path (`/tmp` is `/private/tmp` on macOS), so a root
152
197
  * spelled through a link would have every import resolve to a file this walk never listed, and
@@ -157,36 +202,128 @@ export async function sourcesUnder(
157
202
  appRoot: string,
158
203
  ): Promise<{ root: string; sources: { path: string; text: string }[] }> {
159
204
  const root = realpathSync(appRoot)
160
- const paths = [
161
- ...new Bun.Glob('**/*.abide').scanSync({ cwd: root, absolute: true }),
162
- ].sort()
205
+ return { root, sources: await sourcesAt(root, abideUnder(root)) }
206
+ }
207
+
208
+ /** The `.abide` at each of `relatives`, below `root`, read. */
209
+ async function sourcesAt(
210
+ root: string,
211
+ relatives: readonly string[],
212
+ ): Promise<{ path: string; text: string }[]> {
213
+ const paths: string[] = []
214
+ for (const path of relatives) paths.push(`${root}/${path}`)
163
215
  const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
164
216
  const sources: { path: string; text: string }[] = []
165
217
  for (let at = 0; at < paths.length; at += 1)
166
218
  sources.push({ path: paths[at] as string, text: texts[at] as string })
167
- return { root, sources }
219
+ return sources
220
+ }
221
+
222
+ /**
223
+ * `parsed` and every `.abide` outside them that one of them imports — a dependency's, or a linked
224
+ * package's — parsed, and every one those import in turn; and every `.abide` under a directory one
225
+ * of them mounts, at the position the mount gives it (23.19).
226
+ *
227
+ * 34.27 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
228
+ * one of the app's in every way but its address: it is never routed (`placeOf`) unless mounted.
229
+ */
230
+ export async function withDependencies(
231
+ root: string,
232
+ parsed: readonly ParsedSource[],
233
+ resolve: (specifier: string, from: string) => string | undefined,
234
+ refused?: Diagnosis[],
235
+ ): Promise<ParsedSource[]> {
236
+ const all = [...parsed]
237
+ const byPath = new Map<string, ParsedSource>()
238
+ for (const source of all) byPath.set(source.path, source)
239
+ // 23.20 — a directory mounts once, and by whom.
240
+ const mountedBy = new Map<string, string>()
241
+ const refuse = (path: string, message: string): void => {
242
+ if (refused === undefined) throw new Error(message)
243
+ refused.push(refusal(path, new Error(message)))
244
+ }
245
+ const add = async (found: {
246
+ path: string
247
+ position?: string
248
+ }): Promise<void> => {
249
+ const text = await Bun.file(found.path).text()
250
+ for (const one of parseSources([{ ...found, text }], refused)) {
251
+ byPath.set(one.path, one)
252
+ all.push(one)
253
+ }
254
+ }
255
+ for (let at = 0; at < all.length; at += 1) {
256
+ const source = all[at] as ParsedSource
257
+ const position = source.position ?? relativeTo(root, source.path)
258
+ const mounts = mountsOf(root, source.path, position, source.file)
259
+ if (typeof mounts === 'string') {
260
+ refuse(source.path, mounts)
261
+ continue
262
+ }
263
+ for (const mount of mounts) {
264
+ const other = mountedBy.get(mount.directory)
265
+ if (other !== undefined) {
266
+ refuse(
267
+ source.path,
268
+ `abide: ${other} and ${position} both mount ${mount.specifier}, and a directory answers at one place. RULEBOOK 23.20.`,
269
+ )
270
+ continue
271
+ }
272
+ mountedBy.set(mount.directory, position)
273
+ for (const { path, position: placed } of mount.files) {
274
+ // A file a component import reached first takes its mount's position all the same.
275
+ const known = byPath.get(path)
276
+ if (known !== undefined) {
277
+ known.position = placed
278
+ continue
279
+ }
280
+ await add({ path, position: placed })
281
+ }
282
+ }
283
+ for (const target of abideImportsOf(
284
+ source.path,
285
+ source.file,
286
+ resolve,
287
+ ).values()) {
288
+ if (!byPath.has(target)) await add({ path: target })
289
+ }
290
+ }
291
+ return all
168
292
  }
169
293
 
170
294
  /**
171
- * Compiles every `.abide` source and handler of the app at `appRoot` into its output modules.
295
+ * Compiles every `.abide` source and handler of the app at `appRoot` (`Compilation`).
172
296
  *
173
- * `out` is where the modules will be written, which only a relative import's path back to its
174
- * source depends on (34.14). `kept` is a checker the caller holds across compiles and closes itself.
297
+ * `kept` is a checker the caller holds across compiles and closes itself.
175
298
  */
176
299
  export async function compileApp(
177
300
  appRoot: string,
178
- out = buildDirectory(appRoot),
179
301
  kept?: AppChecker,
180
302
  ): Promise<CompiledApp> {
181
- const { root, sources } = await sourcesUnder(appRoot)
182
- if (sources.length === 0 && !declaresHandlers(root))
183
- return { modules: new Map(), routes: [], sources: 0, warnings: [] }
303
+ const root = realpathSync(appRoot)
304
+ // ONE WALK for the sources and the public files, refusing a tree 23.16-23.18 forbids before
305
+ // anything is read.
306
+ const { abide, published } = appFiles(root)
307
+ const sources = await sourcesAt(root, abide)
308
+ if (sources.length === 0)
309
+ return {
310
+ files: {},
311
+ tables: {},
312
+ routes: [],
313
+ mounts: [],
314
+ sources: 0,
315
+ warnings: [],
316
+ published,
317
+ }
184
318
 
185
319
  // One session for the whole build: opening it spawns the checker, and the cost is paid once
186
320
  // rather than per file.
187
321
  return withChecker(root, kept, async (session) => {
188
322
  // A BUILD STOPS AT THE FIRST REFUSAL, so neither step is handed a list to report it in.
189
- const parsed = parseSources(sources)
323
+ const own = parseSources(sources)
324
+ await writeTypes(root, own)
325
+ const resolve = resolverIn()
326
+ const parsed = await withDependencies(root, own, resolve)
190
327
  const { read, warnings } = await readSources(session, parsed)
191
328
  const { ambient, unkeyed } = await session.lints(root, parsed)
192
329
  // 11.37 — a build error, every read named, rather than a first one and a second run.
@@ -196,14 +333,10 @@ export async function compileApp(
196
333
  lines.push(formatDiagnosis(read, process.cwd()))
197
334
  throw new Error(lines.join('\n'))
198
335
  }
199
- const emitted = await emitApp(
200
- root,
201
- session,
202
- read,
203
- posix.relative(resolve(appRoot), resolve(out)),
204
- )
336
+ const emitted = await emitApp(root, session, read, resolve)
205
337
  return {
206
338
  ...emitted,
339
+ published,
207
340
  sources: sources.length,
208
341
  // 11.30 — warned on, and the build goes on.
209
342
  warnings: [...warnings, ...unkeyed, ...emitted.warnings],
@@ -216,137 +349,260 @@ export async function compileApp(
216
349
  *
217
350
  * `abide check` has done both by then, file by file, and asks this for only what the whole app can
218
351
  * refuse: two handlers at one address, or a template the emit cannot lower.
219
- *
220
- * `output` is where the modules are written, relative to `root`.
221
352
  */
222
353
  export async function emitApp(
223
354
  root: string,
224
355
  session: TypeSession,
225
356
  sources: readonly ReadSource[],
226
- output: string,
227
- ): Promise<Pick<CompiledApp, 'modules' | 'routes' | 'warnings'>> {
228
- const modules = new Map<string, string>()
357
+ /** The resolver the sources' dependencies were found with, whose answers it keeps. */
358
+ resolve: (specifier: string, from: string) => string | undefined,
359
+ ): Promise<
360
+ Pick<CompiledApp, 'files' | 'tables' | 'routes' | 'mounts' | 'warnings'>
361
+ > {
362
+ const files: Record<string, CompiledFile> = {}
363
+ const tables: Record<string, string> = {}
229
364
  const routes: AppRoute[] = []
230
365
  // 32.17 AND 34.14 — WHICH FILES ARE INVOKED AS COMPONENTS, and where each import lands. A
231
366
  // caller's client module imports its child's, so a child with nothing to hydrate still
232
367
  // needs one to exist (`emitClient`'s no-op), and an invoked file carries its own markup
233
368
  // (`emitClient`). Built before the emit loop because a page may invoke a component the
234
369
  // walk reaches later.
235
- const graph = appGraph(root, sources, output, resolverIn(root))
370
+ const graph = appGraph(root, sources, resolve)
236
371
  const parsed = graph.nodes
237
372
 
238
- const layouts: string[] = []
239
- for (const entry of parsed)
240
- if (entry.kind === 'layout') layouts.push(entry.relative)
241
- // 35.2 — every `error.abide`, with the directory it answers for and the layouts around it.
373
+ // 35.1, 35.12 — every `layout.abide` by the route it covers, one to a route; 35.13 — and one
374
+ // with no template wraps nothing, being the route's hooks alone.
375
+ const layouts: AppLayout[] = []
376
+ const layoutAt = new Map<string, string>()
377
+ const errorAt = new Map<string, string>()
378
+ for (const entry of parsed) {
379
+ if (entry.kind !== 'layout' && entry.kind !== 'error') continue
380
+ const route = routeOf(entry.position)
381
+ const held = entry.kind === 'layout' ? layoutAt : errorAt
382
+ const other = held.get(shapeOf(route))
383
+ if (other !== undefined)
384
+ throw new Error(
385
+ `abide: ${other} and ${entry.relative} are both the ${entry.kind}.abide at ${route}. RULEBOOK 35.12.`,
386
+ )
387
+ held.set(shapeOf(route), entry.relative)
388
+ if (entry.kind === 'layout' && entry.file.template.length > 0)
389
+ layouts.push({
390
+ source: entry.relative,
391
+ segments: parseRoutePattern(route),
392
+ })
393
+ }
394
+ // 35.2 — every `error.abide`, with the route it answers for and the layouts around it.
242
395
  const errors: AppRoute[] = []
243
396
  for (const entry of parsed) {
244
397
  if (entry.kind !== 'page' && entry.kind !== 'error') continue
398
+ // 23.3, 35.14 — a `page.abide` or an `error.abide` with nothing to paint is its route's
399
+ // handlers and hooks alone.
400
+ if (!paints(entry.file)) continue
245
401
  ;(entry.kind === 'page' ? routes : errors).push({
246
402
  pattern: entry.route as string,
247
403
  source: entry.relative,
248
- layouts: layoutsAround(entry.relative, layouts),
404
+ layouts: layoutsAround(entry.route as string, layouts),
249
405
  })
250
406
  }
251
407
 
408
+ // 16.2 AND 16.28 — THE HANDLERS, read before a module is emitted: the server mounts each one,
409
+ // and the browser's module of the file declaring it carries its wrapper in the server block's
410
+ // place (34.23). Written whether or not there is a page: a handler is answered over http to a
411
+ // caller that has none.
412
+ const { mounts, warnings } = await handlersUnder(session, parsed)
413
+ refuseSharedRoutes(routes, mounts)
414
+ const standIns = standInsOf(mounts)
415
+ // 37.1 — every module a server block runs in, for the boot to import before `onStart`.
416
+ const servers: string[] = []
252
417
  let transitions = false
253
418
  for (const entry of parsed) {
419
+ // 35.16 — of the files with an address, the root layout alone is outermost in every render.
420
+ if (
421
+ entry.file.document !== undefined &&
422
+ entry.kind !== 'component' &&
423
+ !(entry.kind === 'layout' && routeOf(entry.position) === '/')
424
+ )
425
+ throw new Error(
426
+ `abide: ${entry.relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
427
+ )
254
428
  const emitted = emitSource(
255
429
  entry.relative,
256
430
  entry.file,
257
431
  entry.answer,
258
432
  graph.invoked.has(entry.path),
259
- graph.imports.get(entry.path),
433
+ standIns.get(entry.relative),
434
+ entry.text,
435
+ entry.position,
260
436
  )
261
- const stem = entry.relative.replace(/\.abide$/, '')
262
- if (emitted.sheet !== undefined)
263
- modules.set(`${stem}.style.css`, emitted.sheet)
437
+ if (entry.file.servers.length > 0) servers.push(entry.relative)
264
438
  transitions ||= emitted.transitions
265
- modules.set(`${stem}.ts`, emitted.server)
266
- if (emitted.client !== '')
267
- modules.set(`${stem}.client.ts`, emitted.client)
439
+ // 34.30 — a dependency's module runs on the app's runtime, whatever the package carries.
440
+ const onAppRuntime =
441
+ placeOf(entry.relative) === 'dependency'
442
+ ? (text: string) => onAbideOf(root, entry.path, text)
443
+ : (text: string) => text
444
+ files[entry.relative] = {
445
+ server: onAppRuntime(emitted.server),
446
+ client: onAppRuntime(emitted.client),
447
+ sheet: emitted.sheet,
448
+ // A map placing nothing — a file of markup alone — is left out.
449
+ serverMappings: placing(emitted.serverMappings),
450
+ clientMappings: placing(emitted.clientMappings),
451
+ }
268
452
  }
269
453
  // THE ROUTE TABLE, as the build's own knowledge rather than a list an app maintains. Which
270
- // addresses exist is a fact about the file tree (`page.abide` is an address, CLAUDE.md),
271
- // so a route that is not a directory cannot be written and a directory that is one cannot
272
- // be forgotten. The loaders are dynamic imports, which is what keeps a destination's
454
+ // addresses exist is a fact about the file tree (a `page.abide` is an address, 23.3),
455
+ // so a route that is not a file cannot be written and a file that is one cannot be
456
+ // forgotten. The loaders are dynamic imports, which is what keeps a destination's
273
457
  // markup and bindings off the entry page's bundle until somebody navigates there.
274
458
  //
275
- // TWO TABLES OFF ONE WALK. The client's loads the `.client.ts` that mounts a page and the
276
- // server's loads the `.ts` that paints it — the same addresses either way, which is what
277
- // keeps the pair from being two walks that can disagree. The server's is written with no
459
+ // TWO TABLES OFF ONE WALK, each importing a file's `?component` face — the same addresses
460
+ // either way, which is what keeps the pair from being two walks that can disagree. The
461
+ // server's is written with no
278
462
  // page too — an app of handlers alone still answers — and the client's, with the entry
279
463
  // that boots it, only where there is a page to boot.
280
- modules.set('server.ts', serverTable(routes, errors))
464
+ tables[SERVER_TABLE] = serverTable(routes, errors, servers, mounts)
281
465
  if (routes.length > 0) {
282
- // 39.14 — the shell the server paints this app into, read where the server reads it.
283
- const shellHead = keyedHead(await shellUnder(root))
284
- modules.set('routes.ts', clientTable(routes, transitions, shellHead))
466
+ tables[ROUTES_TABLE] = clientTable(routes, transitions)
285
467
  // The entry's SOURCE carries the name the bundle is written under — `entry-<hash>.js`
286
- // from a build and `entry.js` in dev — and its key in the manifest (38.23, 38.5).
287
- modules.set(`${CLIENT_ENTRY}.ts`, ENTRY_MODULE)
468
+ // from a build and `entry.js` in dev — and its key in the manifest (38.22, 38.5).
469
+ tables[`${CLIENT_ENTRY}.ts`] = ENTRY_MODULE
470
+ }
471
+ return {
472
+ files,
473
+ tables,
474
+ routes,
475
+ mounts,
476
+ warnings,
288
477
  }
289
- // 16.2 AND 16.28 — THE HANDLERS, TWICE OFF ONE READ: the table the server mounts them
290
- // from, and the module a browser is handed in place of each one's source. Written whether
291
- // or not there is a page: a handler is answered over http to a caller that has none.
292
- const { mounts, warnings } = await handlersUnder(root, session)
293
- modules.set('rpc.ts', mountTable(mounts))
294
- for (const [source, wrapper] of wrappersOf(mounts))
295
- modules.set(`${WIRE_DIRECTORY}/${source}`, wrapper)
296
- return { modules, routes, warnings }
297
478
  }
298
479
 
480
+ /**
481
+ * `text`, a module compiled from the dependency's `.abide` at `path`, with each of abide's own
482
+ * specifiers written as the path to the file the app's root resolves it to.
483
+ *
484
+ * 34.30 — RELATIVE, so the compilation still serves once the app and its `node_modules` are moved
485
+ * together, and THE FILE the app's own code loads, so the two are one module instance. A path and
486
+ * not a specifier a plugin answers: bun's runtime hands a plugin no bare specifier that names no
487
+ * file, so a mark on `@abide/abide/server` was never read.
488
+ */
489
+ function onAbideOf(root: string, path: string, text: string): string {
490
+ const from = dirname(realpathSync(path))
491
+ return respecified(text, (specifier) => {
492
+ if (
493
+ specifier !== SHARED_SPECIFIER &&
494
+ !specifier.startsWith(`${SHARED_SPECIFIER}/`)
495
+ )
496
+ return undefined
497
+ let target: string
498
+ try {
499
+ target = resolvedFile(specifier, root)
500
+ } catch {
501
+ return undefined
502
+ }
503
+ const relativePath = posix.relative(from, target)
504
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
505
+ })
506
+ }
507
+
508
+ // `mappings` that place something, or `undefined` for a module of markup alone.
509
+ function placing(mappings: string | undefined): string | undefined {
510
+ return mappings !== undefined && /[^;]/.test(mappings)
511
+ ? mappings
512
+ : undefined
513
+ }
514
+
515
+ /**
516
+ * How a table imports the `.abide` at `source`: from `__abide`, one directory below the root, as
517
+ * the module the file compiles to.
518
+ */
519
+ function tableImport(source: string): string {
520
+ return JSON.stringify(`../${source}${COMPONENT_QUERY}`)
521
+ }
522
+
523
+ // A layout and the route it covers, parsed.
524
+ type AppLayout = { source: string; segments: Segment[] }
525
+
299
526
  /**
300
527
  * The layouts a page renders inside, outermost first.
301
528
  *
302
- * Every `layout.abide` in a directory ON THE WAY DOWN to the page wraps it and no other does, so
303
- * the test is a prefix and the nesting order is the depth — which is what makes a section's own
304
- * chrome a file rather than a registration.
529
+ * 35.1 — every `layout.abide` whose route lies ON THE WAY DOWN to the page's wraps it and no other
530
+ * does: `src/layout.abide` wraps the page `src/media/page.abide` serves at `/media`, and so does
531
+ * `src/[section]/layout.abide`. So the test is a prefix of the route, kind by kind, and the nesting
532
+ * order is the route's depth.
305
533
  */
306
- function layoutsAround(page: string, layouts: string[]): string[] {
307
- const within = page.slice(0, page.lastIndexOf('/') + 1)
308
- const chain: string[] = []
309
- for (const layout of layouts) {
310
- const at = layout.slice(0, layout.lastIndexOf('/') + 1)
311
- if (within.startsWith(at)) chain.push(layout)
534
+ function layoutsAround(route: string, layouts: AppLayout[]): string[] {
535
+ const page = parseRoutePattern(route)
536
+ const chain: AppLayout[] = []
537
+ for (const layout of layouts)
538
+ if (coversRoute(layout.segments, page)) chain.push(layout)
539
+ // A shorter route is an outer one: they are prefixes of each other by construction.
540
+ chain.sort((one, two) => one.segments.length - two.segments.length)
541
+ const sources: string[] = []
542
+ for (const layout of chain) sources.push(layout.source)
543
+ return sources
544
+ }
545
+
546
+ /**
547
+ * A route with its parameters' names erased: what two routes answering one address share.
548
+ *
549
+ * 19.1 — `/media/[id]` and `/media/[slug]` are one route.
550
+ */
551
+ function shapeOf(route: string): string {
552
+ let shape = ''
553
+ for (const segment of parseRoutePattern(route))
554
+ shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
555
+ return shape === '' ? '/' : shape
556
+ }
557
+
558
+ /**
559
+ * Throws, naming both, where two files answer one method at one route.
560
+ *
561
+ * 19.1 — a page answers `GET` at its own, and a handler its method at its address.
562
+ */
563
+ function refuseSharedRoutes(routes: AppRoute[], mounts: Mount[]): void {
564
+ const owners = new Map<string, string>()
565
+ const claim = (method: string, route: string, source: string): void => {
566
+ const key = `${method} ${shapeOf(route)}`
567
+ const owner = owners.get(key)
568
+ if (owner !== undefined)
569
+ throw new Error(
570
+ `abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
571
+ )
572
+ owners.set(key, source)
312
573
  }
313
- // A shorter directory is an outer one: they are prefixes of each other by construction.
314
- chain.sort((one, two) => one.length - two.length)
315
- return chain
574
+ for (const route of routes) claim('GET', route.pattern, route.source)
575
+ for (const mount of mounts) claim(mount.method, mount.address, mount.source)
316
576
  }
317
577
 
318
578
  /**
319
- * Resolves a specifier from a directory as bun does, in `root`'s spelling, or `undefined` where it
320
- * does not resolve.
579
+ * Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
321
580
  *
322
581
  * A COMPONENT IMPORT RESOLVES THE WAY THE APP'S OWN IMPORTS DO — its import map, from the file
323
- * that wrote it. One that does not resolve is left as written, and the bundle names it.
324
- *
325
- * THE RESOLVER FOLLOWS SYMLINKS AND `root` MAY NOT: under a linked root — `/tmp` is one on macOS —
326
- * a component resolves outside the root's own spelling of the app, where an editor's file, its
327
- * unsaved text and the checked module pointed at it are not found. So an answer under the root's
328
- * real path is spelled back under `root`; a build hands `sourcesUnder`'s real root, and nothing
329
- * is spelled back.
582
+ * that wrote it. One that does not resolve is left as written, and the bundle names it. THE
583
+ * ANSWER IS A REAL PATH, as every root handed to the compiler is.
330
584
  */
331
- export function resolverIn(
332
- root: string,
333
- ): (specifier: string, from: string) => string | undefined {
334
- const linked = realpathSync(root)
585
+ export function resolverIn(): (
586
+ specifier: string,
587
+ from: string,
588
+ ) => string | undefined {
589
+ // ONE ANSWER PER SPECIFIER AND DIRECTORY for as long as the resolver lives: a compile resolves
590
+ // every import once for its dependencies and again for its graph, and hands both one resolver.
591
+ const answered = new Map<string, string | undefined>()
335
592
  return (specifier, from) => {
336
- const resolved = resolveFrom(specifier, from)
337
- return resolved !== undefined &&
338
- linked !== root &&
339
- resolved.startsWith(`${linked}/`)
340
- ? `${root}${resolved.slice(linked.length)}`
341
- : resolved
593
+ const key = `${from}\0${specifier}`
594
+ if (answered.has(key)) return answered.get(key)
595
+ const found = resolveFrom(specifier, from)
596
+ answered.set(key, found)
597
+ return found
342
598
  }
343
599
  }
344
600
 
345
601
  function resolveFrom(specifier: string, from: string): string | undefined {
346
602
  try {
347
- return Bun.resolveSync(specifier, from)
603
+ return resolvedFile(specifier, from)
348
604
  } catch {
349
- // 38.66 — BUN KEEPS A DIRECTORY'S LISTING FOR THE LIFE OF THE PROCESS, and looks a `#` specifier up
605
+ // 38.65 — BUN KEEPS A DIRECTORY'S LISTING FOR THE LIFE OF THE PROCESS, and looks a `#` specifier up
350
606
  // in it with no second look — where a relative one that misses reads the directory again.
351
607
  // `abide dev` compiles in one process for its whole session, so a component created since
352
608
  // its directory was listed never resolved, and was read as uninvoked. The `imports` map is
@@ -392,29 +648,21 @@ function throughImports(specifier: string, from: string): string | undefined {
392
648
  return undefined
393
649
  }
394
650
 
395
- /**
396
- * The rows of either route table, one per route.
397
- *
398
- * The two differ only in which module a pattern loads, so a row is written once and the extension
399
- * is the argument.
400
- */
651
+ /** The rows of either route table, one per route, `column` appending the fields only one carries. */
401
652
  function tableOf(
402
653
  routes: AppRoute[],
403
- extension: string,
404
654
  column: (route: AppRoute) => string = () => '',
405
655
  ): string {
406
656
  const rows: string[] = []
407
657
  for (const route of routes) {
408
- const module = `./${route.source.replace(/\.abide$/, extension)}`
409
658
  // 35.1 — THE CHAIN IS PART OF THE ADDRESS, so it is in the table rather than resolved at
410
659
  // run time: which layouts wrap a page is a fact about the file tree, and the server and
411
660
  // the client read it off the same walk for the reason the addresses are (30.8).
412
661
  const wrapping = route.layouts.map(
413
- (layout) =>
414
- `() => import(${JSON.stringify(`./${layout.replace(/\.abide$/, extension)}`)})`,
662
+ (layout) => `() => import(${tableImport(layout)})`,
415
663
  )
416
664
  rows.push(
417
- ` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${JSON.stringify(module)}), layouts: [${wrapping.join(', ')}]${column(route)} },`,
665
+ ` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${tableImport(route.source)}), layouts: [${wrapping.join(', ')}]${column(route)} },`,
418
666
  )
419
667
  }
420
668
  return rows.join('\n')
@@ -427,23 +675,19 @@ function tableOf(
427
675
  * tsconfig includes its build output typechecks it — and a bare `host` is an implicit `any` that
428
676
  * fails `tsc` under the `strict` every abide app is written against.
429
677
  */
430
- function clientTable(
431
- routes: AppRoute[],
432
- transitions: boolean,
433
- shellHead: string,
434
- ): string {
678
+ function clientTable(routes: AppRoute[], transitions: boolean): string {
435
679
  return `// Written by \`abide build\`. The addresses this app answers, and what to load for each.
436
680
  import { listenForRoutes, startRouter } from '${ROUTING_SPECIFIER}'
437
681
 
438
682
  export const routes = [
439
- ${tableOf(routes, '.client.ts')}
683
+ ${tableOf(routes)}
440
684
  ]
441
685
 
442
686
  // The router this document gets, the page it arrived on, and the two places a reader navigates
443
687
  // from (30.7). Hydrating here rather than leaving it to the app is what makes the page the server
444
688
  // painted live without anybody wiring it.
445
689
  export function start(host: Element) {
446
- const router = startRouter(routes, host, ${transitions}, ${JSON.stringify(shellHead)})
690
+ const router = startRouter(routes, host, ${transitions})
447
691
  listenForRoutes(router)
448
692
  const ready = router.hydrate()
449
693
  return { router, ready }
@@ -452,57 +696,70 @@ export function start(host: Element) {
452
696
  }
453
697
 
454
698
  /**
455
- * The source of `server.ts`: the same addresses, loading the module that PAINTS each page.
699
+ * The source of `server.ts`: the same addresses, loading the module that PAINTS each page, the
700
+ * error pages, the modules a server block runs in, and every handler.
456
701
  *
457
702
  * No `start` here and nothing imported: the server holds the matching and the ranking already,
458
703
  * and a server table that carried either would be a second copy of both.
459
704
  */
460
- function serverTable(routes: AppRoute[], errors: AppRoute[]): string {
705
+ function serverTable(
706
+ routes: AppRoute[],
707
+ errors: AppRoute[],
708
+ servers: string[],
709
+ mounts: Mount[],
710
+ ): string {
711
+ let loaders = ''
712
+ for (const source of servers)
713
+ loaders += ` () => import(${tableImport(source)}),\n`
714
+ let handlers = ''
715
+ for (const mount of mounts) {
716
+ const fields = JSON.stringify(mount)
717
+ // The module the handler is declared in — the file's server module, the route table's own.
718
+ handlers += ` { load: () => import(${tableImport(mount.source)}), ${fields.slice(1, -1)} },\n`
719
+ }
461
720
  return `// Written by \`abide build\`. The addresses this app answers, and what paints each.
462
721
  export const routes = [
463
- ${tableOf(routes, '.ts', clientModules)}
722
+ ${tableOf(routes, clientModules)}
464
723
  ]
465
724
 
466
725
  // 35.2 — the error pages, by the directory each answers for.
467
726
  export const errors = [
468
- ${tableOf(errors, '.ts')}
727
+ ${tableOf(errors)}
469
728
  ]
729
+
730
+ // 37.1 — every module a \`<script server>\` runs in, imported before the app boots.
731
+ export const servers = [
732
+ ${loaders}]
733
+
734
+ // 16.2 — every handler this app answers over http, and where each is.
735
+ export const handlers = [
736
+ ${handlers}]
470
737
  `
471
738
  }
472
739
 
473
740
  /**
474
741
  * The server row's `clientModules` column: what the browser will load at this address.
475
742
  *
476
- * 38.29 — named on the SERVER's row: the server writes the document, and the preloads for the page
743
+ * 38.28 — named on the SERVER's row: the server writes the document, and the preloads for the page
477
744
  * and its chain have to be in it before the client has run at all. Keys into the manifest
478
745
  * `manifestOf` reads off the bundle.
479
746
  */
480
747
  function clientModules(route: AppRoute): string {
481
- const modules: string[] = []
482
- for (const source of [route.source, ...route.layouts])
483
- modules.push(source.replace(/\.abide$/, '.client.ts'))
484
- return `, clientModules: ${JSON.stringify(modules)}`
485
- }
486
-
487
- /** The source of `rpc.ts`: the handler rows `mountHandlers` answers from. */
488
- function mountTable(mounts: Mount[]): string {
489
- return `// Written by \`abide build\`. Every handler this app answers over http, and where each is.
490
- export const handlers = ${JSON.stringify(mounts, null, 4)}
491
- `
748
+ return `, clientModules: ${JSON.stringify([route.source, ...route.layouts])}`
492
749
  }
493
750
 
494
751
  /**
495
- * The browser's stand-in module for each handler module, by source path.
752
+ * The wrappers the browser's module of each file exports in its server block's place, by source.
496
753
  *
497
- * 16.28 — WHAT A BROWSER IMPORTS WHERE THE APP WROTE A HANDLER'S MODULE: the same export names,
498
- * each the same call over the wire. The address is written RELATIVE to the bundle's own directory
499
- * and resolved against the chunk at run time (16.10), so an app mounted under a sub-path calls its
500
- * own handlers rather than the root's.
754
+ * 16.28, 34.23 — WHAT A BROWSER IMPORTS WHERE THE APP WROTE A HANDLER: the same export name, the
755
+ * same call over the wire. The address is written RELATIVE to the bundle's own directory and
756
+ * resolved against the chunk at run time (16.10), so an app mounted under a sub-path calls its own
757
+ * handlers rather than the root's.
501
758
  */
502
- function wrappersOf(mounts: Mount[]): Map<string, string> {
503
- const modules = new Map<string, string>()
759
+ function standInsOf(mounts: Mount[]): Map<string, string> {
760
+ const lines = new Map<string, string>()
504
761
  for (const mount of mounts) {
505
- // 20.4 — withheld from the browser: no wrapper, so an import of it is 16.29's refusal.
762
+ // 20.4 — withheld from the browser: no wrapper, so an import of it names nothing there.
506
763
  if (!mount.ui) continue
507
764
  const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
508
765
  // 18.4 — what the handler's value declares that the browser's copy behaves by.
@@ -513,47 +770,30 @@ function wrappersOf(mounts: Mount[]): Map<string, string> {
513
770
  : mount.copy === undefined
514
771
  ? ''
515
772
  : `, ${JSON.stringify(mount.copy)}`
516
- const call = `wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})`
517
- const line =
518
- mount.name === 'default'
519
- ? `export default ${call}\n`
520
- : `export const ${mount.name} = ${call}\n`
521
- modules.set(mount.source, (modules.get(mount.source) ?? '') + line)
773
+ const line = `export const ${mount.name} = __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
774
+ lines.set(mount.source, (lines.get(mount.source) ?? '') + line)
522
775
  }
523
- const wrappers = new Map<string, string>()
524
- for (const [source, body] of modules)
525
- wrappers.set(
776
+ const standIns = new Map<string, string>()
777
+ for (const [source, body] of lines)
778
+ standIns.set(
526
779
  source,
527
- `// Written by \`abide build\`. What a browser holds in place of ${source} (16.28).
528
- import { wire } from '${WIRE_SPECIFIER}'
529
-
780
+ `// 34.23 — what a browser holds in place of the server block.
781
+ import { wire as __wire } from '${WIRE_SPECIFIER}'
530
782
  ${body}`,
531
783
  )
532
- return wrappers
784
+ return standIns
785
+ }
786
+
787
+ /** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
788
+ export function paints(file: AbideFile): boolean {
789
+ return file.template.length > 0 || file.head.length > 0
533
790
  }
534
791
 
535
792
  // THE ENTRY. An app does not write it: which module boots the client is the build's knowledge,
536
793
  // exactly as the route table is, and a hand-written entry is one more file to keep in step with a
537
794
  // layout that moves.
538
795
  const ENTRY_MODULE = `// Written by \`abide build\`. What boots this app in a browser.
539
- import { start } from './routes.ts'
796
+ import { start } from '../${VIRTUAL_DIRECTORY}/${ROUTES_TABLE}'
540
797
 
541
798
  start(document.body)
542
799
  `
543
-
544
- /**
545
- * The nearest file of this name at or above `from`, or `undefined`.
546
- *
547
- * The compiler needs a project for 31.3, and an app's own tsconfig is the one whose paths and lib
548
- * its pages are written against.
549
- */
550
- function nearest(from: string, name: string): string | undefined {
551
- let at = from
552
- while (true) {
553
- const candidate = `${at}/${name}`
554
- if (existsSync(candidate)) return candidate
555
- const up = dirname(at)
556
- if (up === at) return undefined
557
- at = up
558
- }
559
- }