@abide/abide 0.101.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 (157) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/abide.ts +223 -164
  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 +5 -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 -59
  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 +102 -30
  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 +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 +4 -4
  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 +114 -181
  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 +73 -37
  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/RESERVED_DIRECTORIES.ts +15 -0
  107. package/src/shared/cookies.ts +2 -2
  108. package/src/shared/csp.ts +1 -1
  109. package/src/shared/escapeText.ts +1 -1
  110. package/src/shared/filledPath.ts +85 -0
  111. package/src/shared/framing.ts +79 -6
  112. package/src/shared/headKeyOf.ts +2 -0
  113. package/src/shared/index.ts +1 -1
  114. package/src/shared/log.ts +27 -13
  115. package/src/shared/matchRoute.ts +23 -4
  116. package/src/shared/menuItem.ts +3 -3
  117. package/src/shared/reactive/Keyed.ts +2 -2
  118. package/src/shared/reactive/NotLanded.ts +1 -1
  119. package/src/shared/reactive/Reactive.ts +43 -43
  120. package/src/shared/reactive/argsContract.ts +4 -4
  121. package/src/shared/reactive/canonicalKey.ts +4 -4
  122. package/src/shared/reactive/chunksOfStream.ts +6 -0
  123. package/src/shared/reactive/index.ts +1 -1
  124. package/src/shared/reactive/recordWrites.ts +103 -5
  125. package/src/shared/reactive/structural.ts +1 -1
  126. package/src/shared/reactive/tracking.ts +1 -1
  127. package/src/shared/reactive/watch.ts +3 -3
  128. package/src/shared/refuse.ts +1 -1
  129. package/src/shared/routePattern.ts +82 -46
  130. package/src/shared/rpc.ts +37 -8
  131. package/src/shared/rpcRequest.ts +27 -3
  132. package/src/shared/url.ts +4 -44
  133. package/src/shared/writePath.ts +1 -1
  134. package/src/ui/LAYER_NAMES.ts +1 -1
  135. package/src/ui/List.ts +3 -3
  136. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  137. package/src/ui/Mismatch.ts +1 -1
  138. package/src/ui/RawRun.ts +1 -1
  139. package/src/ui/Router.ts +38 -31
  140. package/src/ui/Scope.ts +1 -1
  141. package/src/ui/TryRegion.ts +2 -2
  142. package/src/ui/applyHead.ts +33 -0
  143. package/src/ui/index.ts +1 -1
  144. package/src/ui/menuBridge.ts +1 -1
  145. package/src/ui/moveNode.ts +1 -1
  146. package/src/ui/reconcile.ts +2 -2
  147. package/src/ui/reportMenu.ts +2 -2
  148. package/src/ui/routing.ts +2 -2
  149. package/src/ui/setAttribute.ts +1 -1
  150. package/src/ui/setText.ts +1 -1
  151. package/src/ui/socketWire.ts +9 -7
  152. package/src/ui/wire.ts +1 -1
  153. package/scaffold/src/server/app.ts +0 -6
  154. package/scaffold/src/shared/.gitkeep +0 -0
  155. package/scaffold/src/ui/app.html +0 -8
  156. package/src/server/componentImports.ts +0 -56
  157. /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
@@ -0,0 +1,82 @@
1
+ // 34.24 — WHAT A `.abide` EXPORTS, AS A PLAIN `.ts` IMPORTING IT IS TYPED. TypeScript reads no
2
+ // `.abide`, and a wildcard declaration can name a default alone, so the names a `<script module>` or
3
+ // a `<script server>` exports were untyped to every `.ts` outside abide's own checker. Each file's
4
+ // projection — its module and server blocks, exactly what that checker answers a `.abide` import
5
+ // with (`TypeSession.standIn`) — is written to `.abide/types`, mirroring the tree, where the app's
6
+ // `tsconfig` reaches it: `rootDirs` for a relative import, `paths` for `#src/*`.
7
+ //
8
+ // THE CHECKER IS NOT ASKED, because the API it answers over emits no declarations, and the
9
+ // projection is a module TypeScript infers the same types from. `@ts-nocheck` because the blocks are
10
+ // read as written — a reactive read by name is the checker's to answer (31.3) and `abide check`'s to
11
+ // report, and reported here too it would be one finding twice, at a path nobody wrote.
12
+
13
+ import { rmSync } from 'node:fs' // bun has no file removal of its own
14
+ import { dirname, relative } from 'node:path' // bun exposes no path helpers of its own
15
+ import { buildDirectory } from '#server/buildDirectory.ts'
16
+ import { namesUnder } from '#server/namesUnder.ts'
17
+ import { COMPONENT_MODULE } from './checkedShadow.ts'
18
+ import { formatDiagnosis } from './formatDiagnosis.ts'
19
+ import { projectShadow } from './projectShadow.ts'
20
+ import { type ParsedSource, refusal } from './readSources.ts'
21
+ import type { Diagnosis } from './TypeSession.ts'
22
+
23
+ /**
24
+ * Writes each source's types beside the build, and removes the ones whose `.abide` is gone.
25
+ *
26
+ * A file is written only where its text moved, so an editor watching the directory re-reads what
27
+ * changed rather than everything. WITH `refused`, A SOURCE WHOSE PROJECTION IS REFUSED (34.3, …) is a
28
+ * finding at its first line and keeps the types it last had, as `parseSources` treats one that does
29
+ * not parse; without it the refusal is thrown, naming the file.
30
+ */
31
+ export async function writeTypes(
32
+ root: string,
33
+ sources: readonly Pick<ParsedSource, 'path' | 'file'>[],
34
+ /** Whether `sources` is every file, so the types of one not among them are stale. */
35
+ whole = true,
36
+ refused?: Diagnosis[],
37
+ ): Promise<void> {
38
+ const above = dirname(root)
39
+ // Mirrored below the directory above `root`, as the scaffold's `tsconfig` names it.
40
+ const types = `${buildDirectory(root)}/types`
41
+ const written = new Set<string>()
42
+ const writes: Promise<unknown>[] = []
43
+ for (const source of sources) {
44
+ const path = `${types}/${relative(above, source.path)}.ts`
45
+ written.add(path)
46
+ let projected: string
47
+ try {
48
+ projected = projectShadow(source.file).text
49
+ } catch (thrown) {
50
+ const found = refusal(source.path, thrown)
51
+ if (refused === undefined)
52
+ throw new Error(formatDiagnosis(found, process.cwd()))
53
+ refused.push(found)
54
+ continue
55
+ }
56
+ // The checker's own stand-in for the default: a component taking anything (`COMPONENT_MODULE`).
57
+ const text = `// @ts-nocheck\n${projected}\n${COMPONENT_MODULE}`
58
+ if (WRITTEN.get(path) === text) continue
59
+ writes.push(
60
+ Bun.file(path)
61
+ .text()
62
+ .catch(() => undefined)
63
+ .then((standing) => {
64
+ WRITTEN.set(path, text)
65
+ return standing === text ? undefined : Bun.write(path, text)
66
+ }),
67
+ )
68
+ }
69
+ await Promise.all(writes)
70
+ if (!whole) return
71
+ // Only below this root's own mirror: two apps beside one another share the directory above.
72
+ const mirror = `${types}/${relative(above, root)}`
73
+ for (const name of namesUnder(mirror))
74
+ if (!written.has(`${mirror}/${name}`)) {
75
+ rmSync(`${mirror}/${name}`)
76
+ WRITTEN.delete(`${mirror}/${name}`)
77
+ }
78
+ }
79
+
80
+ // What this process last left at each path, so a compile run again (`abide dev`, the language
81
+ // server) reads back only the files whose text moved since.
82
+ const WRITTEN = new Map<string, string>()
@@ -1,6 +1,6 @@
1
1
  // WHERE THE BROWSER BUNDLE LANDS, named once for the commands that have to agree about it:
2
2
  // `abide build` writes it, `abide start` answers it, and `abide dev` builds it into memory
3
- // (38.17, 38.21). A leaf, with no imports of its own, for the reason every constant crossing a
3
+ // (38.16, 38.20). A leaf, with no imports of its own, for the reason every constant crossing a
4
4
  // seam is one — the build is in `bin/` and the server is in `src/server/`, and a string spelled out
5
5
  // in both is a build that silently serves nothing. The addresses it is answered at are
6
6
  // `#shared/ADDRESSES.ts`.
@@ -24,10 +24,16 @@ export const CLIENT_ENTRY = 'entry'
24
24
  *
25
25
  * 38.5 — the manifest `manifestOf` reads off the bundle, below the build's own output directory
26
26
  * and BESIDE the client directory rather than in it: everything in there is answered under
27
- * `CLIENT_ADDRESS` (38.21), and the manifest is the server's, not the browser's.
27
+ * `CLIENT_ADDRESS` (38.20), and the manifest is the server's, not the browser's.
28
28
  */
29
29
  export const MANIFEST = 'manifest.json'
30
30
 
31
+ /**
32
+ * The compilation `abide build` keeps beside the client directory: every `.abide` compiled, and
33
+ * the build's own tables (`Compilation`), which `abide start` serves without the compiler.
34
+ */
35
+ export const COMPILATION = 'compilation.json'
36
+
31
37
  /**
32
38
  * The encodings a response can be compressed in, each with its precompressed file's suffix.
33
39
  *
@@ -39,18 +45,10 @@ export const MANIFEST = 'manifest.json'
39
45
  export const ENCODING_SUFFIX = { br: '.br', gzip: '.gz' } as const
40
46
 
41
47
  /**
42
- * Where `abide build` copies `src/ui/public`, below its own output directory.
48
+ * Where `abide build` copies every `public` directory, each file at its address, below its own
49
+ * output directory. The source's own name for one is `#shared/RESERVED_DIRECTORIES.ts`'s.
43
50
  *
44
- * 38.19 — a directory rather than the source tree: `abide start` boots what the build produced and
51
+ * 38.18 — a directory rather than the source tree: `abide start` boots what the build produced and
45
52
  * reaches for nothing an app might have moved since.
46
53
  */
47
54
  export const PUBLIC_DIRECTORY = 'public'
48
-
49
- /**
50
- * Where the build puts the module a browser gets in place of each handler module.
51
- *
52
- * 16.28 — below the build's own output: the source's path, with this in front. One lookup for the
53
- * client bundle to make (`fenceServer`), and a place a reader can open to see exactly what the
54
- * browser was handed.
55
- */
56
- export const WIRE_DIRECTORY = 'wire'
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The port an app listens on where neither `--port` nor `PORT` names one: `Bun.serve`'s own default,
3
- * so `abide start`'s, the first `abide dev` tries (38.64), and the one a command reaching the local
3
+ * so `abide start`'s, the first `abide dev` tries (38.63), and the one a command reaching the local
4
4
  * app calls.
5
5
  */
6
6
  export const DEFAULT_PORT = 3000
@@ -0,0 +1,9 @@
1
+ // THE DESKTOP LAUNCHER'S FILES. Its page and the `launcher` options it exports sit at the app's root
2
+ // beside the root layout, a name the framework finds rather than an address (23.14); the icon is a
3
+ // public file, so it is wherever the address it answers at says (36.11).
4
+
5
+ /** The launcher's page and options. 38.80, 38.82. */
6
+ export const LAUNCHER_PAGE = 'launcher.abide'
7
+
8
+ /** The addresses of the desktop app's icon, the `.icns` first. 38.79. */
9
+ export const ICON_ADDRESSES = { icns: '/icon.icns', png: '/icon.png' } as const
@@ -0,0 +1,18 @@
1
+ // WHAT AN IMPORT OF A `.abide` ASKS FOR, spelled on the specifier: bun hands a plugin no import
2
+ // attributes, so the compiler writes the face it wants where the plugin can read it
3
+ // (`abideModules`). A `.abide` named with none is the face a module abide did not compile gets.
4
+
5
+ /** The module the file compiles to, as a compiled module imports it. 32.17. */
6
+ export const COMPONENT_QUERY = '?component'
7
+
8
+ /** The file's scoped stylesheet, which its browser module imports. 34.4. */
9
+ export const STYLE_QUERY = '?style'
10
+
11
+ /** The file's text. 34.25. */
12
+ export const TEXT_QUERY = '?text'
13
+
14
+ /**
15
+ * The file's browser module, asked for outside a browser bundle — a component's hydration tested
16
+ * under a DOM in `bun test`. Its own `.abide` imports are browser modules too.
17
+ */
18
+ export const CLIENT_QUERY = '?client'
@@ -0,0 +1,7 @@
1
+ // The build's own modules below `__abide` (`abideModules`), beside the client entry (`CLIENT_ENTRY`).
2
+
3
+ /** The server's tables: routes, error pages, server blocks and handlers. */
4
+ export const SERVER_TABLE = 'server.ts'
5
+
6
+ /** The client router's table, and the `start` the entry calls. */
7
+ export const ROUTES_TABLE = 'routes.ts'
@@ -1,6 +1,5 @@
1
- // Where `abide dev`'s compiled modules pretend to live, below the app's root: the compile points a
2
- // relative import back at its source from here (34.14), and the worker serves the modules under it.
3
- // Under the app's own root so that `#ui/…` and a bare package name resolve from here exactly as they
4
- // resolve from the build tree — both sit inside the app's package — and under a `__abide` directory
5
- // for the reason the addresses are: no app writes one.
1
+ // Where the build's own modules — the server's tables, the router's and the client entry — are served
2
+ // from, below the app's root (`abideModules`). Under the root so a bare specifier in them resolves as
3
+ // one in the app does, and under a `__abide` directory for the reason the addresses are: no app writes
4
+ // one.
6
5
  export const VIRTUAL_DIRECTORY = '__abide'
@@ -0,0 +1,391 @@
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, beside every name the module exports,
11
+ // and in a browser the component module 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 { dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
21
+ import type { ServerTable } from './answerRequest.ts'
22
+ import type { MountRow } from './answerRpc.ts'
23
+ import { installSourceMaps, type SourceMaps } from './installSourceMaps.ts'
24
+ import {
25
+ CLIENT_QUERY,
26
+ COMPONENT_QUERY,
27
+ STYLE_QUERY,
28
+ TEXT_QUERY,
29
+ } from './MODULE_QUERIES.ts'
30
+ import { realOrAsGiven } from './realOrAsGiven.ts'
31
+ import { SERVER_TABLE } from './TABLES.ts'
32
+ import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
33
+
34
+ /** One `.abide`, compiled: its server module, its browser module, its stylesheet and their maps. */
35
+ export type CompiledFile = {
36
+ server: string
37
+ /** `''` where the file has nothing to hydrate and nothing imports it by name. */
38
+ client: string
39
+ sheet: string | undefined
40
+ /** 38.97 — each module's `mappings` into the `.abide`, or `undefined` where it places nothing. */
41
+ serverMappings: string | undefined
42
+ clientMappings: string | undefined
43
+ }
44
+
45
+ /**
46
+ * What a compile hands the plugin, and what `abide build` keeps and `abide dev` sends its worker.
47
+ *
48
+ * Every path RELATIVE TO THE ROOT, so a build moved to another directory or another machine is
49
+ * served from wherever its root is then.
50
+ */
51
+ export type Compilation = {
52
+ /** Every `.abide` compiled, by its path below the root. */
53
+ files: Record<string, CompiledFile>
54
+ /** The build's own modules, by name below `__abide`. */
55
+ tables: Record<string, string>
56
+ }
57
+
58
+ // A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
59
+ // by its path whatever its namespace, and the runtime's drops a query from it — so a file's text, its
60
+ // stylesheet or its bound face under `?…` took the place of the module the file compiles to.
61
+ const BOUND = '::bound'
62
+ const CLIENT = '::client'
63
+ const TEXT = '::text'
64
+ const STYLE = '::style'
65
+
66
+ /** The namespace a bundle's component module is answered in, at its file's own path. */
67
+ export const COMPONENT_NAMESPACE = 'abide-component'
68
+ /** The namespaces a file's text and a component's stylesheet are loaded in. */
69
+ export const TEXT_NAMESPACE = 'abide-text'
70
+ export const STYLE_NAMESPACE = 'abide-style'
71
+
72
+ /** What the plugin answers from: a compiled file or a table by absolute path. */
73
+ type Served = {
74
+ file: (path: string) => CompiledFile | undefined
75
+ table: (path: string) => string | undefined
76
+ }
77
+
78
+ /** The plugin serving `compilation`'s files and tables, for one bundle. */
79
+ export function abideModules(
80
+ root: string,
81
+ compilation: Compilation,
82
+ ): Bun.BunPlugin {
83
+ const files = new Map<string, CompiledFile>()
84
+ const tables = new Map<string, string>()
85
+ addCompilation(realOrAsGiven(root), compilation, files, tables)
86
+ return modulesPlugin({
87
+ file: (path) => files.get(path),
88
+ table: (path) => tables.get(path),
89
+ })
90
+ }
91
+
92
+ const RUNTIME_FILES = new Map<string, CompiledFile>()
93
+ const RUNTIME_TABLES = new Map<string, string>()
94
+ let registered = false
95
+
96
+ /**
97
+ * Serves `compilation` to this process's imports from now on, and places a stack frame in each of
98
+ * its files (38.97).
99
+ */
100
+ export function serveCompilation(root: string, compilation: Compilation): void {
101
+ const real = realOrAsGiven(root)
102
+ addCompilation(real, compilation, RUNTIME_FILES, RUNTIME_TABLES)
103
+ installSourceMaps(serverMapsOf(compilation, real), real)
104
+ if (registered) return
105
+ registered = true
106
+ Bun.plugin(
107
+ modulesPlugin({
108
+ file: (path) => RUNTIME_FILES.get(path),
109
+ table: (path) => RUNTIME_TABLES.get(path),
110
+ }),
111
+ )
112
+ }
113
+
114
+ /** What the server's table exports: its routes and error pages, server blocks and handlers. */
115
+ export type BuiltTable = ServerTable & {
116
+ /** 37.1 — every module a `<script server>` runs in, imported before the app boots. */
117
+ servers: (() => Promise<unknown>)[]
118
+ /** 16.2 — every handler, each row with its module's loader. */
119
+ handlers: MountRow[]
120
+ }
121
+
122
+ /** The server's table of the app at `root`, out of the compilation this process serves. */
123
+ export function servedTable(root: string): Promise<BuiltTable> {
124
+ return import(
125
+ Bun.pathToFileURL(tablePath(root, SERVER_TABLE)).href
126
+ ) as Promise<BuiltTable>
127
+ }
128
+
129
+ /** The path a table is imported at: `name` below `__abide` under `root`. */
130
+ export function tablePath(root: string, name: string): string {
131
+ return `${realOrAsGiven(root)}/${VIRTUAL_DIRECTORY}/${name}`
132
+ }
133
+
134
+ /** Each server module's map, by the `.abide` it is served at, below `root`. */
135
+ export function serverMapsOf(
136
+ compilation: Compilation,
137
+ root: string,
138
+ ): SourceMaps {
139
+ const maps: SourceMaps = {}
140
+ for (const [relative, compiled] of Object.entries(compilation.files))
141
+ if (compiled.serverMappings !== undefined)
142
+ maps[relative] = [resolve(root, relative), compiled.serverMappings]
143
+ return maps
144
+ }
145
+
146
+ function addCompilation(
147
+ root: string,
148
+ compilation: Compilation,
149
+ files: Map<string, CompiledFile>,
150
+ tables: Map<string, string>,
151
+ ): void {
152
+ // RESOLVED, not joined: a dependency's file is keyed above the root (`../node_modules/…`).
153
+ for (const [relative, compiled] of Object.entries(compilation.files))
154
+ files.set(resolve(root, relative), compiled)
155
+ for (const [name, text] of Object.entries(compilation.tables))
156
+ tables.set(`${root}/${VIRTUAL_DIRECTORY}/${name}`, text)
157
+ }
158
+
159
+ // `Bun.resolveSync` asks EVERY abide plugin's `onResolve` again — this one's, and the runtime's, which
160
+ // is registered for the whole process and so answers inside a bundle too. One flag for all of them
161
+ // is what lets the inner ask fall through to bun's resolver rather than be answered again from a
162
+ // directory read as a file.
163
+ let resolving = false
164
+
165
+ /**
166
+ * Where `specifier` resolves from the directory `from`, as bun's own resolver answers — never as a
167
+ * face this plugin serves.
168
+ *
169
+ * The compiler asks where a `.abide` IS; in a process serving a compilation, the plugin would
170
+ * answer with the face an import of it gets instead.
171
+ */
172
+ export function resolvedFile(specifier: string, from: string): string {
173
+ resolving = true
174
+ try {
175
+ return Bun.resolveSync(specifier, from)
176
+ } finally {
177
+ resolving = false
178
+ }
179
+ }
180
+
181
+ function modulesPlugin(served: Served): Bun.BunPlugin {
182
+ return {
183
+ name: 'abide-modules',
184
+ setup(build) {
185
+ // THE LANE IS THE BUILD'S TARGET: a browser bundle gets each file's browser module, and
186
+ // the runtime — which has no config — and a server bundle get its server module. A
187
+ // browser module the runtime was asked for (`CLIENT_QUERY`) is the one exception, and
188
+ // what it imports is in its lane.
189
+ const bundling: boolean = 'config' in build
190
+ const browser = bundling && build.config.target === 'browser'
191
+ const inBrowser = (importer: string): boolean =>
192
+ browser || pathOf(importer).endsWith(CLIENT)
193
+ // A BUNDLE'S COMPONENT MODULE IS IN A NAMESPACE OF ITS OWN: the bundler refuses a
194
+ // `file` path a plugin answers with `..` anywhere in it, which a `[...rest]` directory
195
+ // writes (23.5). The runtime has no such check, and its stack frames name the path.
196
+ const componentNamespace = bundling ? COMPONENT_NAMESPACE : 'file'
197
+ // The module `path` compiles to, in the lane its importer is in.
198
+ const componentFor = (path: string, importer: string) =>
199
+ inBrowser(importer) && !browser
200
+ ? { path: `${path}${CLIENT}`, namespace: 'abide-client' }
201
+ : { path, namespace: componentNamespace }
202
+ const resolved = (specifier: string, importer: string): string =>
203
+ resolvedFile(specifier, dirname(pathOf(importer)))
204
+ // A filter off the query's own constant, so renaming one cannot silently stop a match.
205
+ const abideWith = (query: string) =>
206
+ new RegExp(`\\.abide${RegExp.escape(query)}$`)
207
+
208
+ build.onResolve(
209
+ { filter: new RegExp(`${RegExp.escape(TEXT_QUERY)}$`) },
210
+ (args) => {
211
+ if (resolving) return undefined
212
+ const specifier = args.path.slice(0, -TEXT_QUERY.length)
213
+ // A path of its own (`TEXT`): a file is often imported as code and as text at once
214
+ // — an example's Files tab.
215
+ return {
216
+ path: `${resolved(specifier, args.importer)}${TEXT}`,
217
+ namespace: TEXT_NAMESPACE,
218
+ }
219
+ },
220
+ )
221
+ build.onResolve({ filter: abideWith(STYLE_QUERY) }, (args) => {
222
+ if (resolving) return undefined
223
+ const specifier = args.path.slice(0, -STYLE_QUERY.length)
224
+ return {
225
+ path: `${resolved(specifier, args.importer)}${STYLE}`,
226
+ namespace: STYLE_NAMESPACE,
227
+ }
228
+ })
229
+ build.onResolve({ filter: abideWith(COMPONENT_QUERY) }, (args) => {
230
+ if (resolving) return undefined
231
+ const specifier = args.path.slice(0, -COMPONENT_QUERY.length)
232
+ return componentFor(
233
+ resolved(specifier, args.importer),
234
+ args.importer,
235
+ )
236
+ })
237
+ build.onResolve({ filter: abideWith(CLIENT_QUERY) }, (args) => {
238
+ if (resolving) return undefined
239
+ const path = resolved(
240
+ args.path.slice(0, -CLIENT_QUERY.length),
241
+ args.importer,
242
+ )
243
+ return browser
244
+ ? { path, namespace: componentNamespace }
245
+ : { path: `${path}${CLIENT}`, namespace: 'abide-client' }
246
+ })
247
+ build.onResolve({ filter: /\.abide$/ }, (args) => {
248
+ if (resolving) return undefined
249
+ const path = resolved(args.path, args.importer)
250
+ // A bound face's own import of its component, and every browser import, is the
251
+ // component module.
252
+ if (
253
+ inBrowser(args.importer) ||
254
+ pathOf(args.importer) === `${path}${BOUND}`
255
+ )
256
+ return componentFor(path, args.importer)
257
+ return { path: `${path}${BOUND}`, namespace: 'abide-bound' }
258
+ })
259
+ build.onResolve(
260
+ { filter: new RegExp(`/${VIRTUAL_DIRECTORY}/[^/]+\\.ts$`) },
261
+ (args) => {
262
+ if (resolving) return undefined
263
+ const path = resolve(
264
+ dirname(pathOf(args.importer)),
265
+ args.path,
266
+ )
267
+ return served.table(path) === undefined
268
+ ? undefined
269
+ : { path, namespace: 'abide-table' }
270
+ },
271
+ )
272
+
273
+ // A RELATIVE SPECIFIER A FACE IMPORTS AT RUN TIME is resolved from the file the face
274
+ // stands for: bun's runtime reads one from an importer in a namespace as no path at
275
+ // all. A bare one it resolves itself. Registered after every face, so a `.abide` is
276
+ // still answered as one — and NOT IN A BUNDLE, which needs it for no face it serves,
277
+ // and where a hook matching every relative import in a package costs the bundle that
278
+ // package's `sideEffects`: a two-page app grew by 12 KB, the validator one page never
279
+ // calls moved into the chunk both pages load.
280
+ if (!bundling)
281
+ build.onResolve({ filter: /^\.\.?\// }, (args) =>
282
+ resolving || pathOf(args.importer) === args.importer
283
+ ? undefined
284
+ : { path: resolved(args.path, args.importer) },
285
+ )
286
+
287
+ build.onLoad(
288
+ { filter: /\.abide$/, namespace: componentNamespace },
289
+ (args) => {
290
+ const compiled = compiledAt(served, args.path)
291
+ return {
292
+ contents: browser ? compiled.client : compiled.server,
293
+ loader: 'ts',
294
+ }
295
+ },
296
+ )
297
+ build.onLoad(
298
+ { filter: /.*/, namespace: 'abide-client' },
299
+ (args) => ({
300
+ contents: compiledAt(
301
+ served,
302
+ args.path.slice(0, -CLIENT.length),
303
+ ).client,
304
+ loader: 'ts',
305
+ }),
306
+ )
307
+ build.onLoad({ filter: /.*/, namespace: 'abide-bound' }, (args) => {
308
+ const path = args.path.slice(0, -BOUND.length)
309
+ return {
310
+ contents: boundModule(
311
+ path,
312
+ compiledAt(served, path).server,
313
+ ),
314
+ loader: 'js',
315
+ }
316
+ })
317
+ // A stylesheet is the bundle's; outside one there is no document to apply it to.
318
+ build.onLoad(
319
+ { filter: /.*/, namespace: STYLE_NAMESPACE },
320
+ (args) =>
321
+ bundling
322
+ ? {
323
+ contents:
324
+ compiledAt(
325
+ served,
326
+ args.path.slice(0, -STYLE.length),
327
+ ).sheet ?? '',
328
+ loader: 'css',
329
+ }
330
+ : { contents: '', loader: 'js' },
331
+ )
332
+ build.onLoad(
333
+ { filter: /.*/, namespace: TEXT_NAMESPACE },
334
+ async (args) => ({
335
+ contents: `export default ${JSON.stringify(await Bun.file(args.path.slice(0, -TEXT.length)).text())}\n`,
336
+ loader: 'js',
337
+ }),
338
+ )
339
+ build.onLoad(
340
+ { filter: /.*/, namespace: 'abide-table' },
341
+ (args) => ({
342
+ contents: served.table(args.path) ?? '',
343
+ loader: 'ts',
344
+ }),
345
+ )
346
+ },
347
+ }
348
+ }
349
+
350
+ // AN IMPORTER IN ONE OF THIS PLUGIN'S NAMESPACES CARRIES IT in front of its path at run time — with
351
+ // a `/` in front of that again, where the bundler hands the path alone.
352
+ function pathOf(importer: string): string {
353
+ return importer.replace(/^\/?abide-[a-z]+:/, '')
354
+ }
355
+
356
+ // THE PATH BUN NAMES A FILE BY HAS ITS LINKS RESOLVED, and so has the root a compilation is served
357
+ // under (`serveCompilation`).
358
+ function compiledAt(served: Served, path: string): CompiledFile {
359
+ const compiled = served.file(path)
360
+ if (compiled === undefined)
361
+ throw new Error(
362
+ `abide: ${path} is imported, and no compile of this app reached it.`,
363
+ )
364
+ return compiled
365
+ }
366
+
367
+ const SCANNER = new Bun.Transpiler({ loader: 'ts' })
368
+
369
+ // 34.16 — BOUND, NOT RAW. The compiled module is a function of accessors answering its markup (33.4);
370
+ // what an app's own module calls is a function of plain props answering a `Component` (35.4), which
371
+ // `render` invokes with nothing. Every name the module exports rides beside it: a `<script module>`'s,
372
+ // and the handlers a `<script server>` declares (34.23).
373
+ //
374
+ // NAMED RATHER THAN `export *`: the server blocks load side by side at boot, and a star export
375
+ // through a module still loading found none of its names.
376
+ function boundModule(path: string, server: string): string {
377
+ const specifier = JSON.stringify(path)
378
+ const names: string[] = []
379
+ for (const name of SCANNER.scan(server).exports)
380
+ if (name !== 'default') names.push(name)
381
+ return `import component from ${specifier}
382
+ ${names.length === 0 ? '' : `export { ${names.join(', ')} } from ${specifier}\n`}export default (props = {}) => {
383
+ const accessors = {}
384
+ for (const key in props) {
385
+ const value = props[key]
386
+ accessors[key] = () => value
387
+ }
388
+ return () => component(accessors)
389
+ }
390
+ `
391
+ }
@@ -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(