@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
@@ -1,28 +1,59 @@
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 { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
16
+ import {
17
+ coversRoute,
18
+ parseRoutePattern,
19
+ placeOf,
20
+ routeOf,
21
+ type Segment,
22
+ } from '#shared/routePattern.ts'
23
+ import {
24
+ ROUTING_SPECIFIER,
25
+ SHARED_SPECIFIER,
26
+ WIRE_SPECIFIER,
27
+ } from '#shared/SPECIFIERS.ts'
28
+ import { abideImportsOf, appGraph } from './componentGraph.ts'
10
29
  import { emitSource } from './emitSource.ts'
11
30
  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'
31
+ import {
32
+ type HandlerFile,
33
+ handlersUnder,
34
+ type Refuse,
35
+ THROW,
36
+ } from './handlersUnder.ts'
37
+ import { hooksCalled } from './hooksCalled.ts'
38
+ import { mountsOf } from './mountsOf.ts'
39
+ import { nearest } from './nearest.ts'
40
+ import type { AbideFile } from './parseAbide.ts'
41
+ import {
42
+ type ParsedSource,
43
+ parseSources,
44
+ type ReadSource,
45
+ readSources,
46
+ refusal,
47
+ } from './readSources.ts'
48
+ import { respecified } from './splitImports.ts'
15
49
  import { type Diagnosis, TypeSession } from './TypeSession.ts'
16
50
  import { writeDefaultProject } from './writeDefaultProject.ts'
17
51
 
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).
52
+ // AN APP'S `.abide` SOURCES, COMPILED — every file keyed by its path below the root, and loaded
53
+ // there through its `?component` face (`abideModules`). Nothing here touches the output directory:
54
+ // `abide build` writes what this hands back and `abide dev` keeps it in memory (38.16), and the two
55
+ // commands serving different markup from the same sources is exactly what one compile in two sinks
56
+ // makes impossible.
26
57
 
27
58
  type AppRoute = {
28
59
  /** The address, in the bracket spelling `routePattern` parses. */
@@ -38,21 +69,17 @@ type AppRoute = {
38
69
  layouts: string[]
39
70
  }
40
71
 
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>
72
+ /** An app's compiled output: every file and table (`Compilation`), its routes, and its warnings. */
73
+ export type CompiledApp = Compilation & {
51
74
  routes: AppRoute[]
75
+ /** Every handler, where it is mounted. 16.2. */
76
+ mounts: Mount[]
52
77
  /** How many `.abide` files were read, which is what the commands report. */
53
78
  sources: number
54
79
  /** What the compiler has to say that does not stop the build, on the `.abide` line. */
55
80
  warnings: Diagnosis[]
81
+ /** Every public file's source path to its address, as `appFiles` read them. 36.11. */
82
+ published: Map<string, string>
56
83
  }
57
84
 
58
85
  /**
@@ -64,12 +91,19 @@ export type CompiledApp = {
64
91
  */
65
92
  export type AppChecker = {
66
93
  session: TypeSession
94
+ /** The app's root, real — every path a checker is asked about is spelled under it. */
95
+ root: string
67
96
  /**
68
97
  * Records a path a watcher saw move, or `undefined` for a move it could not place.
69
98
  *
70
99
  * Held until `current`.
71
100
  */
72
101
  saw: (path: string | undefined) => void
102
+ /**
103
+ * The app's own `.abide`, read (`sourcesUnder`) — kept until `saw` hears anything move, so a
104
+ * check asked again and again while an editor types re-reads nothing on disk.
105
+ */
106
+ sources: () => ReturnType<typeof sourcesUnder>
73
107
  /**
74
108
  * Whether the project it was opened on is still the app's; if so, flushes what `saw` held.
75
109
  *
@@ -82,9 +116,15 @@ export type AppChecker = {
82
116
  close: () => Promise<void>
83
117
  }
84
118
 
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
119
+ /**
120
+ * Opens the checker for the app at `appRoot`, on a default tsconfig where it has none (38.30).
121
+ *
122
+ * AT ITS REAL PATH, which is the one bun resolves every import to: an editor holding the app
123
+ * through a link (`/tmp` is one on macOS) is spelled back at its own edge (`serveLanguage`).
124
+ */
125
+ export async function openChecker(appRoot: string): Promise<AppChecker> {
126
+ const root = realpathSync(appRoot)
127
+ // Where the app has no tsconfig it is typed against a default one (38.30), so every app is
88
128
  // read by the same compiler.
89
129
  const found = nearest(root, 'tsconfig.json')
90
130
  const opened = projectFiles(root)
@@ -93,14 +133,25 @@ export async function openChecker(root: string): Promise<AppChecker> {
93
133
  ? await writeDefaultProject(root)
94
134
  : { tsconfig: found, remove: async (): Promise<void> => {} }
95
135
  const session = await TypeSession.open(project.tsconfig)
136
+ session.root = root
96
137
  const moved = new Set<string>()
97
138
  let movedSomewhere = false
139
+ let sources: ReturnType<typeof sourcesUnder> | undefined
98
140
  return {
99
141
  session,
142
+ root,
100
143
  saw: (path) => {
144
+ sources = undefined
101
145
  if (path === undefined) movedSomewhere = true
102
146
  else moved.add(path)
103
147
  },
148
+ sources: () => {
149
+ sources ??= sourcesUnder(root).catch((thrown: unknown) => {
150
+ sources = undefined
151
+ throw thrown
152
+ })
153
+ return sources
154
+ },
104
155
  current: () => {
105
156
  if (projectFiles(root) !== opened) return false
106
157
  if (movedSomewhere || moved.size > 0)
@@ -146,7 +197,7 @@ export async function withChecker<Result>(
146
197
  }
147
198
 
148
199
  /**
149
- * Every `.abide` under the app, read, in path order — and the root as bun resolves it.
200
+ * Every `.abide` of the app's own, read, in path order — and the root as bun resolves it.
150
201
  *
151
202
  * `Bun.resolveSync` answers a symlink-free path (`/tmp` is `/private/tmp` on macOS), so a root
152
203
  * spelled through a link would have every import resolve to a file this walk never listed, and
@@ -157,36 +208,175 @@ export async function sourcesUnder(
157
208
  appRoot: string,
158
209
  ): Promise<{ root: string; sources: { path: string; text: string }[] }> {
159
210
  const root = realpathSync(appRoot)
160
- const paths = [
161
- ...new Bun.Glob('**/*.abide').scanSync({ cwd: root, absolute: true }),
162
- ].sort()
211
+ return { root, sources: await sourcesAt(root, abideUnder(root)) }
212
+ }
213
+
214
+ /** The `.abide` at each of `relatives`, below `root`, read. */
215
+ async function sourcesAt(
216
+ root: string,
217
+ relatives: readonly string[],
218
+ ): Promise<{ path: string; text: string }[]> {
219
+ const paths: string[] = []
220
+ for (const path of relatives) paths.push(`${root}/${path}`)
163
221
  const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
164
222
  const sources: { path: string; text: string }[] = []
165
223
  for (let at = 0; at < paths.length; at += 1)
166
224
  sources.push({ path: paths[at] as string, text: texts[at] as string })
167
- return { root, sources }
225
+ return sources
168
226
  }
169
227
 
170
228
  /**
171
- * Compiles every `.abide` source and handler of the app at `appRoot` into its output modules.
229
+ * `parsed` and every `.abide` outside them that one of them imports — a dependency's, or a linked
230
+ * package's — parsed, and every one those import in turn; and every `.abide` under a directory one
231
+ * of them mounts, at the position the mount gives it (23.19). With them, the app's own handler files
232
+ * — `own`, below `root`, as its walk found them (`appFiles`) — and every one a mounted directory
233
+ * holds, each at the position it answers by.
172
234
  *
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.
235
+ * 34.23 — COMPILED WITH THE APP, by the app's checker, so a component a package ships as source is
236
+ * one of the app's in every way but its address: it is never routed (`placeOf`) unless mounted.
237
+ */
238
+ export async function withDependencies(
239
+ root: string,
240
+ parsed: readonly ParsedSource[],
241
+ own: readonly string[],
242
+ resolve: (specifier: string, from: string) => string | undefined,
243
+ refused?: Diagnosis[],
244
+ ): Promise<{
245
+ sources: ParsedSource[]
246
+ handlers: HandlerFile[]
247
+ mounted: Compilation['mounted']
248
+ }> {
249
+ const all = [...parsed]
250
+ const handlers: HandlerFile[] = []
251
+ for (const relative of own)
252
+ handlers.push({
253
+ path: `${root}/${relative}`,
254
+ relative,
255
+ position: relative,
256
+ })
257
+ // The mounted handler files placed so far; the app's own is never mounted (23.20).
258
+ const handled = new Set<string>()
259
+ const mounted: Compilation['mounted'] = {}
260
+ const byPath = new Map<string, ParsedSource>()
261
+ for (const source of all) byPath.set(source.path, source)
262
+ // 23.20 — a directory mounts once, and by whom.
263
+ const mountedBy = new Map<string, string>()
264
+ const refuse = (path: string, message: string): void => {
265
+ if (refused === undefined) throw new Error(message)
266
+ refused.push(refusal(path, new Error(message)))
267
+ }
268
+ const add = async (found: {
269
+ path: string
270
+ position?: string
271
+ }): Promise<void> => {
272
+ const text = await Bun.file(found.path).text()
273
+ for (const one of parseSources([{ ...found, text }], refused)) {
274
+ byPath.set(one.path, one)
275
+ all.push(one)
276
+ }
277
+ }
278
+ for (let at = 0; at < all.length; at += 1) {
279
+ const source = all[at] as ParsedSource
280
+ const position = source.position ?? relativeTo(root, source.path)
281
+ const mounts = mountsOf(root, source.path, position, source.file)
282
+ if (typeof mounts === 'string') {
283
+ refuse(source.path, mounts)
284
+ continue
285
+ }
286
+ for (const mount of mounts) {
287
+ const other = mountedBy.get(mount.directory)
288
+ if (other !== undefined) {
289
+ refuse(
290
+ source.path,
291
+ `abide: ${other} and ${position} both mount ${mount.specifier}, and a directory answers at one place. RULEBOOK 23.20.`,
292
+ )
293
+ continue
294
+ }
295
+ mountedBy.set(mount.directory, position)
296
+ mounted[relativeTo(root, mount.directory)] = mount.at
297
+ for (const placed of mount.handlers) {
298
+ // Two mounts can overlap, one directory inside the other; a file is placed once.
299
+ if (handled.has(placed.path)) continue
300
+ handled.add(placed.path)
301
+ handlers.push({
302
+ path: placed.path,
303
+ relative: relativeTo(root, placed.path),
304
+ position: placed.position,
305
+ })
306
+ }
307
+ for (const { path, position: placed } of mount.files) {
308
+ // A file a component import reached first takes its mount's position all the same.
309
+ const known = byPath.get(path)
310
+ if (known !== undefined) {
311
+ known.position = placed
312
+ continue
313
+ }
314
+ await add({ path, position: placed })
315
+ }
316
+ }
317
+ for (const target of abideImportsOf(
318
+ source.path,
319
+ source.file,
320
+ resolve,
321
+ ).values()) {
322
+ if (!byPath.has(target)) await add({ path: target })
323
+ }
324
+ }
325
+ return { sources: all, handlers, mounted }
326
+ }
327
+
328
+ /**
329
+ * The `Compilation` alone out of `app`, which is what is written to disk and served: the rest of a
330
+ * compiled app is the commands' to read.
331
+ */
332
+ export function compilationOf(app: CompiledApp): Compilation {
333
+ return {
334
+ files: app.files,
335
+ handlers: app.handlers,
336
+ tables: app.tables,
337
+ mounted: app.mounted,
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Compiles every `.abide` source and handler of the app at `appRoot` (`Compilation`).
343
+ *
344
+ * `kept` is a checker the caller holds across compiles and closes itself.
175
345
  */
176
346
  export async function compileApp(
177
347
  appRoot: string,
178
- out = buildDirectory(appRoot),
179
348
  kept?: AppChecker,
180
349
  ): 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: [] }
350
+ const root = realpathSync(appRoot)
351
+ // ONE WALK for the sources, the handler files and the public files, refusing a tree 23.16-23.18
352
+ // forbids before anything is read.
353
+ const { abide, handlers, published } = appFiles(root)
354
+ const sources = await sourcesAt(root, abide)
355
+ // 16.2 — an app of handler files alone is still an app.
356
+ if (sources.length === 0 && handlers.length === 0)
357
+ return {
358
+ files: {},
359
+ tables: {},
360
+ handlers: {},
361
+ mounted: {},
362
+ routes: [],
363
+ mounts: [],
364
+ sources: 0,
365
+ warnings: [],
366
+ published,
367
+ }
184
368
 
185
369
  // One session for the whole build: opening it spawns the checker, and the cost is paid once
186
370
  // rather than per file.
187
371
  return withChecker(root, kept, async (session) => {
188
372
  // A BUILD STOPS AT THE FIRST REFUSAL, so neither step is handed a list to report it in.
189
- const parsed = parseSources(sources)
373
+ const own = parseSources(sources)
374
+ const resolve = resolverIn()
375
+ const {
376
+ sources: parsed,
377
+ handlers: handlerFiles,
378
+ mounted,
379
+ } = await withDependencies(root, own, handlers, resolve)
190
380
  const { read, warnings } = await readSources(session, parsed)
191
381
  const { ambient, unkeyed } = await session.lints(root, parsed)
192
382
  // 11.37 — a build error, every read named, rather than a first one and a second run.
@@ -200,10 +390,13 @@ export async function compileApp(
200
390
  root,
201
391
  session,
202
392
  read,
203
- posix.relative(resolve(appRoot), resolve(out)),
393
+ resolve,
394
+ handlerFiles,
204
395
  )
205
396
  return {
206
397
  ...emitted,
398
+ mounted,
399
+ published,
207
400
  sources: sources.length,
208
401
  // 11.30 — warned on, and the build goes on.
209
402
  warnings: [...warnings, ...unkeyed, ...emitted.warnings],
@@ -216,137 +409,340 @@ export async function compileApp(
216
409
  *
217
410
  * `abide check` has done both by then, file by file, and asks this for only what the whole app can
218
411
  * 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
412
  */
222
413
  export async function emitApp(
223
414
  root: string,
224
415
  session: TypeSession,
225
416
  sources: readonly ReadSource[],
226
- output: string,
227
- ): Promise<Pick<CompiledApp, 'modules' | 'routes' | 'warnings'>> {
228
- const modules = new Map<string, string>()
417
+ /** The resolver the sources' dependencies were found with, whose answers it keeps. */
418
+ resolve: (specifier: string, from: string) => string | undefined,
419
+ /** The handler files, the app's own and each mounted one (`withDependencies`). */
420
+ handlerFiles: readonly HandlerFile[],
421
+ /** Told of each refusal at its file; the build's throws the first. */
422
+ refuse: Refuse = THROW,
423
+ ): Promise<
424
+ Pick<
425
+ CompiledApp,
426
+ 'files' | 'tables' | 'handlers' | 'routes' | 'mounts' | 'warnings'
427
+ >
428
+ > {
429
+ const files: Record<string, CompiledFile> = {}
430
+ const tables: Record<string, string> = {}
229
431
  const routes: AppRoute[] = []
230
432
  // 32.17 AND 34.14 — WHICH FILES ARE INVOKED AS COMPONENTS, and where each import lands. A
231
433
  // caller's client module imports its child's, so a child with nothing to hydrate still
232
434
  // needs one to exist (`emitClient`'s no-op), and an invoked file carries its own markup
233
435
  // (`emitClient`). Built before the emit loop because a page may invoke a component the
234
436
  // walk reaches later.
235
- const graph = appGraph(root, sources, output, resolverIn(root))
437
+ const graph = appGraph(root, sources, resolve)
236
438
  const parsed = graph.nodes
237
439
 
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.
440
+ // 35.1, 35.12 — every `layout.abide` by the route it covers, one to a route; 35.13 — and one
441
+ // with no template wraps nothing, being the route's hooks alone.
442
+ const layouts: AppLayout[] = []
443
+ const layoutAt = new Map<string, string>()
444
+ const errorAt = new Map<string, string>()
445
+ for (const entry of parsed) {
446
+ if (entry.kind !== 'layout' && entry.kind !== 'error') continue
447
+ const route = routeOf(entry.position)
448
+ const held = entry.kind === 'layout' ? layoutAt : errorAt
449
+ const other = held.get(shapeOf(route))
450
+ if (other !== undefined) {
451
+ refuse(
452
+ entry.relative,
453
+ new Error(
454
+ `abide: ${other} and ${entry.relative} are both the ${entry.kind}.abide at ${route}. RULEBOOK 35.12.`,
455
+ ),
456
+ )
457
+ continue
458
+ }
459
+ held.set(shapeOf(route), entry.relative)
460
+ if (entry.kind === 'layout' && entry.file.template.length > 0)
461
+ layouts.push({
462
+ source: entry.relative,
463
+ segments: parseRoutePattern(route),
464
+ })
465
+ }
466
+ // 35.2 — every `error.abide`, with the route it answers for and the layouts around it.
242
467
  const errors: AppRoute[] = []
243
468
  for (const entry of parsed) {
244
469
  if (entry.kind !== 'page' && entry.kind !== 'error') continue
470
+ // 23.3, 35.14 — a page or an `error.abide` with nothing to paint is its route's hooks
471
+ // alone.
472
+ if (!paints(entry.file)) continue
245
473
  ;(entry.kind === 'page' ? routes : errors).push({
246
474
  pattern: entry.route as string,
247
475
  source: entry.relative,
248
- layouts: layoutsAround(entry.relative, layouts),
476
+ layouts: layoutsAround(entry.route as string, layouts),
249
477
  })
250
478
  }
251
479
 
480
+ // 16.2 AND 16.28 — THE HANDLERS, read before a module is emitted: the server mounts each one,
481
+ // and the browser holds its wrapper in its place — a handler file's as that file, a private
482
+ // handler's in the server block's place in its own file's module. Written whether or not there
483
+ // is a page: a handler is answered over http to a caller that has none.
484
+ const { mounts, warnings } = await handlersUnder(
485
+ session,
486
+ parsed,
487
+ handlerFiles,
488
+ refuse,
489
+ )
490
+ refuseSharedRoutes(routes, mounts, refuse)
491
+ refuseStrayLauncher(parsed, handlerFiles, refuse)
492
+ const standIns = standInsOf(mounts)
493
+ // 37.1 — every module a server block or a handler file runs in, for the boot to import before
494
+ // `onStart`.
495
+ const servers: string[] = []
496
+ for (const file of handlerFiles) servers.push(file.relative)
252
497
  let transitions = false
253
498
  for (const entry of parsed) {
499
+ // 35.16 — of the files with an address, the root layout alone is outermost in every render.
500
+ if (
501
+ entry.file.document !== undefined &&
502
+ entry.kind !== 'component' &&
503
+ !(entry.kind === 'layout' && routeOf(entry.position) === '/')
504
+ ) {
505
+ refuse(
506
+ entry.relative,
507
+ new Error(
508
+ `abide: ${entry.relative} writes <html>, and of the pages, layouts and error pages only the root layout.abide writes the document. RULEBOOK 35.16.`,
509
+ ),
510
+ )
511
+ continue
512
+ }
254
513
  const emitted = emitSource(
255
514
  entry.relative,
256
515
  entry.file,
257
516
  entry.answer,
258
517
  graph.invoked.has(entry.path),
259
- graph.imports.get(entry.path),
518
+ standIns.get(entry.relative),
519
+ entry.text,
520
+ entry.position,
260
521
  )
261
- const stem = entry.relative.replace(/\.abide$/, '')
262
- if (emitted.sheet !== undefined)
263
- modules.set(`${stem}.style.css`, emitted.sheet)
522
+ if (entry.file.servers.length > 0) servers.push(entry.relative)
264
523
  transitions ||= emitted.transitions
265
- modules.set(`${stem}.ts`, emitted.server)
266
- if (emitted.client !== '')
267
- modules.set(`${stem}.client.ts`, emitted.client)
524
+ // 34.26 — a dependency's module runs on the app's runtime, whatever the package carries.
525
+ const onAppRuntime =
526
+ placeOf(entry.relative) === 'dependency'
527
+ ? (text: string) => onAbideOf(root, entry.path, text)
528
+ : (text: string) => text
529
+ files[entry.relative] = {
530
+ server: onAppRuntime(emitted.server),
531
+ client: onAppRuntime(emitted.client),
532
+ sheet: emitted.sheet,
533
+ // A map placing nothing — a file of markup alone — is left out.
534
+ serverMappings: placing(emitted.serverMappings),
535
+ clientMappings: placing(emitted.clientMappings),
536
+ }
268
537
  }
269
538
  // 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
539
+ // addresses exist is a fact about the file tree (a page is an address, 23.3),
540
+ // so a route that is not a file cannot be written and a file that is one cannot be
541
+ // forgotten. The loaders are dynamic imports, which is what keeps a destination's
273
542
  // markup and bindings off the entry page's bundle until somebody navigates there.
274
543
  //
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
544
+ // TWO TABLES OFF ONE WALK, each importing a file's `?component` face — the same addresses
545
+ // either way, which is what keeps the pair from being two walks that can disagree. The
546
+ // server's is written with no
278
547
  // page too — an app of handlers alone still answers — and the client's, with the entry
279
548
  // that boots it, only where there is a page to boot.
280
- modules.set('server.ts', serverTable(routes, errors))
549
+ tables[SERVER_TABLE] = serverTable(routes, errors, servers, mounts)
550
+ // 16.74 — what the browser holds of each handler file, and the route its hooks are filed at:
551
+ // read by the module's load, which runs before the server table is imported.
552
+ const handlers: Compilation['handlers'] = {}
553
+ for (const mount of mounts)
554
+ if (!mount.private)
555
+ handlers[mount.source] = {
556
+ scope: mount.scope,
557
+ client: standIns.get(mount.source) ?? '',
558
+ }
281
559
  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))
560
+ tables[ROUTES_TABLE] = clientTable(routes, transitions)
285
561
  // 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)
562
+ // from a build and `entry.js` in dev — and its key in the manifest (38.22, 38.5).
563
+ tables[`${CLIENT_ENTRY}.ts`] = ENTRY_MODULE
564
+ }
565
+ return {
566
+ files,
567
+ tables,
568
+ handlers,
569
+ routes,
570
+ mounts,
571
+ warnings,
288
572
  }
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
573
  }
298
574
 
575
+ /**
576
+ * `text`, a module compiled from the dependency's `.abide` at `path`, with each of abide's own
577
+ * specifiers written as the path to the file the app's root resolves it to.
578
+ *
579
+ * 34.26 — RELATIVE, so the compilation still serves once the app and its `node_modules` are moved
580
+ * together, and THE FILE the app's own code loads, so the two are one module instance. A path and
581
+ * not a specifier a plugin answers: bun's runtime hands a plugin no bare specifier that names no
582
+ * file, so a mark on `@abide/abide/server` was never read.
583
+ */
584
+ function onAbideOf(root: string, path: string, text: string): string {
585
+ const from = dirname(realpathSync(path))
586
+ return respecified(text, (specifier) => {
587
+ if (
588
+ specifier !== SHARED_SPECIFIER &&
589
+ !specifier.startsWith(`${SHARED_SPECIFIER}/`)
590
+ )
591
+ return undefined
592
+ let target: string
593
+ try {
594
+ target = resolvedFile(specifier, root)
595
+ } catch {
596
+ return undefined
597
+ }
598
+ const relativePath = posix.relative(from, target)
599
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
600
+ })
601
+ }
602
+
603
+ // `mappings` that place something, or `undefined` for a module of markup alone.
604
+ function placing(mappings: string | undefined): string | undefined {
605
+ return mappings !== undefined && /[^;]/.test(mappings)
606
+ ? mappings
607
+ : undefined
608
+ }
609
+
610
+ /**
611
+ * How a table imports the `.abide` at `source`: from `__abide`, one directory below the root, as
612
+ * the module the file compiles to.
613
+ */
614
+ function tableImport(source: string): string {
615
+ return JSON.stringify(
616
+ source.endsWith('.abide')
617
+ ? `../${source}${COMPONENT_QUERY}`
618
+ : `../${source}`,
619
+ )
620
+ }
621
+
622
+ // A layout and the route it covers, parsed.
623
+ type AppLayout = { source: string; segments: Segment[] }
624
+
299
625
  /**
300
626
  * The layouts a page renders inside, outermost first.
301
627
  *
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.
628
+ * 35.1 — every `layout.abide` whose route lies ON THE WAY DOWN to the page's wraps it and no other
629
+ * does: `src/layout.abide` wraps the page `src/media/index.abide` serves at `/media`, and so does
630
+ * `src/[section]/layout.abide`. So the test is a prefix of the route, kind by kind, and the nesting
631
+ * order is the route's depth.
305
632
  */
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)
633
+ function layoutsAround(route: string, layouts: AppLayout[]): string[] {
634
+ const page = parseRoutePattern(route)
635
+ const chain: AppLayout[] = []
636
+ for (const layout of layouts)
637
+ if (coversRoute(layout.segments, page)) chain.push(layout)
638
+ // A shorter route is an outer one: they are prefixes of each other by construction.
639
+ chain.sort((one, two) => one.segments.length - two.segments.length)
640
+ const sources: string[] = []
641
+ for (const layout of chain) sources.push(layout.source)
642
+ return sources
643
+ }
644
+
645
+ /**
646
+ * A route with its parameters' names erased: what two routes answering one address share.
647
+ *
648
+ * 19.1 — `/media/[id]` and `/media/[slug]` are one route.
649
+ */
650
+ function shapeOf(route: string): string {
651
+ let shape = ''
652
+ for (const segment of parseRoutePattern(route))
653
+ shape += `/${segment.kind === 'static' ? segment.name : `[${segment.kind}]`}`
654
+ return shape === '' ? '/' : shape
655
+ }
656
+
657
+ /**
658
+ * Refuses, naming both, where two files answer one method at one route — at the second.
659
+ *
660
+ * 19.1 — a page answers `GET` at its own, and a handler its method at its address.
661
+ */
662
+ function refuseSharedRoutes(
663
+ routes: AppRoute[],
664
+ mounts: Mount[],
665
+ refuse: Refuse,
666
+ ): void {
667
+ const owners = new Map<string, string>()
668
+ const claim = (method: string, route: string, source: string): void => {
669
+ const key = `${method} ${shapeOf(route)}`
670
+ const owner = owners.get(key)
671
+ if (owner === undefined) owners.set(key, source)
672
+ else
673
+ refuse(
674
+ source,
675
+ new Error(
676
+ `abide: ${owner} and ${source} both answer ${method} at ${route}. RULEBOOK 19.1.`,
677
+ ),
678
+ )
312
679
  }
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
680
+ for (const route of routes) claim('GET', route.pattern, route.source)
681
+ for (const mount of mounts) claim(mount.method, mount.address, mount.source)
316
682
  }
317
683
 
318
684
  /**
319
- * Resolves a specifier from a directory as bun does, in `root`'s spelling, or `undefined` where it
320
- * does not resolve.
685
+ * Refuses, naming the file, where anything but the root's `launcher.abide` calls `onLauncher`.
321
686
  *
322
- * 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.
687
+ * 38.107 — the launch loads that file alone, so a call anywhere else is never read.
688
+ */
689
+ function refuseStrayLauncher(
690
+ sources: readonly { relative: string; position: string; file: AbideFile }[],
691
+ handlerFiles: readonly HandlerFile[],
692
+ refuse: Refuse,
693
+ ): void {
694
+ const stray = (relative: string): void =>
695
+ refuse(
696
+ relative,
697
+ new Error(
698
+ `abide: ${relative} calls onLauncher, which is read from src/${LAUNCHER_PAGE} alone. Move the call there. RULEBOOK 38.107.`,
699
+ ),
700
+ )
701
+ for (const source of sources)
702
+ if (
703
+ source.position !== LAUNCHER_PAGE &&
704
+ source.file.servers.some(callsLauncher)
705
+ )
706
+ stray(source.relative)
707
+ for (const file of handlerFiles)
708
+ if (callsLauncher(readFileSync(file.path, 'utf8'))) stray(file.relative)
709
+ }
710
+
711
+ function callsLauncher(source: string): boolean {
712
+ return (
713
+ source.includes('onLauncher') &&
714
+ hooksCalled(source).includes('onLauncher')
715
+ )
716
+ }
717
+
718
+ /**
719
+ * Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
324
720
  *
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.
721
+ * A COMPONENT IMPORT RESOLVES THE WAY THE APP'S OWN IMPORTS DO — its import map, from the file
722
+ * that wrote it. One that does not resolve is left as written, and the bundle names it. THE
723
+ * ANSWER IS A REAL PATH, as every root handed to the compiler is.
330
724
  */
331
- export function resolverIn(
332
- root: string,
333
- ): (specifier: string, from: string) => string | undefined {
334
- const linked = realpathSync(root)
725
+ export function resolverIn(): (
726
+ specifier: string,
727
+ from: string,
728
+ ) => string | undefined {
729
+ // ONE ANSWER PER SPECIFIER AND DIRECTORY for as long as the resolver lives: a compile resolves
730
+ // every import once for its dependencies and again for its graph, and hands both one resolver.
731
+ const answered = new Map<string, string | undefined>()
335
732
  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
733
+ const key = `${from}\0${specifier}`
734
+ if (answered.has(key)) return answered.get(key)
735
+ const found = resolveFrom(specifier, from)
736
+ answered.set(key, found)
737
+ return found
342
738
  }
343
739
  }
344
740
 
345
741
  function resolveFrom(specifier: string, from: string): string | undefined {
346
742
  try {
347
- return Bun.resolveSync(specifier, from)
743
+ return resolvedFile(specifier, from)
348
744
  } catch {
349
- // 38.66 — BUN KEEPS A DIRECTORY'S LISTING FOR THE LIFE OF THE PROCESS, and looks a `#` specifier up
745
+ // 38.65 — BUN KEEPS A DIRECTORY'S LISTING FOR THE LIFE OF THE PROCESS, and looks a `#` specifier up
350
746
  // in it with no second look — where a relative one that misses reads the directory again.
351
747
  // `abide dev` compiles in one process for its whole session, so a component created since
352
748
  // its directory was listed never resolved, and was read as uninvoked. The `imports` map is
@@ -392,29 +788,21 @@ function throughImports(specifier: string, from: string): string | undefined {
392
788
  return undefined
393
789
  }
394
790
 
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
- */
791
+ /** The rows of either route table, one per route, `column` appending the fields only one carries. */
401
792
  function tableOf(
402
793
  routes: AppRoute[],
403
- extension: string,
404
794
  column: (route: AppRoute) => string = () => '',
405
795
  ): string {
406
796
  const rows: string[] = []
407
797
  for (const route of routes) {
408
- const module = `./${route.source.replace(/\.abide$/, extension)}`
409
798
  // 35.1 — THE CHAIN IS PART OF THE ADDRESS, so it is in the table rather than resolved at
410
799
  // run time: which layouts wrap a page is a fact about the file tree, and the server and
411
800
  // the client read it off the same walk for the reason the addresses are (30.8).
412
801
  const wrapping = route.layouts.map(
413
- (layout) =>
414
- `() => import(${JSON.stringify(`./${layout.replace(/\.abide$/, extension)}`)})`,
802
+ (layout) => `() => import(${tableImport(layout)})`,
415
803
  )
416
804
  rows.push(
417
- ` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${JSON.stringify(module)}), layouts: [${wrapping.join(', ')}]${column(route)} },`,
805
+ ` { pattern: ${JSON.stringify(route.pattern)}, load: () => import(${tableImport(route.source)}), layouts: [${wrapping.join(', ')}]${column(route)} },`,
418
806
  )
419
807
  }
420
808
  return rows.join('\n')
@@ -427,23 +815,19 @@ function tableOf(
427
815
  * tsconfig includes its build output typechecks it — and a bare `host` is an implicit `any` that
428
816
  * fails `tsc` under the `strict` every abide app is written against.
429
817
  */
430
- function clientTable(
431
- routes: AppRoute[],
432
- transitions: boolean,
433
- shellHead: string,
434
- ): string {
818
+ function clientTable(routes: AppRoute[], transitions: boolean): string {
435
819
  return `// Written by \`abide build\`. The addresses this app answers, and what to load for each.
436
820
  import { listenForRoutes, startRouter } from '${ROUTING_SPECIFIER}'
437
821
 
438
822
  export const routes = [
439
- ${tableOf(routes, '.client.ts')}
823
+ ${tableOf(routes)}
440
824
  ]
441
825
 
442
826
  // The router this document gets, the page it arrived on, and the two places a reader navigates
443
827
  // from (30.7). Hydrating here rather than leaving it to the app is what makes the page the server
444
828
  // painted live without anybody wiring it.
445
829
  export function start(host: Element) {
446
- const router = startRouter(routes, host, ${transitions}, ${JSON.stringify(shellHead)})
830
+ const router = startRouter(routes, host, ${transitions})
447
831
  listenForRoutes(router)
448
832
  const ready = router.hydrate()
449
833
  return { router, ready }
@@ -452,108 +836,122 @@ export function start(host: Element) {
452
836
  }
453
837
 
454
838
  /**
455
- * The source of `server.ts`: the same addresses, loading the module that PAINTS each page.
839
+ * The source of `server.ts`: the same addresses, loading the module that PAINTS each page, the
840
+ * error pages, the modules a server block runs in, and every handler.
456
841
  *
457
842
  * No `start` here and nothing imported: the server holds the matching and the ranking already,
458
843
  * and a server table that carried either would be a second copy of both.
459
844
  */
460
- function serverTable(routes: AppRoute[], errors: AppRoute[]): string {
845
+ function serverTable(
846
+ routes: AppRoute[],
847
+ errors: AppRoute[],
848
+ servers: string[],
849
+ mounts: Mount[],
850
+ ): string {
851
+ let loaders = ''
852
+ for (const source of servers)
853
+ loaders += ` () => import(${tableImport(source)}),\n`
854
+ let handlers = ''
855
+ for (const mount of mounts) {
856
+ const fields = JSON.stringify(mount)
857
+ // The module the handler is declared in — the handler file, or the `.abide`'s server
858
+ // module, the route table's own.
859
+ handlers += ` { load: () => import(${tableImport(mount.source)}), ${fields.slice(1, -1)} },\n`
860
+ }
461
861
  return `// Written by \`abide build\`. The addresses this app answers, and what paints each.
462
862
  export const routes = [
463
- ${tableOf(routes, '.ts', clientModules)}
863
+ ${tableOf(routes, clientModules)}
464
864
  ]
465
865
 
466
866
  // 35.2 — the error pages, by the directory each answers for.
467
867
  export const errors = [
468
- ${tableOf(errors, '.ts')}
868
+ ${tableOf(errors)}
469
869
  ]
870
+
871
+ // 37.1 — every module a \`<script server>\` runs in, imported before the app boots.
872
+ export const servers = [
873
+ ${loaders}]
874
+
875
+ // 16.2 — every handler this app answers over http, and where each is.
876
+ export const handlers = [
877
+ ${handlers}]
470
878
  `
471
879
  }
472
880
 
473
881
  /**
474
882
  * The server row's `clientModules` column: what the browser will load at this address.
475
883
  *
476
- * 38.29 — named on the SERVER's row: the server writes the document, and the preloads for the page
884
+ * 38.28 — named on the SERVER's row: the server writes the document, and the preloads for the page
477
885
  * and its chain have to be in it before the client has run at all. Keys into the manifest
478
886
  * `manifestOf` reads off the bundle.
479
887
  */
480
888
  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
- `
889
+ return `, clientModules: ${JSON.stringify([route.source, ...route.layouts])}`
492
890
  }
493
891
 
494
892
  /**
495
- * The browser's stand-in module for each handler module, by source path.
893
+ * The wrappers the browser holds in each handler's place, by source.
496
894
  *
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.
895
+ * 16.28 — WHAT A BROWSER HOLDS WHERE THE APP WROTE A PRIVATE HANDLER: the same name, the same call
896
+ * over the wire, bound in its `.abide`'s server block's place. 16.74 — a handler file's is its own
897
+ * module, its default, answered in the file's place.
501
898
  */
502
- function wrappersOf(mounts: Mount[]): Map<string, string> {
503
- const modules = new Map<string, string>()
899
+ function standInsOf(mounts: Mount[]): Map<string, string> {
900
+ const lines = new Map<string, string[]>()
504
901
  for (const mount of mounts) {
505
- // 20.4 — withheld from the browser: no wrapper, so an import of it is 16.29's refusal.
902
+ // 20.4 — withheld from the browser: no wrapper, so a call of it there names nothing.
506
903
  if (!mount.ui) continue
507
- const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
508
- // 18.4 — what the handler's value declares that the browser's copy behaves by.
509
- // 11.29 — and the defaults its parameter writes, which the copy keys by.
510
- const copy =
511
- mount.defaults !== undefined
512
- ? `, ${JSON.stringify(mount.copy)}, ${JSON.stringify(mount.defaults)}`
513
- : mount.copy === undefined
514
- ? ''
515
- : `, ${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)
522
- }
523
- const wrappers = new Map<string, string>()
524
- for (const [source, body] of modules)
525
- wrappers.set(
526
- source,
527
- `// Written by \`abide build\`. What a browser holds in place of ${source} (16.28).
528
- import { wire } from '${WIRE_SPECIFIER}'
529
-
530
- ${body}`,
904
+ const held = lines.get(mount.source) ?? []
905
+ held.push(
906
+ wireLine(
907
+ mount,
908
+ mount.private ? `const ${mount.name} =` : 'export default',
909
+ ),
531
910
  )
532
- return wrappers
911
+ lines.set(mount.source, held)
912
+ }
913
+ const standIns = new Map<string, string>()
914
+ for (const [source, held] of lines) standIns.set(source, wireModule(held))
915
+ return standIns
916
+ }
917
+
918
+ /**
919
+ * One handler's wrapper, bound as `binding` writes it.
920
+ *
921
+ * The address is written RELATIVE to the bundle's own directory and resolved against the chunk at
922
+ * run time (16.10), so an app mounted under a sub-path calls its own handlers rather than the
923
+ * root's.
924
+ */
925
+ function wireLine(mount: Mount, binding: string): string {
926
+ const at = JSON.stringify(posix.relative(CLIENT_ADDRESS, mount.address))
927
+ // 18.4 — what the handler's value declares that the browser's copy behaves by.
928
+ // 11.29 — and the defaults its parameter writes, which the copy keys by.
929
+ const copy =
930
+ mount.defaults !== undefined
931
+ ? `, ${JSON.stringify(mount.copy)}, ${JSON.stringify(mount.defaults)}`
932
+ : mount.copy === undefined
933
+ ? ''
934
+ : `, ${JSON.stringify(mount.copy)}`
935
+ return `${binding} __wire(${JSON.stringify(mount.method)}, new URL(${at}, import.meta.url).pathname${copy})\n`
936
+ }
937
+
938
+ /** A module of wrappers: the import they share, then each line. */
939
+ function wireModule(lines: string[]): string {
940
+ return `// 16.28 — what a browser holds in place of a handler.
941
+ import { wire as __wire } from '${WIRE_SPECIFIER}'
942
+ ${lines.join('')}`
943
+ }
944
+
945
+ /** Whether a page or an error page has anything to paint: a template, or a head. 23.3, 35.14. */
946
+ export function paints(file: AbideFile): boolean {
947
+ return file.template.length > 0 || file.head.length > 0
533
948
  }
534
949
 
535
950
  // THE ENTRY. An app does not write it: which module boots the client is the build's knowledge,
536
951
  // exactly as the route table is, and a hand-written entry is one more file to keep in step with a
537
952
  // layout that moves.
538
953
  const ENTRY_MODULE = `// Written by \`abide build\`. What boots this app in a browser.
539
- import { start } from './routes.ts'
954
+ import { start } from '../${VIRTUAL_DIRECTORY}/${ROUTES_TABLE}'
540
955
 
541
956
  start(document.body)
542
957
  `
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
- }