@abide/abide 0.101.0 → 0.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/abide.ts +226 -164
  3. package/docs/REGISTRY.md +730 -0
  4. package/docs/RULEBOOK.md +2768 -0
  5. package/docs/abide.md +13127 -0
  6. package/package.json +6 -3
  7. package/scaffold/AGENTS.md +38 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +341 -71
  13. package/src/compiler/appMap.ts +96 -0
  14. package/src/compiler/argsSchemaOf.ts +1 -1
  15. package/src/compiler/checkApp.ts +53 -8
  16. package/src/compiler/checkedShadow.ts +73 -6
  17. package/src/compiler/classifyRead.ts +6 -4
  18. package/src/compiler/classifyTemplate.ts +1 -1
  19. package/src/compiler/compileApp.ts +598 -200
  20. package/src/compiler/compiledEntry.ts +48 -40
  21. package/src/compiler/componentGraph.ts +57 -71
  22. package/src/compiler/declaredExports.ts +26 -9
  23. package/src/compiler/emitClient.ts +142 -72
  24. package/src/compiler/emitComponent.ts +41 -12
  25. package/src/compiler/emitSource.ts +102 -13
  26. package/src/compiler/handlersUnder.ts +164 -76
  27. package/src/compiler/hooksCalled.ts +35 -0
  28. package/src/compiler/index.ts +7 -0
  29. package/src/compiler/mountsOf.ts +150 -0
  30. package/src/compiler/nearest.ts +14 -0
  31. package/src/compiler/parseAbide.ts +157 -20
  32. package/src/compiler/projectShadow.ts +250 -19
  33. package/src/compiler/reactiveTypes.ts +20 -7
  34. package/src/compiler/readSources.ts +15 -2
  35. package/src/compiler/readsIn.ts +62 -8
  36. package/src/compiler/renderMarkup.ts +3 -3
  37. package/src/compiler/scaffold.ts +37 -15
  38. package/src/compiler/serveLanguage.ts +101 -34
  39. package/src/compiler/sharedModuleWrites.ts +2 -2
  40. package/src/compiler/sourceMapOf.ts +193 -0
  41. package/src/compiler/splitImports.ts +205 -83
  42. package/src/compiler/tokenize.ts +14 -0
  43. package/src/compiler/writeDefaultProject.ts +29 -2
  44. package/src/server/CLIENT_BUILD.ts +11 -13
  45. package/src/server/DEFAULT_PORT.ts +1 -1
  46. package/src/server/MODULE_QUERIES.ts +27 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +457 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +241 -107
  52. package/src/server/answerRpc.ts +107 -33
  53. package/src/server/answerSocket.ts +65 -20
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +238 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +38 -37
  65. package/src/server/componentModules.d.ts +2 -1
  66. package/src/server/compressAnswer.ts +4 -4
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +77 -79
  70. package/src/server/devWorker.ts +118 -181
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +57 -32
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +137 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +12 -7
  77. package/src/server/installSourceMaps.ts +93 -0
  78. package/src/server/launcherOptions.ts +33 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +6 -6
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/preload.ts +13 -0
  85. package/src/server/principal.ts +10 -5
  86. package/src/server/realOrAsGiven.ts +10 -0
  87. package/src/server/refuseAddressCollision.ts +21 -14
  88. package/src/server/remote.ts +49 -52
  89. package/src/server/render.ts +65 -94
  90. package/src/server/renderComponent.ts +5 -0
  91. package/src/server/reportHealth.ts +2 -2
  92. package/src/server/requestScope.ts +12 -1
  93. package/src/server/runBuild.ts +24 -0
  94. package/src/server/runCompiled.ts +5 -5
  95. package/src/server/runDesktop.ts +48 -28
  96. package/src/server/serveBuild.ts +43 -52
  97. package/src/server/serveDev.ts +79 -37
  98. package/src/server/shell.ts +7 -22
  99. package/src/server/sourceMappings.ts +139 -0
  100. package/src/server/surfaceOf.ts +20 -3
  101. package/src/server/testBuild.ts +1 -19
  102. package/src/server/withAbideSources.ts +117 -0
  103. package/src/shared/ADDRESSES.ts +10 -18
  104. package/src/shared/APP_META.ts +7 -0
  105. package/src/shared/CopyOptions.ts +1 -1
  106. package/src/shared/DEBUG_META.ts +1 -1
  107. package/src/shared/LAUNCHER_FILES.ts +9 -0
  108. package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
  109. package/src/shared/cookies.ts +2 -2
  110. package/src/shared/csp.ts +1 -1
  111. package/src/shared/escapeText.ts +1 -1
  112. package/src/shared/filledPath.ts +85 -0
  113. package/src/shared/framing.ts +79 -6
  114. package/src/shared/headKeyOf.ts +2 -0
  115. package/src/shared/index.ts +2 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/{ui → shared}/menuBridge.ts +2 -2
  119. package/src/shared/menuItem.ts +4 -4
  120. package/src/shared/onMenu.ts +58 -0
  121. package/src/shared/reactive/Keyed.ts +2 -2
  122. package/src/shared/reactive/NotLanded.ts +1 -1
  123. package/src/shared/reactive/Reactive.ts +43 -43
  124. package/src/shared/reactive/argsContract.ts +4 -4
  125. package/src/shared/reactive/canonicalKey.ts +4 -4
  126. package/src/shared/reactive/chunksOfStream.ts +6 -0
  127. package/src/shared/reactive/index.ts +1 -1
  128. package/src/shared/reactive/recordWrites.ts +103 -5
  129. package/src/shared/reactive/structural.ts +1 -1
  130. package/src/shared/reactive/tracking.ts +1 -1
  131. package/src/shared/reactive/watch.ts +3 -3
  132. package/src/shared/refuse.ts +1 -1
  133. package/src/{ui → shared}/reportMenu.ts +8 -10
  134. package/src/shared/routePattern.ts +122 -48
  135. package/src/shared/rpc.ts +37 -8
  136. package/src/shared/rpcRequest.ts +27 -3
  137. package/src/shared/url.ts +4 -44
  138. package/src/shared/writePath.ts +1 -1
  139. package/src/ui/LAYER_NAMES.ts +1 -1
  140. package/src/ui/List.ts +3 -3
  141. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  142. package/src/ui/Mismatch.ts +1 -1
  143. package/src/ui/RawRun.ts +1 -1
  144. package/src/ui/Router.ts +37 -36
  145. package/src/ui/Scope.ts +1 -1
  146. package/src/ui/TryRegion.ts +2 -2
  147. package/src/ui/applyHead.ts +33 -0
  148. package/src/ui/index.ts +1 -1
  149. package/src/ui/moveNode.ts +1 -1
  150. package/src/ui/reconcile.ts +2 -2
  151. package/src/ui/routing.ts +2 -2
  152. package/src/ui/setAttribute.ts +1 -1
  153. package/src/ui/setText.ts +1 -1
  154. package/src/ui/socketWire.ts +9 -7
  155. package/src/ui/wire.ts +1 -1
  156. package/scaffold/src/server/app.ts +0 -6
  157. package/scaffold/src/shared/.gitkeep +0 -0
  158. package/scaffold/src/ui/app.html +0 -8
  159. package/src/server/componentImports.ts +0 -56
  160. /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
  161. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
@@ -0,0 +1,457 @@
1
+ // 34.14, 34.16, 32.17 — EVERY `.abide` IS SERVED AT ITS OWN PATH, compiled, by one plugin the runtime
2
+ // and the bundler share. Served there, a compiled module's relative, `#` and bare specifiers resolve
3
+ // from the file that wrote them through bun's own resolver — from the app, from a dependency under
4
+ // `node_modules`, or from a linked package outside the root alike — so nothing about a file's
5
+ // compile depends on where the build would have put it.
6
+ //
7
+ // WHICH FACE AN IMPORT GETS IS SPELLED ON ITS SPECIFIER (`MODULE_QUERIES`), because bun hands a plugin
8
+ // no import attributes: a compiled module asks for `?component`, a browser module for its own
9
+ // `?style`, a text import for `?text`. A `.abide` named bare is a module abide did not compile
10
+ // asking — on a server that is the bound face 34.16 states, and in a browser the component module
11
+ // itself.
12
+ //
13
+ // THE TABLES — the route, error and handler tables, and in a browser the router's table and the
14
+ // entry — are the build's own modules, served from `__abide` below the root, where no app writes.
15
+ //
16
+ // THE RUNTIME'S REGISTRATION IS THE PROCESS'S: a `.abide` filter is answered by the first plugin
17
+ // that registered one, so a second app booted in the same process — a test's — adds its compilation
18
+ // to this one rather than registering a plugin nobody asks.
19
+
20
+ import { basename, dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
21
+ import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
22
+ import type { ServerTable } from './answerRequest.ts'
23
+ import type { MountRow } from './answerRpc.ts'
24
+ import { installSourceMaps, type SourceMaps } from './installSourceMaps.ts'
25
+ import {
26
+ CLIENT_QUERY,
27
+ COMPONENT_QUERY,
28
+ HANDLER_FACE,
29
+ HANDLER_SUFFIX,
30
+ STYLE_QUERY,
31
+ TEXT_QUERY,
32
+ } from './MODULE_QUERIES.ts'
33
+ import { realOrAsGiven } from './realOrAsGiven.ts'
34
+ import { SERVER_TABLE } from './TABLES.ts'
35
+ import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
36
+
37
+ /** One `.abide`, compiled: its server module, its browser module, its stylesheet and their maps. */
38
+ export type CompiledFile = {
39
+ server: string
40
+ /** `''` where the file has nothing to hydrate and nothing imports it by name. */
41
+ client: string
42
+ sheet: string | undefined
43
+ /** 38.97 — each module's `mappings` into the `.abide`, or `undefined` where it places nothing. */
44
+ serverMappings: string | undefined
45
+ clientMappings: string | undefined
46
+ }
47
+
48
+ /**
49
+ * What a compile hands the plugin, and what `abide build` keeps and `abide dev` sends its worker.
50
+ *
51
+ * Every path RELATIVE TO THE ROOT, so a build moved to another directory or another machine is
52
+ * served from wherever its root is then.
53
+ */
54
+ export type Compilation = {
55
+ /** Every `.abide` compiled, by its path below the root. */
56
+ files: Record<string, CompiledFile>
57
+ /**
58
+ * Every handler file, by its path below the root: the route its hooks are filed at (37.1), and
59
+ * the module a browser is handed in its place (16.74), `''` where the browser is handed none.
60
+ */
61
+ handlers: Record<string, HandlerFace>
62
+ /** The build's own modules, by name below `__abide`. */
63
+ tables: Record<string, string>
64
+ /**
65
+ * 23.19 — every directory a file mounts, by its path below the root, to the position its files
66
+ * take: what the browser's fence reads a mounted file's place by (16.29).
67
+ */
68
+ mounted: Record<string, string>
69
+ }
70
+
71
+ // A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
72
+ // by its path whatever its namespace, and the runtime's drops a query from it — so a file's text, its
73
+ // stylesheet or its bound face under `?…` took the place of the module the file compiles to.
74
+ const BOUND = '::bound'
75
+ const CLIENT = '::client'
76
+ const TEXT = '::text'
77
+ const STYLE = '::style'
78
+
79
+ /** The namespace a bundle's component module is answered in, at its file's own path. */
80
+ export const COMPONENT_NAMESPACE = 'abide-component'
81
+ /** The namespaces a file's text and a component's stylesheet are loaded in. */
82
+ export const TEXT_NAMESPACE = 'abide-text'
83
+ export const STYLE_NAMESPACE = 'abide-style'
84
+ const RELATIVE_OR_SUBPATH = /^(?:\.\.?\/|#)/
85
+
86
+ /** One handler file, as `Compilation.handlers` holds it. */
87
+ type HandlerFace = { scope: string; client: string }
88
+
89
+ /** The plugin serving `compilation`'s files and tables, for one bundle. */
90
+ export function abideModules(
91
+ root: string,
92
+ compilation: Compilation,
93
+ ): Bun.BunPlugin {
94
+ const held: Held = {
95
+ files: new Map(),
96
+ handlers: new Map(),
97
+ tables: new Map(),
98
+ }
99
+ addCompilation(realOrAsGiven(root), compilation, held)
100
+ return modulesPlugin(held)
101
+ }
102
+
103
+ /**
104
+ * A compilation as the plugin keys it, and answers from: a compiled file, a handler file or a table
105
+ * by absolute path.
106
+ */
107
+ type Held = {
108
+ files: Map<string, CompiledFile>
109
+ handlers: Map<string, HandlerFace>
110
+ tables: Map<string, string>
111
+ }
112
+
113
+ const RUNTIME: Held = {
114
+ files: new Map(),
115
+ handlers: new Map(),
116
+ tables: new Map(),
117
+ }
118
+ let registered = false
119
+
120
+ /**
121
+ * Serves `compilation` to this process's imports from now on, and places a stack frame in each of
122
+ * its files (38.97).
123
+ */
124
+ export function serveCompilation(root: string, compilation: Compilation): void {
125
+ const real = realOrAsGiven(root)
126
+ addCompilation(real, compilation, RUNTIME)
127
+ installSourceMaps(serverMapsOf(compilation, real), real)
128
+ if (registered) return
129
+ registered = true
130
+ Bun.plugin(modulesPlugin(RUNTIME))
131
+ }
132
+
133
+ /** What the server's table exports: its routes and error pages, server blocks and handlers. */
134
+ export type BuiltTable = ServerTable & {
135
+ /** 37.1 — every module a `<script server>` runs in, imported before the app boots. */
136
+ servers: (() => Promise<unknown>)[]
137
+ /** 16.2 — every handler, each row with its module's loader. */
138
+ handlers: MountRow[]
139
+ }
140
+
141
+ /** The server's table of the app at `root`, out of the compilation this process serves. */
142
+ export function servedTable(root: string): Promise<BuiltTable> {
143
+ return import(
144
+ Bun.pathToFileURL(tablePath(root, SERVER_TABLE)).href
145
+ ) as Promise<BuiltTable>
146
+ }
147
+
148
+ /** The path a table is imported at: `name` below `__abide` under `root`. */
149
+ export function tablePath(root: string, name: string): string {
150
+ return `${realOrAsGiven(root)}/${VIRTUAL_DIRECTORY}/${name}`
151
+ }
152
+
153
+ /** Each server module's map, by the `.abide` it is served at, below `root`. */
154
+ export function serverMapsOf(
155
+ compilation: Compilation,
156
+ root: string,
157
+ ): SourceMaps {
158
+ const maps: SourceMaps = {}
159
+ for (const [relative, compiled] of Object.entries(compilation.files))
160
+ if (compiled.serverMappings !== undefined)
161
+ maps[relative] = [resolve(root, relative), compiled.serverMappings]
162
+ return maps
163
+ }
164
+
165
+ function addCompilation(
166
+ root: string,
167
+ compilation: Compilation,
168
+ held: Held,
169
+ ): void {
170
+ // RESOLVED, not joined: a dependency's file is keyed above the root (`../node_modules/…`).
171
+ for (const [relative, compiled] of Object.entries(compilation.files))
172
+ held.files.set(resolve(root, relative), compiled)
173
+ for (const [relative, face] of Object.entries(compilation.handlers))
174
+ held.handlers.set(resolve(root, relative), face)
175
+ for (const [name, text] of Object.entries(compilation.tables))
176
+ held.tables.set(`${root}/${VIRTUAL_DIRECTORY}/${name}`, text)
177
+ }
178
+
179
+ // `Bun.resolveSync` asks EVERY abide plugin's `onResolve` again — this one's, and the runtime's, which
180
+ // is registered for the whole process and so answers inside a bundle too. One flag for all of them
181
+ // is what lets the inner ask fall through to bun's resolver rather than be answered again from a
182
+ // directory read as a file.
183
+ let resolving = false
184
+
185
+ /**
186
+ * Where `specifier` resolves from the directory `from`, as bun's own resolver answers — never as a
187
+ * face this plugin serves.
188
+ *
189
+ * The compiler asks where a `.abide` IS; in a process serving a compilation, the plugin would
190
+ * answer with the face an import of it gets instead.
191
+ */
192
+ export function resolvedFile(specifier: string, from: string): string {
193
+ resolving = true
194
+ try {
195
+ return Bun.resolveSync(specifier, from)
196
+ } finally {
197
+ resolving = false
198
+ }
199
+ }
200
+
201
+ function modulesPlugin(served: Held): Bun.BunPlugin {
202
+ return {
203
+ name: 'abide-modules',
204
+ setup(build) {
205
+ // THE LANE IS THE BUILD'S TARGET: a browser bundle gets each file's browser module, and
206
+ // the runtime — which has no config — and a server bundle get its server module. A
207
+ // browser module the runtime was asked for (`CLIENT_QUERY`) is the one exception, and
208
+ // what it imports is in its lane.
209
+ const bundling: boolean = 'config' in build
210
+ const browser = bundling && build.config.target === 'browser'
211
+ const inBrowser = (importer: string): boolean =>
212
+ browser || pathOf(importer).endsWith(CLIENT)
213
+ // A BUNDLE'S COMPONENT MODULE IS IN A NAMESPACE OF ITS OWN: the bundler refuses a
214
+ // `file` path a plugin answers with `..` anywhere in it, which a `[...rest]` directory
215
+ // writes (23.5). The runtime has no such check, and its stack frames name the path.
216
+ const componentNamespace = bundling ? COMPONENT_NAMESPACE : 'file'
217
+ // The module `path` compiles to, in the lane its importer is in.
218
+ const componentFor = (path: string, importer: string) =>
219
+ inBrowser(importer) && !browser
220
+ ? { path: `${path}${CLIENT}`, namespace: 'abide-client' }
221
+ : { path, namespace: componentNamespace }
222
+ const resolved = (specifier: string, importer: string): string =>
223
+ resolvedFile(specifier, dirname(pathOf(importer)))
224
+ // A filter off the query's own constant, so renaming one cannot silently stop a match.
225
+ const abideWith = (query: string) =>
226
+ new RegExp(`\\.abide${RegExp.escape(query)}$`)
227
+
228
+ build.onResolve(
229
+ { filter: new RegExp(`${RegExp.escape(TEXT_QUERY)}$`) },
230
+ (args) => {
231
+ if (resolving) return undefined
232
+ const specifier = args.path.slice(0, -TEXT_QUERY.length)
233
+ // A path of its own (`TEXT`): a file is often imported as code and as text at once
234
+ // — an example's Files tab.
235
+ return {
236
+ path: `${resolved(specifier, args.importer)}${TEXT}`,
237
+ namespace: TEXT_NAMESPACE,
238
+ }
239
+ },
240
+ )
241
+ build.onResolve({ filter: abideWith(STYLE_QUERY) }, (args) => {
242
+ if (resolving) return undefined
243
+ const specifier = args.path.slice(0, -STYLE_QUERY.length)
244
+ return {
245
+ path: `${resolved(specifier, args.importer)}${STYLE}`,
246
+ namespace: STYLE_NAMESPACE,
247
+ }
248
+ })
249
+ build.onResolve({ filter: abideWith(COMPONENT_QUERY) }, (args) => {
250
+ if (resolving) return undefined
251
+ const specifier = args.path.slice(0, -COMPONENT_QUERY.length)
252
+ return componentFor(
253
+ resolved(specifier, args.importer),
254
+ args.importer,
255
+ )
256
+ })
257
+ build.onResolve({ filter: abideWith(CLIENT_QUERY) }, (args) => {
258
+ if (resolving) return undefined
259
+ const path = resolved(
260
+ args.path.slice(0, -CLIENT_QUERY.length),
261
+ args.importer,
262
+ )
263
+ return browser
264
+ ? { path, namespace: componentNamespace }
265
+ : { path: `${path}${CLIENT}`, namespace: 'abide-client' }
266
+ })
267
+ build.onResolve({ filter: /\.abide$/ }, (args) => {
268
+ if (resolving) return undefined
269
+ const path = resolved(args.path, args.importer)
270
+ // A bound face's own import of its component, and every browser import, is the
271
+ // component module.
272
+ if (
273
+ inBrowser(args.importer) ||
274
+ pathOf(args.importer) === `${path}${BOUND}`
275
+ )
276
+ return componentFor(path, args.importer)
277
+ return { path: `${path}${BOUND}`, namespace: 'abide-bound' }
278
+ })
279
+ build.onResolve(
280
+ { filter: new RegExp(`/${VIRTUAL_DIRECTORY}/[^/]+\\.ts$`) },
281
+ (args) => {
282
+ if (resolving) return undefined
283
+ const path = resolve(
284
+ dirname(pathOf(args.importer)),
285
+ args.path,
286
+ )
287
+ return served.tables.get(path) === undefined
288
+ ? undefined
289
+ : { path, namespace: 'abide-table' }
290
+ },
291
+ )
292
+
293
+ // A HANDLER FILE IS FOUND BY WHERE ITS IMPORT RESOLVES, however the import spells it, and
294
+ // only from the app's own modules: a package's never names one, and asking bun again for
295
+ // each of a package's relative imports is a resolve per module of the bundle. IN A BUNDLE
296
+ // THE FILTER IS THE HANDLER FILES' NAMES: a hook matching every relative import costs the
297
+ // bundle abide's own `sideEffects`, and the barrel's rest — the validator, the csp — moved
298
+ // into the chunk every page loads (38.94). The runtime is told of each compilation after
299
+ // it registered, so it matches every relative or `#` import and asks.
300
+ // Each handler file's name without `.ts` — what an import of one ends in.
301
+ let stems: string[] | undefined
302
+ if (bundling) {
303
+ const names = new Set<string>()
304
+ for (const path of served.handlers.keys())
305
+ names.add(basename(path, '.ts'))
306
+ stems = [...names]
307
+ }
308
+ const handlerImport =
309
+ stems === undefined
310
+ ? RELATIVE_OR_SUBPATH
311
+ : new RegExp(
312
+ `(?:^|/)(?:${stems.map(RegExp.escape).join('|')})(?:\\.ts)?$`,
313
+ )
314
+ if (stems?.length !== 0)
315
+ build.onResolve({ filter: handlerImport }, (args) => {
316
+ if (resolving || !RELATIVE_OR_SUBPATH.test(args.path))
317
+ return undefined
318
+ const importer = pathOf(args.importer)
319
+ if (importer.includes('/node_modules/')) return undefined
320
+ let path: string
321
+ try {
322
+ path = resolved(args.path, args.importer)
323
+ } catch {
324
+ return undefined
325
+ }
326
+ const face = served.handlers.get(path)
327
+ if (face === undefined) return undefined
328
+ // 20.4 — a handler the browser is handed no wrapper for is no import of its.
329
+ if (inBrowser(args.importer) && face.client === '')
330
+ throw new Error(
331
+ `abide: ${pathOf(args.importer)} imports ${args.path}, whose handler withholds itself from the browser (\`clients: { ui: false }\`). Call it from a <script server>, or let the browser have it. RULEBOOK 20.4.`,
332
+ )
333
+ return { path: `${path}${HANDLER_SUFFIX}` }
334
+ })
335
+
336
+ // A RELATIVE SPECIFIER A FACE IMPORTS AT RUN TIME is resolved from the file the face
337
+ // stands for: bun's runtime reads one from an importer in a namespace as no path at
338
+ // all. A bare one it resolves itself. Registered after every face, so a `.abide` is
339
+ // still answered as one — and NOT IN A BUNDLE, which needs it for no face it serves,
340
+ // and where a hook matching every relative import in a package costs the bundle that
341
+ // package's `sideEffects`: a two-page app grew by 12 KB, the validator one page never
342
+ // calls moved into the chunk both pages load.
343
+ if (!bundling)
344
+ build.onResolve({ filter: /^\.\.?\// }, (args) =>
345
+ resolving || pathOf(args.importer) === args.importer
346
+ ? undefined
347
+ : { path: resolved(args.path, args.importer) },
348
+ )
349
+
350
+ build.onLoad(
351
+ { filter: /\.abide$/, namespace: componentNamespace },
352
+ (args) => {
353
+ const compiled = compiledAt(served, args.path)
354
+ return {
355
+ contents: browser ? compiled.client : compiled.server,
356
+ loader: 'ts',
357
+ }
358
+ },
359
+ )
360
+ // 16.74, 37.1 — A HANDLER FILE: the browser's is its wrapper, and the server's is the file
361
+ // as written, between the two calls that file its hooks at its route — on its first
362
+ // line, so every line of the file stays where its frames name it. ITS FACE is its path
363
+ // with a suffix, in bun's own namespace — so what it imports, relative, `#` or bare,
364
+ // resolves from its directory as the file's own imports do. A namespace of abide's own
365
+ // had bun resolve a bare one from the working directory: a second abide, run from `/`.
366
+ build.onLoad({ filter: HANDLER_FACE }, async (args) => {
367
+ const path = args.path.slice(0, -HANDLER_SUFFIX.length)
368
+ const face = served.handlers.get(path) as HandlerFace
369
+ if (browser) return { contents: face.client, loader: 'js' }
370
+ const text = await Bun.file(path).text()
371
+ return {
372
+ contents: `import { fileRoute as __fileRoute } from '${SERVER_SPECIFIER}';__fileRoute(${JSON.stringify(face.scope)});${text}\n;__fileRoute(undefined)\n`,
373
+ loader: 'ts',
374
+ }
375
+ })
376
+ build.onLoad(
377
+ { filter: /.*/, namespace: 'abide-client' },
378
+ (args) => ({
379
+ contents: compiledAt(
380
+ served,
381
+ args.path.slice(0, -CLIENT.length),
382
+ ).client,
383
+ loader: 'ts',
384
+ }),
385
+ )
386
+ build.onLoad({ filter: /.*/, namespace: 'abide-bound' }, (args) => {
387
+ const path = args.path.slice(0, -BOUND.length)
388
+ return {
389
+ contents: boundModule(path),
390
+ loader: 'js',
391
+ }
392
+ })
393
+ // A stylesheet is the bundle's; outside one there is no document to apply it to.
394
+ build.onLoad(
395
+ { filter: /.*/, namespace: STYLE_NAMESPACE },
396
+ (args) =>
397
+ bundling
398
+ ? {
399
+ contents:
400
+ compiledAt(
401
+ served,
402
+ args.path.slice(0, -STYLE.length),
403
+ ).sheet ?? '',
404
+ loader: 'css',
405
+ }
406
+ : { contents: '', loader: 'js' },
407
+ )
408
+ build.onLoad(
409
+ { filter: /.*/, namespace: TEXT_NAMESPACE },
410
+ async (args) => ({
411
+ contents: `export default ${JSON.stringify(await Bun.file(args.path.slice(0, -TEXT.length)).text())}\n`,
412
+ loader: 'js',
413
+ }),
414
+ )
415
+ build.onLoad(
416
+ { filter: /.*/, namespace: 'abide-table' },
417
+ (args) => ({
418
+ contents: served.tables.get(args.path) ?? '',
419
+ loader: 'ts',
420
+ }),
421
+ )
422
+ },
423
+ }
424
+ }
425
+
426
+ // AN IMPORTER IN ONE OF THIS PLUGIN'S NAMESPACES CARRIES IT in front of its path at run time — with
427
+ // a `/` in front of that again, where the bundler hands the path alone.
428
+ function pathOf(importer: string): string {
429
+ return importer.replace(/^\/?abide-[a-z]+:/, '')
430
+ }
431
+
432
+ // THE PATH BUN NAMES A FILE BY HAS ITS LINKS RESOLVED, and so has the root a compilation is served
433
+ // under (`serveCompilation`).
434
+ function compiledAt(served: Held, path: string): CompiledFile {
435
+ const compiled = served.files.get(path)
436
+ if (compiled === undefined)
437
+ throw new Error(
438
+ `abide: ${path} is imported, and no compile of this app reached it.`,
439
+ )
440
+ return compiled
441
+ }
442
+
443
+ // 34.16 — BOUND, NOT RAW. The compiled module is a function of accessors answering its markup (33.4);
444
+ // what an app's own module calls is a function of plain props answering a `Component` (35.4), which
445
+ // `render` invokes with nothing. The component is the module's one export (34.17).
446
+ function boundModule(path: string): string {
447
+ return `import component from ${JSON.stringify(path)}
448
+ export default (props = {}) => {
449
+ const accessors = {}
450
+ for (const key in props) {
451
+ const value = props[key]
452
+ accessors[key] = () => value
453
+ }
454
+ return () => component(accessors)
455
+ }
456
+ `
457
+ }
@@ -6,10 +6,11 @@
6
6
  import { type Failed, problemOf } from '#shared/Failed.ts'
7
7
  import { RENDER_LOG } from '#shared/log.ts'
8
8
  import { coversPrefix, partsOf } from '#shared/matchRoute.ts'
9
+ import { coversRoute, parseRoutePattern } from '#shared/routePattern.ts'
9
10
  import type { AppSources } from './answerRequest.ts'
10
11
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
11
- import { boundPage, render } from './render.ts'
12
- import { beginRender } from './requestScope.ts'
12
+ import { boundPage, renderDocument } from './render.ts'
13
+ import { beginRender, currentRequestScope } from './requestScope.ts'
13
14
 
14
15
  /** What a page's failure is painted from: the error pages, and the document they go into. */
15
16
  export type ErrorPages = Pick<AppSources, 'errors' | 'errorDocument'>
@@ -47,13 +48,22 @@ export function answered(failure: Failed<string, unknown>): Response {
47
48
  /**
48
49
  * The error pages for `pathname`, nearest first.
49
50
  *
50
- * 35.2 — every one whose directory is a prefix of the address, each segment meaning what it means
51
- * in a route.
51
+ * 35.2 — AS A LAYOUT IS: over the route the request matched, every one whose route covers it, as
52
+ * a layout's and an `onError`'s does (35.1, 37.5). An address no route matched has only its path,
53
+ * and every one whose directory is a prefix of it answers, each segment meaning what it means in a
54
+ * route.
52
55
  */
53
56
  function errorsFor(errors: RankedError[], pathname: string): RankedError[] {
57
+ const route = currentRequestScope()?.route
58
+ const found: RankedError[] = []
59
+ if (route !== undefined) {
60
+ const segments = parseRoutePattern(route)
61
+ for (const error of errors)
62
+ if (coversRoute(error.segments, segments)) found.push(error)
63
+ return found
64
+ }
54
65
  // 15.16's address has no parts to be nearest to, so the root's page paints it.
55
66
  const parts = partsOf(pathname) ?? []
56
- const found: RankedError[] = []
57
67
  for (const error of errors)
58
68
  if (coversPrefix(error.segments, parts)) found.push(error)
59
69
  return found
@@ -77,7 +87,7 @@ export async function answerFailure(
77
87
  try {
78
88
  const bound = boundPage(error, { failure: () => failure })
79
89
  beginRender(undefined)
80
- const document = await render(bound, app.errorDocument)
90
+ const document = await renderDocument(bound, app.errorDocument)
81
91
  return new Response(document, { status, headers })
82
92
  } catch (thrown) {
83
93
  RENDER_LOG.warning(