@abide/abide 0.103.0 → 0.105.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 (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +27 -27
  4. package/docs/RULEBOOK.md +68 -24
  5. package/docs/abide.md +31 -46
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +46 -89
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -25
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +12 -10
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +183 -478
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/projectShadow.ts +1 -1
  25. package/src/compiler/readSources.ts +27 -42
  26. package/src/compiler/readsIn.ts +5 -16
  27. package/src/compiler/resolverIn.ts +77 -0
  28. package/src/server/abideModules.ts +152 -146
  29. package/src/server/answerRequest.ts +18 -4
  30. package/src/server/answerSocket.ts +136 -76
  31. package/src/server/answerValue.ts +28 -15
  32. package/src/server/appTree.ts +59 -73
  33. package/src/server/claudeApi.ts +2 -5
  34. package/src/server/clientBundle.ts +198 -0
  35. package/src/server/coerceArgs.ts +1 -1
  36. package/src/server/commands.ts +93 -57
  37. package/src/server/createApp.ts +90 -16
  38. package/src/server/deferredBuilds.ts +175 -0
  39. package/src/server/devWorker.ts +107 -319
  40. package/src/server/faceOf.ts +120 -0
  41. package/src/server/fenceServer.ts +10 -18
  42. package/src/server/generatedSurfaces.ts +12 -53
  43. package/src/server/index.ts +3 -0
  44. package/src/server/placement.ts +167 -0
  45. package/src/server/remote.ts +14 -42
  46. package/src/server/render.ts +25 -17
  47. package/src/server/renderComponent.ts +9 -5
  48. package/src/server/requestScope.ts +10 -4
  49. package/src/server/responseRelay.ts +389 -0
  50. package/src/server/runCompiled.ts +9 -12
  51. package/src/server/runDesktop.ts +6 -9
  52. package/src/server/serveBuild.ts +26 -73
  53. package/src/server/serveDev.ts +19 -176
  54. package/src/server/streamEvent.ts +29 -0
  55. package/src/server/versionOf.ts +1 -2
  56. package/src/server/withAbideSources.ts +2 -1
  57. package/src/shared/HandlerRoute.ts +68 -0
  58. package/src/shared/REFUSAL_KEY.ts +5 -0
  59. package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
  60. package/src/shared/answerOf.ts +105 -78
  61. package/src/shared/filledPath.ts +1 -9
  62. package/src/shared/framing.ts +127 -128
  63. package/src/shared/queryText.ts +15 -7
  64. package/src/shared/reactive/Cursor.ts +69 -124
  65. package/src/shared/reactive/Reactive.ts +296 -162
  66. package/src/shared/reactive/callLoad.ts +4 -18
  67. package/src/shared/reactive/index.ts +11 -8
  68. package/src/shared/reactive/pairReactive.ts +1 -1
  69. package/src/shared/reactive/scoped.ts +4 -3
  70. package/src/shared/reactive/share.ts +11 -1
  71. package/src/shared/routePattern.ts +0 -110
  72. package/src/shared/rpc.ts +17 -24
  73. package/src/shared/rpcRequest.ts +18 -47
  74. package/src/shared/takeEarlyCall.ts +12 -37
  75. package/src/shared/url.ts +1 -1
  76. package/src/ui/Level.ts +276 -0
  77. package/src/ui/List.ts +17 -17
  78. package/src/ui/Router.ts +20 -87
  79. package/src/ui/applyHead.ts +92 -79
  80. package/src/ui/index.ts +4 -2
  81. package/src/ui/reconcile.ts +4 -110
  82. package/src/ui/rows.ts +123 -2
  83. package/src/ui/wire.ts +12 -27
  84. package/src/compiler/hooksCalled.ts +0 -35
  85. package/src/server/clientBuildOptions.ts +0 -54
  86. package/src/server/refuseAddressCollision.ts +0 -37
  87. package/src/shared/reactive/chunksOfStream.ts +0 -57
  88. package/src/ui/MOUNTING_LEVEL.ts +0 -18
@@ -5,22 +5,23 @@
5
5
  // So listing a directory lists its routes.
6
6
  //
7
7
  // ONE WALK for the sources, the handler files, the server and lib modules, and the public files
8
- // `abide build` copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22) — and
9
- // ONE SKIP RULE, `skipped`, for every walk of the tree, beside `placeOf`, which answers the same
10
- // question of one path.
8
+ // `abide build` copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22) — each
9
+ // file what `placeOf` says of its path, so the walk and every reader of one path agree, and ONE SKIP
10
+ // RULE, `skipped`, for every walk of the tree.
11
11
  //
12
12
  // A WALK RATHER THAN A GLOB: a glob descends `node_modules` and the build output before a filter
13
13
  // can drop them.
14
14
 
15
15
  import { type Dirent, existsSync, readdirSync } from 'node:fs' // bun has no directory listing or sync existence check of its own
16
+ import type { Refusals } from '#compiler/Refusals.ts'
16
17
  import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
17
18
  import {
18
- LIB_DIRECTORY,
19
+ HOLDS_SERVER,
19
20
  PUBLIC_DIRECTORY,
20
21
  RESERVED_DIRECTORIES,
21
22
  SERVER_DIRECTORY,
22
23
  } from '#shared/RESERVED_DIRECTORIES.ts'
23
- import { DIRECTORY_FILES, routeOf, skipped } from '#shared/routePattern.ts'
24
+ import { DIRECTORY_FILES, placeOf, routeOf, skipped } from './placement.ts'
24
25
 
25
26
  /** What one walk of the app finds, every path relative to the root. */
26
27
  type AppFiles = {
@@ -28,29 +29,19 @@ type AppFiles = {
28
29
  abide: string[]
29
30
  /** Every handler file, in path order. */
30
31
  handlers: string[]
31
- /** Every module under a `server` or a `lib` directory, in path order — run as it is imported. */
32
+ /** Every module under a `server` or a `lib` directory, in path order — read for 37.13. */
32
33
  modules: string[]
33
34
  /** Every file under a `public` directory, to the address it is answered at below the mount. */
34
35
  published: Map<string, string>
35
36
  }
36
37
 
37
- /** Told of each thing the tree refuses, at the path relative to the root. */
38
- type Refuse = (path: string, message: string) => void
39
-
40
- const THROW: Refuse = (_path, message) => {
41
- throw new Error(message)
42
- }
43
-
44
38
  /**
45
39
  * The app's `.abide` sources, handler files, server and lib modules and public files, refusing a
46
- * tree 23.16, 23.17, 23.18 or 23.22 forbids.
47
- *
48
- * `refuse` throws by default, which is the build's answer; `abide check` collects instead (38.33),
49
- * and the checker's route table ignores them, since a refused tree is the build's to report.
40
+ * tree 23.16, 23.17, 23.18 or 23.22 forbids into `refusals`, each at its path below the root.
50
41
  *
51
42
  * A missing root is an app with nothing in it, not a fault.
52
43
  */
53
- export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
44
+ export function appFiles(root: string, refusals: Refusals): AppFiles {
54
45
  const files: AppFiles = {
55
46
  abide: [],
56
47
  handlers: [],
@@ -60,7 +51,7 @@ export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
60
51
  // 23.22, 38.25 — a root that is its package's directory holds the package's own files beside its
61
52
  // pages.
62
53
  const packaged = existsSync(`${root}/package.json`)
63
- walk(root, '', undefined, files, refuse, packaged)
54
+ walk(root, '', undefined, files, refusals, packaged)
64
55
  files.abide.sort()
65
56
  files.handlers.sort()
66
57
  files.modules.sort()
@@ -68,13 +59,14 @@ export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
68
59
  }
69
60
 
70
61
  // `reserved` is the nearest reserved directory above `relative` — its path and its name — or
71
- // `undefined` where none is.
62
+ // `undefined` where none is: what 23.16 refuses is the nesting of two, which is a directory's shape
63
+ // rather than a file's place.
72
64
  function walk(
73
65
  directory: string,
74
66
  relative: string,
75
67
  reserved: { path: string; name: string } | undefined,
76
68
  files: AppFiles,
77
- refuse: Refuse,
69
+ refusals: Refusals,
78
70
  packaged = false,
79
71
  ): void {
80
72
  let entries: Dirent[]
@@ -90,10 +82,19 @@ function walk(
90
82
  if (entry.isDirectory()) {
91
83
  let within = reserved
92
84
  if (RESERVED_DIRECTORIES.has(entry.name)) {
93
- if (reserved !== undefined)
94
- refuse(
85
+ // 23.16 — `server` nests in the fences that hold code.
86
+ if (
87
+ reserved !== undefined &&
88
+ !(
89
+ entry.name === SERVER_DIRECTORY &&
90
+ HOLDS_SERVER.has(reserved.name)
91
+ )
92
+ )
93
+ refusals.at(
95
94
  path,
96
- `abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
95
+ new Error(
96
+ `abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
97
+ ),
97
98
  )
98
99
  within = { path, name: entry.name }
99
100
  }
@@ -102,37 +103,43 @@ function walk(
102
103
  `${path}/`,
103
104
  within,
104
105
  files,
105
- refuse,
106
+ refusals,
106
107
  )
107
108
  continue
108
109
  }
109
110
  const abide = path.endsWith('.abide')
110
111
  if (abide) files.abide.push(path)
111
- if (reserved === undefined) {
112
+ const place = placeOf(path)
113
+ if (place === 'address') {
112
114
  if (!abide && !isModule(path)) {
113
115
  if (packaged && relative === '' && !path.endsWith('.ts'))
114
116
  continue
115
- refuse(
117
+ refusals.at(
116
118
  path,
117
- `abide: ${path} is directly in an address directory, which holds pages, layouts, error pages and handler files alone. Move it into a lib directory beside it. RULEBOOK 23.22.`,
119
+ new Error(
120
+ `abide: ${path} is directly in an address directory, which holds pages, layouts, error pages and handler files alone. Move it into a lib directory beside it. RULEBOOK 23.22.`,
121
+ ),
118
122
  )
119
123
  continue
120
124
  }
121
125
  if (!abide) files.handlers.push(path)
122
126
  // 23.17 — anything that could answer here would answer in abide's own namespace.
123
- refuseFrameworkRoute(routeOf(path), path, refuse)
124
- } else if (DIRECTORY_FILES.has(entry.name))
125
- refuse(
127
+ const framework = frameworkRouteRefusal(routeOf(path), path)
128
+ if (framework !== undefined) refusals.at(path, framework)
129
+ continue
130
+ }
131
+ // Any other place is a fence's, and the walk passed through it.
132
+ const fence = reserved as { path: string; name: string }
133
+ if (DIRECTORY_FILES.has(entry.name))
134
+ refusals.at(
126
135
  path,
127
- `abide: ${path} is under the ${reserved.name} directory ${reserved.path}, where nothing has an address, and ${entry.name} is a route's name. RULEBOOK 23.18.`,
136
+ new Error(
137
+ `abide: ${path} is under the ${fence.name} directory ${fence.path}, where nothing has an address, and ${entry.name} is a route's name. RULEBOOK 23.18.`,
138
+ ),
128
139
  )
129
- else if (reserved.name === PUBLIC_DIRECTORY)
130
- files.published.set(path, publicAddress(path, reserved.path))
131
- else if (
132
- (reserved.name === SERVER_DIRECTORY ||
133
- reserved.name === LIB_DIRECTORY) &&
134
- isModule(path)
135
- )
140
+ else if (place === 'public')
141
+ files.published.set(path, publicAddress(path, fence.path))
142
+ else if ((place === 'server' || place === 'lib') && isModule(path))
136
143
  files.modules.push(path)
137
144
  }
138
145
  }
@@ -149,17 +156,16 @@ function isModule(path: string): boolean {
149
156
  )
150
157
  }
151
158
 
152
- /** Refuses `source` where `route` lies in the framework's own namespace. 23.17. */
153
- export function refuseFrameworkRoute(
159
+ /** The refusal of `source` where `route` lies in the framework's own namespace, or `undefined`. 23.17. */
160
+ export function frameworkRouteRefusal(
154
161
  route: string,
155
162
  source: string,
156
- refuse: Refuse = THROW,
157
- ): void {
158
- if (route === ABIDE_ADDRESS || route.startsWith(`${ABIDE_ADDRESS}/`))
159
- refuse(
160
- source,
161
- `abide: ${source} answers at ${route}, which is under ${ABIDE_ADDRESS}, where abide's own addresses are. RULEBOOK 23.17.`,
162
- )
163
+ ): Error | undefined {
164
+ return route === ABIDE_ADDRESS || route.startsWith(`${ABIDE_ADDRESS}/`)
165
+ ? new Error(
166
+ `abide: ${source} answers at ${route}, which is under ${ABIDE_ADDRESS}, where abide's own addresses are. RULEBOOK 23.17.`,
167
+ )
168
+ : undefined
163
169
  }
164
170
 
165
171
  // 36.11 — the directories above `public`, then the file's own path below it.
@@ -183,19 +189,6 @@ export function publishedUnder(
183
189
  return paths
184
190
  }
185
191
 
186
- /**
187
- * Every `.abide` under `root`, relative to it, in path order — the app's own, and none a
188
- * dependency ships.
189
- *
190
- * Refuses nothing: the language server lists them on every check, and a tree 23.16 forbids is the
191
- * build's to refuse (`appFiles`).
192
- */
193
- export function abideUnder(root: string): string[] {
194
- const found: string[] = []
195
- for (const path of abideFilesUnder(root, '')) found.push(path)
196
- return found.sort()
197
- }
198
-
199
192
  /**
200
193
  * Whether an app lies under `root`: a `.abide` anywhere or a handler file, stopping at the first.
201
194
  *
@@ -203,15 +196,13 @@ export function abideUnder(root: string): string[] {
203
196
  * is as cheap as the tree allows.
204
197
  */
205
198
  export function holdsApp(root: string): boolean {
206
- return !abideFilesUnder(root, '', true).next().done
199
+ return !appSourcesUnder(root, '').next().done
207
200
  }
208
201
 
209
- // Every `.abide` under `directory` — and, where `handlers`, every handler file outside a fence.
210
- function* abideFilesUnder(
202
+ // Every `.abide` under `directory`, and every handler file — a module whose place is an address.
203
+ function* appSourcesUnder(
211
204
  directory: string,
212
205
  relative: string,
213
- handlers = false,
214
- fenced = false,
215
206
  ): Generator<string> {
216
207
  let entries: Dirent[]
217
208
  try {
@@ -223,15 +214,10 @@ function* abideFilesUnder(
223
214
  if (skipped(entry.name)) continue
224
215
  const path = `${relative}${entry.name}`
225
216
  if (entry.isDirectory())
226
- yield* abideFilesUnder(
227
- `${directory}/${entry.name}`,
228
- `${path}/`,
229
- handlers,
230
- fenced || RESERVED_DIRECTORIES.has(entry.name),
231
- )
217
+ yield* appSourcesUnder(`${directory}/${entry.name}`, `${path}/`)
232
218
  else if (
233
219
  path.endsWith('.abide') ||
234
- (handlers && !fenced && isModule(path))
220
+ (isModule(path) && placeOf(path) === 'address')
235
221
  )
236
222
  yield path
237
223
  }
@@ -6,7 +6,7 @@
6
6
  // Every block of an assistant turn goes back as it came — a `thinking` block with its signature
7
7
  // included, which a turn continuing after a tool call is refused without.
8
8
 
9
- import { framedStream, framingPrefix } from '#shared/framing.ts'
9
+ import { framedStream } from '#shared/framing.ts'
10
10
  import { refuse } from '#shared/refuse.ts'
11
11
  import type { AssistantEvent, Provider } from './assistant.ts'
12
12
 
@@ -112,10 +112,7 @@ export function claudeApi(config: ClaudeApiConfig): Provider {
112
112
  const partials: string[] = []
113
113
  const called: Block[] = []
114
114
  let stop = 'end_turn'
115
- for await (const chunk of framedStream(
116
- answered.body,
117
- framingPrefix('text/event-stream') as string,
118
- )) {
115
+ for await (const chunk of framedStream(answered.body, 'sse')) {
119
116
  const event = chunk as ApiEvent
120
117
  switch (event.type) {
121
118
  case 'message_start':
@@ -0,0 +1,198 @@
1
+ // 38.5, 38.16 — THE CLIENT BUNDLE, for both lanes that build one: `abide build`, which writes it to
2
+ // disk for `abide start`, and `abide dev`'s worker, which holds it in memory. What decides what the
3
+ // bundle IS — its plugins and their order, whether there is a page to boot, how a stylesheet's
4
+ // addresses are written, the manifest — is here once, so dev resolves an address the way the
5
+ // built app does by construction; a lane picks only a `mode`. And both answer it at
6
+ // `/__abide/client/<name>` through `bundleServer`, differing in where a byte is read from and how
7
+ // long a browser may keep it.
8
+
9
+ import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
10
+ import { abideModules, type Compilation, tablePath } from './abideModules.ts'
11
+ import { appPlugins } from './appPlugins.ts'
12
+ import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
13
+ import { fenceServer } from './fenceServer.ts'
14
+ import { type Manifest, manifestOf, NO_MANIFEST } from './manifestOf.ts'
15
+ import { type Encoding, negotiateEncoding } from './negotiateEncoding.ts'
16
+
17
+ /** One file the bundle produced: its bytes — a stylesheet's already rewritten — and its kind. */
18
+ type BundledFile = {
19
+ bytes: Uint8Array<ArrayBuffer>
20
+ type: string
21
+ kind: Bun.BuildArtifact['kind']
22
+ }
23
+
24
+ /** A client bundle: every file it produced, by its name below `/__abide/client`, and its manifest. */
25
+ export type ClientBundle = {
26
+ files: Map<string, BundledFile>
27
+ manifest: Manifest
28
+ }
29
+
30
+ /**
31
+ * The client bundle of the app at `root`, out of `compilation`, built for `mode`.
32
+ *
33
+ * `published` is each public file's source path to its address. `'build'` is 38.5's — minified and
34
+ * every name hashed; `'dev'` is 38.16's — unminified, the entry named by its source, with linked
35
+ * maps. A bundle that fails throws, its logs the error's.
36
+ *
37
+ * AN APP OF HANDLERS ALONE has no page to boot, and is bundled as nothing with no manifest.
38
+ */
39
+ export async function clientBundle(
40
+ root: string,
41
+ compilation: Compilation,
42
+ published: Map<string, string>,
43
+ mode: 'build' | 'dev',
44
+ ): Promise<ClientBundle> {
45
+ const bundle: ClientBundle = { files: new Map(), manifest: NO_MANIFEST }
46
+ // A page among what compiled: the compiler writes the entry only where there is one to boot.
47
+ if (compilation.tables[`${CLIENT_ENTRY}.ts`] === undefined) return bundle
48
+ const built = await Bun.build({
49
+ // The entry is a table the plugin serves (`abideModules`), below the root.
50
+ entrypoints: [tablePath(root, `${CLIENT_ENTRY}.ts`)],
51
+ // The app's own root, because `Bun.build` opens the entry's directory otherwise, and the
52
+ // tables' is not on disk to open.
53
+ root,
54
+ // 16.28, 16.29 — the browser's view of the server: a `<script server>` is its stand-ins and a
55
+ // `server/` module refuses — FENCED FIRST, before the plugin that would serve the module. 38.43
56
+ // — then the app's own, as `bunfig.toml` declares them.
57
+ plugins: [
58
+ fenceServer(root, compilation.mounted),
59
+ abideModules(root, compilation),
60
+ ...(await appPlugins(root)),
61
+ ],
62
+ target: 'browser',
63
+ format: 'esm',
64
+ // So a destination's markup and bindings stay in the chunk its own route loads (30.11)
65
+ // rather than landing in the entry every page pays for.
66
+ splitting: true,
67
+ metafile: true,
68
+ // 36.15 — a stylesheet's `url('/fonts/…')` is the public file's address, not a path here,
69
+ // and `Bun.build` reads a leading `/` as a path on this disk and fails on it not being
70
+ // there. The list is the files that exist rather than every `/…`: a URL naming nothing the
71
+ // app serves is still worth a build error.
72
+ external: [...published.values()],
73
+ throw: false,
74
+ ...(mode === 'build'
75
+ ? {
76
+ // 38.5 — MINIFIED, and EVERY NAME HASHED, the entry's included: chunks and assets are
77
+ // already `[name]-[hash]` by default and the entry is not, which left the one file
78
+ // every page loads served for a year under a name the next deploy writes different
79
+ // bytes to. The manifest is how the document learns what the entry was called.
80
+ //
81
+ // NAMES KEPT, because a class's name is what an error reads to say what it was
82
+ // handed (`canonicalKey`), and a production error naming `a e` is one nobody can
83
+ // act on. It is free where it is paid: the JS the docs app's `/values` sends is
84
+ // 25 600 bytes brotli with the names and without, against 31 900 unminified.
85
+ minify: {
86
+ whitespace: true,
87
+ syntax: true,
88
+ identifiers: true,
89
+ keepNames: true,
90
+ },
91
+ naming: { entry: '[name]-[hash].[ext]' },
92
+ }
93
+ : {
94
+ // 38.97 — a debugger in the browser reads the `.abide` (`withAbideSources`).
95
+ sourcemap: 'linked' as const,
96
+ // 38.16 — the entry named by its source, so what a browser loads and what a stack
97
+ // trace names are the module the author wrote. A dev chunk that changed has to be
98
+ // fetched again, and a name that changes with it is a cache the reload cannot clear.
99
+ naming: {
100
+ entry: '[name].[ext]',
101
+ chunk: '[name]-[hash].[ext]',
102
+ asset: '[name]-[hash].[ext]',
103
+ },
104
+ }),
105
+ })
106
+ // THROWN RATHER THAN LOGGED: a build that emitted server modules and no bundle is an app that
107
+ // serves every page dead — the markup right and nothing wired — which is the one failure a
108
+ // reader cannot see. Dev catches it (38.17); the build stops on it.
109
+ if (!built.success)
110
+ throw new AggregateError(
111
+ built.logs,
112
+ `abide ${mode}: the client bundle failed`,
113
+ )
114
+ // The outputs are independent, so they are read at once rather than one after another.
115
+ const read = await Promise.all(
116
+ built.outputs.map((output) => output.arrayBuffer()),
117
+ )
118
+ for (let index = 0; index < built.outputs.length; index += 1) {
119
+ const output = built.outputs[index] as Bun.BuildArtifact
120
+ const name = output.path.slice(output.path.lastIndexOf('/') + 1)
121
+ let bytes = new Uint8Array(read[index] as ArrayBuffer)
122
+ if (name.endsWith('.css'))
123
+ bytes = new TextEncoder().encode(
124
+ stylesheetUnderMount(new TextDecoder().decode(bytes)),
125
+ )
126
+ bundle.files.set(name, { bytes, type: output.type, kind: output.kind })
127
+ }
128
+ bundle.manifest = manifestOf(built.metafile as Bun.BuildMetafile, root)
129
+ return bundle
130
+ }
131
+
132
+ // From the bundle's own directory up to the app's root: `../../` below `/__abide/client`.
133
+ const UP_TO_ROOT = '../'.repeat(CLIENT_ADDRESS.split('/').length - 1)
134
+
135
+ /**
136
+ * A built stylesheet with every root-absolute `url()` written relative to the bundle's directory.
137
+ *
138
+ * 16.10, 36.15 — THE SAME MECHANISM AS THE HANDLER WRAPPERS: the address is written relative to
139
+ * where the bundle is served and the browser resolves it against the sheet, so one build serves
140
+ * at the root and below a mount. What survives the build as `url(/…)` is a public file's address
141
+ * (the `external` list above), and the browser would resolve it against the origin. `//` is
142
+ * another host's.
143
+ */
144
+ function stylesheetUnderMount(css: string): string {
145
+ return css.replace(
146
+ /url\((["']?)\/(?!\/)/g,
147
+ (_whole, quote: string) => `url(${quote}${UP_TO_ROOT}`,
148
+ )
149
+ }
150
+
151
+ /** A bundle file as it is answered: its type, its bytes, and each precompressed copy of them. */
152
+ export type ServedFile = {
153
+ type: string
154
+ body: () => BodyInit
155
+ encoded: Partial<Record<Encoding, () => BodyInit>>
156
+ }
157
+
158
+ /**
159
+ * Answers `/__abide/client/<name>` out of `files`, each with `cacheControl`; `undefined` for any
160
+ * other address, and for a name the bundle does not hold.
161
+ *
162
+ * THE BUNDLE, ENUMERATED RATHER THAN RESOLVED. A request names a file in this set or it names
163
+ * nothing — there is no path to normalize, so `..` is not a case that has to be got right, and what
164
+ * is servable is exactly what the bundle produced.
165
+ */
166
+ export function bundleServer(
167
+ files: Map<string, ServedFile>,
168
+ cacheControl: string,
169
+ ): (pathname: string, request: Request) => Response | undefined {
170
+ // Each file's encodings listed once, rather than on every request for it.
171
+ const offeredOf = new Map<string, Encoding[]>()
172
+ for (const [name, file] of files)
173
+ offeredOf.set(name, Object.keys(file.encoded) as Encoding[])
174
+ return (pathname, request) => {
175
+ if (!pathname.startsWith(`${CLIENT_ADDRESS}/`)) return undefined
176
+ const name = pathname.slice(CLIENT_ADDRESS.length + 1)
177
+ const file = files.get(name)
178
+ if (file === undefined) return undefined
179
+ const headers: Record<string, string> = {
180
+ 'cache-control': cacheControl,
181
+ 'content-type': file.type,
182
+ }
183
+ const offered = offeredOf.get(name) as Encoding[]
184
+ if (offered.length === 0) return new Response(file.body(), { headers })
185
+ headers.vary = 'accept-encoding'
186
+ // 38.29 — the precompressed copy where the request takes its encoding.
187
+ const encoding = negotiateEncoding(
188
+ request.headers.get('accept-encoding'),
189
+ offered,
190
+ )
191
+ const encoded =
192
+ encoding === undefined ? undefined : file.encoded[encoding]
193
+ if (encoding === undefined || encoded === undefined)
194
+ return new Response(file.body(), { headers })
195
+ headers['content-encoding'] = encoding
196
+ return new Response(encoded(), { headers })
197
+ }
198
+ }
@@ -7,8 +7,8 @@
7
7
  // has declared no types, and its args stay the text they arrived as.
8
8
 
9
9
  import { numberFromText } from '#shared/numberFromText.ts'
10
+ import { bareName } from '#shared/queryText.ts'
10
11
  import { type Gated, gateOf } from '#shared/reactive/argsContract.ts'
11
- import { bareName } from '#shared/rpcRequest.ts'
12
12
 
13
13
  export type Document = {
14
14
  type?: string | string[]