@abide/abide 0.101.0 → 0.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/abide.ts +226 -164
  3. package/docs/REGISTRY.md +730 -0
  4. package/docs/RULEBOOK.md +2768 -0
  5. package/docs/abide.md +13127 -0
  6. package/package.json +6 -3
  7. package/scaffold/AGENTS.md +38 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +341 -71
  13. package/src/compiler/appMap.ts +96 -0
  14. package/src/compiler/argsSchemaOf.ts +1 -1
  15. package/src/compiler/checkApp.ts +53 -8
  16. package/src/compiler/checkedShadow.ts +73 -6
  17. package/src/compiler/classifyRead.ts +6 -4
  18. package/src/compiler/classifyTemplate.ts +1 -1
  19. package/src/compiler/compileApp.ts +598 -200
  20. package/src/compiler/compiledEntry.ts +48 -40
  21. package/src/compiler/componentGraph.ts +57 -71
  22. package/src/compiler/declaredExports.ts +26 -9
  23. package/src/compiler/emitClient.ts +142 -72
  24. package/src/compiler/emitComponent.ts +41 -12
  25. package/src/compiler/emitSource.ts +102 -13
  26. package/src/compiler/handlersUnder.ts +164 -76
  27. package/src/compiler/hooksCalled.ts +35 -0
  28. package/src/compiler/index.ts +7 -0
  29. package/src/compiler/mountsOf.ts +150 -0
  30. package/src/compiler/nearest.ts +14 -0
  31. package/src/compiler/parseAbide.ts +157 -20
  32. package/src/compiler/projectShadow.ts +250 -19
  33. package/src/compiler/reactiveTypes.ts +20 -7
  34. package/src/compiler/readSources.ts +15 -2
  35. package/src/compiler/readsIn.ts +62 -8
  36. package/src/compiler/renderMarkup.ts +3 -3
  37. package/src/compiler/scaffold.ts +37 -15
  38. package/src/compiler/serveLanguage.ts +101 -34
  39. package/src/compiler/sharedModuleWrites.ts +2 -2
  40. package/src/compiler/sourceMapOf.ts +193 -0
  41. package/src/compiler/splitImports.ts +205 -83
  42. package/src/compiler/tokenize.ts +14 -0
  43. package/src/compiler/writeDefaultProject.ts +29 -2
  44. package/src/server/CLIENT_BUILD.ts +11 -13
  45. package/src/server/DEFAULT_PORT.ts +1 -1
  46. package/src/server/MODULE_QUERIES.ts +27 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +457 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +241 -107
  52. package/src/server/answerRpc.ts +107 -33
  53. package/src/server/answerSocket.ts +65 -20
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +238 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +38 -37
  65. package/src/server/componentModules.d.ts +2 -1
  66. package/src/server/compressAnswer.ts +4 -4
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +77 -79
  70. package/src/server/devWorker.ts +118 -181
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +57 -32
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +137 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +12 -7
  77. package/src/server/installSourceMaps.ts +93 -0
  78. package/src/server/launcherOptions.ts +33 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +6 -6
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/preload.ts +13 -0
  85. package/src/server/principal.ts +10 -5
  86. package/src/server/realOrAsGiven.ts +10 -0
  87. package/src/server/refuseAddressCollision.ts +21 -14
  88. package/src/server/remote.ts +49 -52
  89. package/src/server/render.ts +65 -94
  90. package/src/server/renderComponent.ts +5 -0
  91. package/src/server/reportHealth.ts +2 -2
  92. package/src/server/requestScope.ts +12 -1
  93. package/src/server/runBuild.ts +24 -0
  94. package/src/server/runCompiled.ts +5 -5
  95. package/src/server/runDesktop.ts +48 -28
  96. package/src/server/serveBuild.ts +43 -52
  97. package/src/server/serveDev.ts +79 -37
  98. package/src/server/shell.ts +7 -22
  99. package/src/server/sourceMappings.ts +139 -0
  100. package/src/server/surfaceOf.ts +20 -3
  101. package/src/server/testBuild.ts +1 -19
  102. package/src/server/withAbideSources.ts +117 -0
  103. package/src/shared/ADDRESSES.ts +10 -18
  104. package/src/shared/APP_META.ts +7 -0
  105. package/src/shared/CopyOptions.ts +1 -1
  106. package/src/shared/DEBUG_META.ts +1 -1
  107. package/src/shared/LAUNCHER_FILES.ts +9 -0
  108. package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
  109. package/src/shared/cookies.ts +2 -2
  110. package/src/shared/csp.ts +1 -1
  111. package/src/shared/escapeText.ts +1 -1
  112. package/src/shared/filledPath.ts +85 -0
  113. package/src/shared/framing.ts +79 -6
  114. package/src/shared/headKeyOf.ts +2 -0
  115. package/src/shared/index.ts +2 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/{ui → shared}/menuBridge.ts +2 -2
  119. package/src/shared/menuItem.ts +4 -4
  120. package/src/shared/onMenu.ts +58 -0
  121. package/src/shared/reactive/Keyed.ts +2 -2
  122. package/src/shared/reactive/NotLanded.ts +1 -1
  123. package/src/shared/reactive/Reactive.ts +43 -43
  124. package/src/shared/reactive/argsContract.ts +4 -4
  125. package/src/shared/reactive/canonicalKey.ts +4 -4
  126. package/src/shared/reactive/chunksOfStream.ts +6 -0
  127. package/src/shared/reactive/index.ts +1 -1
  128. package/src/shared/reactive/recordWrites.ts +103 -5
  129. package/src/shared/reactive/structural.ts +1 -1
  130. package/src/shared/reactive/tracking.ts +1 -1
  131. package/src/shared/reactive/watch.ts +3 -3
  132. package/src/shared/refuse.ts +1 -1
  133. package/src/{ui → shared}/reportMenu.ts +8 -10
  134. package/src/shared/routePattern.ts +122 -48
  135. package/src/shared/rpc.ts +37 -8
  136. package/src/shared/rpcRequest.ts +27 -3
  137. package/src/shared/url.ts +4 -44
  138. package/src/shared/writePath.ts +1 -1
  139. package/src/ui/LAYER_NAMES.ts +1 -1
  140. package/src/ui/List.ts +3 -3
  141. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  142. package/src/ui/Mismatch.ts +1 -1
  143. package/src/ui/RawRun.ts +1 -1
  144. package/src/ui/Router.ts +37 -36
  145. package/src/ui/Scope.ts +1 -1
  146. package/src/ui/TryRegion.ts +2 -2
  147. package/src/ui/applyHead.ts +33 -0
  148. package/src/ui/index.ts +1 -1
  149. package/src/ui/moveNode.ts +1 -1
  150. package/src/ui/reconcile.ts +2 -2
  151. package/src/ui/routing.ts +2 -2
  152. package/src/ui/setAttribute.ts +1 -1
  153. package/src/ui/setText.ts +1 -1
  154. package/src/ui/socketWire.ts +9 -7
  155. package/src/ui/wire.ts +1 -1
  156. package/scaffold/src/server/app.ts +0 -6
  157. package/scaffold/src/shared/.gitkeep +0 -0
  158. package/scaffold/src/ui/app.html +0 -8
  159. package/src/server/componentImports.ts +0 -56
  160. /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
  161. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
@@ -1,4 +1,4 @@
1
- // 38.15 — THE APP, IN A CHILD PROCESS. Everything an app's own code touches is in here: the modules
1
+ // 38.14 — THE APP, IN A CHILD PROCESS. Everything an app's own code touches is in here: the modules
2
2
  // it compiled to, the client bundle, and the render. The listener is the parent (`serveDev`), so
3
3
  // replacing this process is how a reload happens and nothing an app left behind — a module in the
4
4
  // registry, a timer, a `share`, a file bun once failed to find — survives one.
@@ -6,15 +6,15 @@
6
6
  // THE COMPILER IS NOT THE APP, and it stays outside: `serveDev` compiles and sends the modules
7
7
  // in, so the compiler is loaded once for every reload — `serveDev` says what that saves.
8
8
  //
9
- // NOTHING IS WRITTEN TO DISK (38.17). The modules arrive as text and are reached through a
10
- // plugin: `Bun.plugin` for the server modules this worker imports, and a build plugin for the
11
- // bundle a browser fetches. The paths they are keyed under mirror the output tree `abide build`
12
- // would have written, so a module's imports resolve the same in either.
9
+ // NOTHING IS WRITTEN TO DISK (38.16). The compilation arrives as text and is served through the one
10
+ // plugin `abide start` serves a build's through (`abideModules`): at runtime for the server modules
11
+ // this worker imports, and in the bundle a browser fetches — each `.abide` at its own path, so a
12
+ // module's imports resolve from the source that wrote them in either.
13
13
  //
14
- // A REQUEST ARRIVES AS A MESSAGE, and its answer leaves as a head and then a chunk per pull (38.36,
15
- // 38.37): the listener asks for the next chunk when its connection has taken the last, so a stream
14
+ // A REQUEST ARRIVES AS A MESSAGE, and its answer leaves as a head and then a chunk per pull (38.35,
15
+ // 38.36): the listener asks for the next chunk when its connection has taken the last, so a stream
16
16
  // is read no faster than the browser reads it. A request BODY is still collected, which is what
17
- // lets the listener re-ask a request of the replacement (38.25).
17
+ // lets the listener re-ask a request of the replacement (38.24).
18
18
  //
19
19
  // 18.1, 18.3 — AND SO DOES A SOCKET. The listener holds the browser's web socket, which has to
20
20
  // outlive this worker; what crosses is each frame, tagged with the connection it came over, and
@@ -23,13 +23,16 @@
23
23
  import { CLIENT_ADDRESS, DEV_ADDRESS } from '#shared/ADDRESSES.ts'
24
24
  import { escapeText } from '#shared/escapeText.ts'
25
25
  import {
26
- appSources,
27
- type LoadedSources,
28
- type ServerTable,
29
- } from './answerRequest.ts'
26
+ abideModules,
27
+ type Compilation,
28
+ serveCompilation,
29
+ servedTable,
30
+ } from './abideModules.ts'
31
+ import { type AppSources, appSources } from './answerRequest.ts'
30
32
  import type { Mux, MuxConnection } from './answerSocket.ts'
31
33
  import { appPlugins } from './appPlugins.ts'
32
- import { CLIENT_ENTRY, PUBLIC_DIRECTORY } from './CLIENT_BUILD.ts'
34
+ import { publishedUnder } from './appTree.ts'
35
+ import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
33
36
  import {
34
37
  clientBuildOptions,
35
38
  stylesheetUnderMount,
@@ -40,26 +43,28 @@ import { fenceServer } from './fenceServer.ts'
40
43
  import { type Manifest, manifestOf, NO_MANIFEST } from './manifestOf.ts'
41
44
  import { admitsMux } from './muxUpgrade.ts'
42
45
  import { refuseAddressCollision } from './refuseAddressCollision.ts'
43
- import { shellUnder } from './render.ts'
44
46
  import { LISTENING } from './requestScope.ts'
45
- import { handlersIn, pathsUnder, sourceModuleLoader } from './serveBuild.ts'
46
- import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
47
+ import { withAbideSources } from './withAbideSources.ts'
47
48
 
48
49
  // The reload client, which the worker serves and every document it answers loads.
49
50
  const RELOAD_CLIENT_ADDRESS = `${DEV_ADDRESS}/client.js`
50
51
 
51
52
  // The bundle this worker answers `/__abide/client/…` out of, the type each name is served as, and
52
- // the chunks each route module needs (38.29).
53
+ // the chunks each route module needs (38.28).
53
54
  type Bundle = {
54
- // A stylesheet is held as its text, rewritten (`stylesheetUnderMount`); the rest as built.
55
- files: Map<string, { bytes: ArrayBuffer | string; type: string }>
55
+ // A stylesheet is held as its text, rewritten (`stylesheetUnderMount`); a source map as the
56
+ // rewrite of it, taken on the first ask (`withAbideSources`); the rest as built.
57
+ files: Map<
58
+ string,
59
+ { bytes: ArrayBuffer | string | (() => Promise<string>); type: string }
60
+ >
56
61
  manifest: Manifest
57
62
  }
58
63
 
59
64
  /**
60
65
  * A message from the listener to this worker.
61
66
  *
62
- * 38.15 — WHAT CROSSES BETWEEN THE LISTENER AND THIS WORKER, one union per direction. In: the app
67
+ * 38.14 — WHAT CROSSES BETWEEN THE LISTENER AND THIS WORKER, one union per direction. In: the app
63
68
  * compiled (and what stopped it compiling, where something did), a request, a pull or cancel of
64
69
  * its body, and each browser web socket's frames for the mux.
65
70
  */
@@ -67,7 +72,9 @@ export type ToWorker =
67
72
  | {
68
73
  kind: 'start'
69
74
  root: string
70
- modules: Map<string, string>
75
+ compilation: Compilation
76
+ /** Every public file, to its address, as the compile's walk found them. */
77
+ published: Map<string, string>
71
78
  failure: string
72
79
  }
73
80
  | ({ kind: 'request' } & Ask)
@@ -120,12 +127,12 @@ export type Ask = {
120
127
  /**
121
128
  * Sends a message to the listener.
122
129
  *
123
- * 38.15 — over bun's IPC: this is its child process (`serveDev` says why a process).
130
+ * 38.14 — over bun's IPC: this is its child process (`serveDev` says why a process).
124
131
  */
125
132
  function toListener(message: FromWorker): void {
126
133
  process.send?.(message)
127
134
  }
128
- // 38.97 — EACH REQUEST'S SIGNAL, by request id, until its answer ends: the listener holds the
135
+ // 38.96 — EACH REQUEST'S SIGNAL, by request id, until its answer ends: the listener holds the
129
136
  // connection, so a hang-up reaches this worker as a `cancel` rather than through bun.
130
137
  const HANG_UPS = new Map<number, AbortController>()
131
138
 
@@ -157,9 +164,9 @@ let app: BootedApp | undefined
157
164
  // 18.1 — the mux over this worker's handlers, and each connection the listener holds.
158
165
  let mux: Mux | undefined
159
166
  // What the mux carries, which `admitsMux` reads: an app with no handler has no mux to open.
160
- let carried: LoadedSources | undefined
167
+ let carried: AppSources | undefined
161
168
  const connections = new Map<number, MuxConnection>()
162
- // What stopped the app compiling, kept rather than thrown: 38.4 keeps the server up and 38.18
169
+ // What stopped the app compiling, kept rather than thrown: 38.4 keeps the server up and 38.17
163
170
  // keeps it up through a bundle that failed, so the failure is something to SERVE.
164
171
  let failure = ''
165
172
 
@@ -196,7 +203,7 @@ async function receive(message: ToWorker): Promise<void> {
196
203
  })
197
204
  },
198
205
  // NOTHING TO CLOSE: the listener holds the browser's socket, and it outlives this
199
- // worker (38.15). Nothing here stops the mux, either.
206
+ // worker (38.14). Nothing here stops the mux, either.
200
207
  close() {},
201
208
  },
202
209
  )
@@ -343,79 +350,78 @@ async function open(
343
350
  failure = start.failure
344
351
  if (failure !== '') return
345
352
  const root = start.root
353
+ const compilation = start.compilation
346
354
  try {
347
- const base = `${root}/${VIRTUAL_DIRECTORY}`
348
- const modules = new Map<string, string>()
349
- for (const [name, text] of start.modules)
350
- modules.set(`${base}/${name}`, text)
351
355
  // A page among what compiled — an app of handlers alone has the tables and no client to
352
356
  // bundle.
353
- const paged = modules.has(`${base}/${CLIENT_ENTRY}.ts`)
357
+ const paged = compilation.tables[`${CLIENT_ENTRY}.ts`] !== undefined
354
358
 
355
359
  // ONE PLUGIN, TWO CALLERS: the runtime resolves this worker's `import()` through it and
356
360
  // the bundler resolves the browser's graph through it, so the two lanes cannot disagree
357
361
  // about what a specifier means. Registering it is process-wide — which is a worker rather
358
362
  // than the command, so a reload is a new registry rather than a cache to bust.
359
- const plugin = virtualModules(modules)
360
- Bun.plugin(plugin)
363
+ serveCompilation(root, compilation)
361
364
 
362
- // ASKED FOR TOGETHER. The bundle, the shell, the public directory and the server's own
365
+ // ASKED FOR TOGETHER. The bundle, the public directory and the server's own
363
366
  // tables depend on none of each other — the plugin is registered above, before any of
364
367
  // them asks it anything — and on a reload every one of them is latency the author waits on,
365
368
  // so the bundle is handed over still building (`SourceParts.client`).
366
369
  //
367
- // ONE LISTING of the public directory, for the bundle's externals and for what is served.
368
- // 38.20 — OUT OF THE SOURCE TREE. `abide build` copies these into its output and dev has no
369
- // output, so the address answers where the author's file actually is.
370
- const published = pathsUnder(`${root}/ui/${PUBLIC_DIRECTORY}`)
371
- const publicAddresses = Array.from(
372
- published.keys(),
373
- (name) => `/${name}`,
374
- )
375
- // 38.44 — the app's own plugins, imported beside the reads below rather than ahead of them.
370
+ // ONE LISTING of the public directories — the compile's — for the bundle's externals and for
371
+ // what is served. 38.19 — OUT OF THE SOURCE TREE. `abide build` copies these into its output
372
+ // and dev has no output, so the address answers where the author's file actually is.
373
+ const publicSources = start.published
374
+ const published = publishedUnder(root, publicSources)
375
+ const publicAddresses = [...publicSources.values()]
376
+ // 38.43 — the app's own plugins, imported beside the reads below rather than ahead of them.
376
377
  const bundling = appPlugins(root).then((own) =>
377
378
  bundleInMemory(
378
379
  root,
379
- base,
380
- // 16.28, 16.29 — the browser's view of `server/`, as `abide build` gives it.
381
- [plugin, fenceServer(root, start.modules), ...own],
380
+ // 16.28, 16.29 — the browser's view of the server, as `abide build` gives it.
381
+ [
382
+ fenceServer(root, compilation.mounted),
383
+ abideModules(root, compilation),
384
+ ...own,
385
+ ],
382
386
  paged,
383
387
  publicAddresses,
388
+ compilation,
384
389
  ),
385
390
  )
386
- const [shell, table, handlers] = await Promise.all([
387
- shellUnder(root),
388
- import(
389
- Bun.pathToFileURL(`${base}/server.ts`).href
390
- ) as Promise<ServerTable>,
391
- handlersIn(base),
392
- ])
393
- // 38.18 — THE RELOAD CLIENT IS THIS WORKER'S, not the bundle's, and it goes in whether or
394
- // not there is an entry beside it: a bundle that failed is exactly when a reader needs the
395
- // next save to reach them. `clientTags` writes nothing for a manifest with no entry.
396
- const sources = await appSources({
397
- table,
398
- shell,
399
- handlers,
400
- handlerModule: sourceModuleLoader(root, base),
401
- published,
402
- client: bundling.then((bundle) => ({
403
- manifest: bundle.manifest,
404
- head: `<script type="module" src="${RELOAD_CLIENT_ADDRESS}"></script>`,
405
- framework: (pathname) => served(bundle, pathname),
406
- })),
407
- explained: (thrown) => failedDocument(describe(thrown)),
408
- })
409
-
391
+ const table = await servedTable(root)
392
+ const handlers = table.handlers
410
393
  // 36.13, IN THIS LANE TOO. A throw here lands in `failure`, which is what 38.4 asks
411
394
  // for: the command keeps answering and what it answers names both halves. Without it
412
395
  // `answerRequest` below would silently prefer the file — its public-before-routes
413
396
  // order rests on this collision being impossible.
414
- refuseAddressCollision(table.routes, publicAddresses, 'abide dev')
397
+ refuseAddressCollision(
398
+ table.routes,
399
+ handlers,
400
+ publicSources,
401
+ 'abide dev',
402
+ )
415
403
 
416
- carried = sources
417
- // THE SAME `App` `abide start` boots, so an app's hooks and middleware run in dev too.
418
- app = await bootApp(originUnder(root), async () => sources)
404
+ // THE SAME `App` `abide start` boots, so an app's hooks and middleware run in dev too — and its
405
+ // sources are read INSIDE the boot, as `abide start` reads them: a document's head carries
406
+ // what the resolved configuration says (28.4's app name), which is not resolved before it.
407
+ app = await bootApp(originUnder(root), async () => {
408
+ // 38.17 — THE RELOAD CLIENT IS THIS WORKER'S, not the bundle's, and it goes in whether or
409
+ // not there is an entry beside it: a bundle that failed is exactly when a reader needs the
410
+ // next save to reach them. `clientTags` writes nothing for a manifest with no entry.
411
+ const sources = await appSources({
412
+ table,
413
+ handlers,
414
+ published,
415
+ client: bundling.then((bundle) => ({
416
+ manifest: bundle.manifest,
417
+ head: `<script type="module" src="${RELOAD_CLIENT_ADDRESS}"></script>`,
418
+ framework: (pathname) => served(bundle, pathname),
419
+ })),
420
+ explained: (thrown) => failedDocument(describe(thrown)),
421
+ })
422
+ carried = sources
423
+ return sources
424
+ })
419
425
  // 18.1 — the mux `App.listen` would open, over the app's own record: a frame's `rpc.raw` and
420
426
  // MCP surface are this app's.
421
427
  mux = app.openMux()
@@ -435,9 +441,21 @@ function served(bundle: Bundle, pathname: string): Response | undefined {
435
441
  if (!pathname.startsWith(`${CLIENT_ADDRESS}/`)) return undefined
436
442
  const found = bundle.files.get(pathname.slice(CLIENT_ADDRESS.length + 1))
437
443
  if (found === undefined) return undefined
444
+ const { bytes } = found
445
+ const body =
446
+ typeof bytes === 'function'
447
+ ? new ReadableStream({
448
+ async start(controller) {
449
+ controller.enqueue(
450
+ new TextEncoder().encode(await bytes()),
451
+ )
452
+ controller.close()
453
+ },
454
+ })
455
+ : bytes
438
456
  // NO CACHING AT ALL, which is where dev parts company with 21.5: the names are stable across
439
457
  // rebuilds, so a browser told to keep `entry.js` for a year keeps the app it had at boot.
440
- return new Response(found.bytes, {
458
+ return new Response(body, {
441
459
  headers: { 'content-type': found.type, 'cache-control': 'no-store' },
442
460
  })
443
461
  }
@@ -445,30 +463,28 @@ function served(bundle: Bundle, pathname: string): Response | undefined {
445
463
  /**
446
464
  * The client bundle, built into memory.
447
465
  *
448
- * 38.17 — Unminified, and the entry named by its source rather than content-hashed, so what a
466
+ * 38.16 — Unminified, and the entry named by its source rather than content-hashed, so what a
449
467
  * browser loads and what a stack trace names are the module the author wrote. 38.5's hashes and
450
468
  * manifest are the BUILD's, and the difference is deliberate: a dev chunk that changed has to be
451
469
  * fetched again, and a name that changes with it is a cache the reload cannot clear.
452
470
  *
453
- * A FAILURE IS AN EMPTY BUNDLE AND NOT A THROW (38.18). The pages still render, the document
471
+ * A FAILURE IS AN EMPTY BUNDLE AND NOT A THROW (38.17). The pages still render, the document
454
472
  * still loads the reload client, and the next save is what fixes it.
455
473
  */
456
474
  async function bundleInMemory(
457
475
  root: string,
458
- base: string,
459
476
  plugins: Bun.BunPlugin[],
460
477
  paged: boolean,
461
478
  publicAddresses: string[],
479
+ compilation: Compilation,
462
480
  ): Promise<Bundle> {
463
481
  const bundle: Bundle = { files: new Map(), manifest: NO_MANIFEST }
464
482
  if (!paged) return bundle
465
483
  const built = await Bun.build({
466
- // 36.15's addresses off the source tree this lane answers them out of (38.20).
467
- ...clientBuildOptions(base, plugins, publicAddresses),
468
- // The app's own root, because `Bun.build` opens this directory and a virtual one is not
469
- // there to open. What it decides is where the output names are rooted, and the naming
470
- // below makes them flat anyway.
471
- root,
484
+ // 36.15's addresses off the source tree this lane answers them out of (38.19).
485
+ ...clientBuildOptions(root, plugins, publicAddresses),
486
+ // 38.97 — a debugger in the browser reads the `.abide` (`withAbideSources`).
487
+ sourcemap: 'linked',
472
488
  naming: {
473
489
  entry: '[name].[ext]',
474
490
  chunk: '[name]-[hash].[ext]',
@@ -476,7 +492,7 @@ async function bundleInMemory(
476
492
  },
477
493
  throw: false,
478
494
  })
479
- // THE TERMINAL RATHER THAN THE DOCUMENT. 38.18 keeps the server up, and a page that still
495
+ // THE TERMINAL RATHER THAN THE DOCUMENT. 38.17 keeps the server up, and a page that still
480
496
  // paints is what it keeps up — so the pages stay served, unhydrated, and the author reads the
481
497
  // build error where they started the command.
482
498
  if (!built.success) {
@@ -493,103 +509,24 @@ async function bundleInMemory(
493
509
  const output = built.outputs[index] as Bun.BuildArtifact
494
510
  const name = output.path.slice(output.path.lastIndexOf('/') + 1)
495
511
  const bytes = read[index] as ArrayBuffer
496
- bundle.files.set(name, {
497
- bytes: name.endsWith('.css')
498
- ? stylesheetUnderMount(new TextDecoder().decode(bytes))
499
- : bytes,
500
- type: output.type,
501
- })
502
- }
503
- // `base` AS GIVEN, with no real path taken: every route module is one the plugin served, and
504
- // the metafile names those under the plugin's namespace exactly as the plugin spelled them.
505
- bundle.manifest = manifestOf(built.metafile as Bun.BuildMetafile, base)
506
- return bundle
507
- }
508
-
509
- /**
510
- * The plugin serving the compiled modules out of `modules`, for the runtime and the bundler alike.
511
- *
512
- * A module in the map wins; everything else is resolved from where that module PRETENDS to be,
513
- * which is inside the app's package — so `abide`, `#ui/…` and a stylesheet all answer exactly as
514
- * they do for a build on disk.
515
- */
516
- function virtualModules(modules: Map<string, string>): Bun.BunPlugin {
517
- return {
518
- name: 'abide-dev',
519
- setup(build) {
520
- build.onResolve({ filter: /.*/ }, (args) => {
521
- if (modules.has(args.path))
522
- return { path: args.path, namespace: 'abide-dev' }
523
- // The importer carries its namespace in front of the path when it is one of
524
- // ours — and the runtime puts a `/` in front of that again, where the bundler
525
- // does not. Both spellings are stripped here because a `file://` URL refuses to
526
- // take either as a base, and the one that was missed cost the whole route table:
527
- // every dynamic import in it read as a module nobody could find.
528
- const importer = args.importer.replace(/^\/?abide-dev:/, '')
529
- if (!modules.has(importer)) return undefined
530
- const from = importer.slice(0, importer.lastIndexOf('/'))
531
- if (args.path.startsWith('.')) {
532
- const beside = Bun.fileURLToPath(
533
- new URL(args.path, Bun.pathToFileURL(`${from}/`)),
534
- )
535
- if (modules.has(beside))
536
- return { path: beside, namespace: 'abide-dev' }
537
- }
538
- // OUT OF THE MAP AND INTO THE PACKAGE. Resolved from the virtual directory rather
539
- // than left to the default, which has an `abide-dev:` prefix to walk up from and
540
- // finds nothing.
541
- try {
542
- return {
543
- path: Bun.resolveSync(args.path, from),
544
- namespace: 'file',
545
- }
546
- } catch {
547
- return undefined
548
- }
549
- })
550
- // ONLY THE RUNTIME'S LOADS ARE REWRITTEN: the bundler asks `onResolve` above for a `#`
551
- // specifier itself, and a reload's loads are nearly all the bundler's — 181 of 183 on
552
- // the docs app, where the rewrite's parse was ~25 ms of each reload.
553
- const bundling = 'config' in build
554
- build.onLoad({ filter: /.*/, namespace: 'abide-dev' }, (args) => ({
555
- contents: bundling
556
- ? (modules.get(args.path) ?? '')
557
- : withSeamsResolved(
558
- modules.get(args.path) ?? '',
559
- args.path,
560
- ),
561
- // 34.4 — a component's scoped styles are a stylesheet the bundle collects.
562
- loader: args.path.endsWith('.css') ? 'css' : 'ts',
563
- }))
564
- },
565
- }
566
- }
567
-
568
- // A `#` SPECIFIER IN A MODULE FROM THE MAP, resolved before the module is handed over. The runtime
569
- // takes a `#` specifier to the package's `imports` itself rather than through this plugin, and from
570
- // a module in the map it takes the target as an exact file — so the extensionless
571
- // `#server/rpc/contacts` a component writes found nothing, where the bundler, which does ask the
572
- // plugin, probed its way to the `.ts` through the same `Bun.resolveSync` — so the rewrite is the
573
- // runtime's alone, and both lanes land on the same path.
574
- const TRANSPILER = new Bun.Transpiler({ loader: 'ts' })
575
-
576
- function withSeamsResolved(source: string, path: string): string {
577
- if (!path.endsWith('.ts')) return source
578
- const from = path.slice(0, path.lastIndexOf('/'))
579
- let resolved = source
580
- for (const { path: specifier } of TRANSPILER.scanImports(source)) {
581
- if (!specifier.startsWith('#')) continue
582
- let target: string
583
- try {
584
- target = Bun.resolveSync(specifier, from)
585
- } catch {
586
- continue
512
+ let served: ArrayBuffer | string | (() => Promise<string>) = bytes
513
+ if (name.endsWith('.css'))
514
+ served = stylesheetUnderMount(new TextDecoder().decode(bytes))
515
+ // 38.97 — PLACED ON THE FIRST ASK: only a debugger reads a map, and placing every chunk's
516
+ // up front was ~45 ms of each reload on the docs app, whether or not one was open.
517
+ else if (output.kind === 'sourcemap') {
518
+ let placed: Promise<string> | undefined
519
+ served = () =>
520
+ (placed ??= withAbideSources(
521
+ new TextDecoder().decode(bytes),
522
+ compilation,
523
+ root,
524
+ ))
587
525
  }
588
- resolved = resolved
589
- .replaceAll(`'${specifier}'`, `'${target}'`)
590
- .replaceAll(`"${specifier}"`, `"${target}"`)
526
+ bundle.files.set(name, { bytes: served, type: output.type })
591
527
  }
592
- return resolved
528
+ bundle.manifest = manifestOf(built.metafile as Bun.BuildMetafile, root)
529
+ return bundle
593
530
  }
594
531
 
595
532
  /**
@@ -620,7 +557,7 @@ function failureMarkup(why: string): string {
620
557
  return `<main><h1>abide dev</h1><pre>${escapeText(why)}</pre></main>`
621
558
  }
622
559
 
623
- // 38.16's other end. The listener force-closes every live socket when it replaces this worker, so
560
+ // 38.15's other end. The listener force-closes every live socket when it replaces this worker, so
624
561
  // a CLOSE is the rebuild and the reconnect that follows is the new app — there is no message to
625
562
  // send and nothing to version. A reader who never opened the socket is told nothing, which is the
626
563
  // same as before the save.
@@ -0,0 +1,4 @@
1
+ // 38.46 — A SCOPED NAME IS NAMED BY ITS LAST PART: `@acme/ledger` is `ledger`, as `bunx` calls it.
2
+ export function executableName(packageName: string | undefined): string {
3
+ return (packageName ?? 'app').replace(/^@[^/]+\//, '')
4
+ }
@@ -1,48 +1,73 @@
1
- // 16.28 AND 16.29 — WHAT THE BROWSER'S BUNDLE SEES OF `server/`. A page imports a handler from the
2
- // module the app wrote it in, and the server's render reaches that module as written; the bundle a
3
- // browser loads reaches it through here instead, and gets the wrapper the build wrote in its place
4
- // (`WIRE_DIRECTORY`). Anything else under `server/` is refused, so a secret, a database client or
5
- // the server barrel cannot ride into the browser by being imported from the wrong file.
1
+ // 16.29 — WHAT THE BROWSER'S BUNDLE SEES OF THE SERVER: nothing under a `server` directory, at any
2
+ // depth, so a secret or a database client cannot ride into the browser by being imported from the
3
+ // wrong file. A handler crosses as the wrapper the compiler wrote in its place — a handler file as
4
+ // that file, a private handler in its server block's place (16.28); this is the fence around
5
+ // everything else.
6
6
  //
7
7
  // A PLUGIN ON THE BUNDLE, NOT A REWRITE OF THE PAGES. A rewrite reaches the specifiers the compiler
8
- // emitted; the bundle reaches every edge — a `#ui` helper importing a handler included.
8
+ // emitted; the bundle reaches every edge — a helper the page imports, importing a server module,
9
+ // included.
10
+ //
11
+ // REGISTERED AHEAD OF `abideModules`, AND IN ITS NAMESPACES: a load is answered by the first plugin
12
+ // that answers it, and a component or a file's text is loaded there rather than as a `file`.
13
+ //
14
+ // BY POSITION, NOT BY PATH (23.19): a directory a file mounts places its files where the app says,
15
+ // so a `server` directory inside it is fenced as the app's own would be, wherever the package lies.
9
16
 
10
- import { realpathSync } from 'node:fs' // bun exposes no realpath of its own
11
- import { WIRE_DIRECTORY } from './CLIENT_BUILD.ts'
17
+ import { resolve } from 'node:path' // bun exposes no path helpers of its own
18
+ import { SERVER_DIRECTORY } from '#shared/RESERVED_DIRECTORIES.ts'
19
+ import { placeOf } from '#shared/routePattern.ts'
20
+ import {
21
+ COMPONENT_NAMESPACE,
22
+ type Compilation,
23
+ STYLE_NAMESPACE,
24
+ TEXT_NAMESPACE,
25
+ } from './abideModules.ts'
26
+
27
+ // Every namespace a browser bundle loads a file of the app's in: its modules, its components, a
28
+ // file's text and a component's stylesheet.
29
+ const FENCED = ['file', COMPONENT_NAMESPACE, TEXT_NAMESPACE, STYLE_NAMESPACE]
12
30
 
13
31
  /**
14
- * The bundler plugin handing the browser each handler's wrapper and refusing the rest of `server/`.
32
+ * The bundler plugin refusing everything under a `server` directory to the browser's bundle.
15
33
  *
16
- * `modules` is the compiled app, keyed by path below the output directory as `compileApp` hands it
17
- * back — where the wrappers are.
34
+ * `root` is its real path (`appRoot`), which is how the bundler names a file; `mounted` is the
35
+ * compilation's. Listed before `abideModules`.
18
36
  */
19
37
  export function fenceServer(
20
38
  root: string,
21
- modules: Map<string, string>,
39
+ mounted: Compilation['mounted'],
22
40
  ): Bun.BunPlugin {
23
- // BOTH SPELLINGS OF THE ROOT, because the bundler names a file with its symlinks resolved and an
24
- // app under `/tmp` is `/private/tmp` to it.
25
- const roots = new Set([root, realpathSync(root)])
41
+ // Mounted directories first: one under the root's `node_modules` is the app's at its position.
42
+ const directories: [string, string][] = []
43
+ for (const [directory, at] of Object.entries(mounted))
44
+ directories.push([`${resolve(root, directory)}/`, at])
45
+ const positionOf = (path: string): string | undefined => {
46
+ for (const [directory, at] of directories)
47
+ if (path.startsWith(directory))
48
+ return `${at}${path.slice(directory.length)}`
49
+ return path.startsWith(`${root}/`)
50
+ ? path.slice(root.length + 1)
51
+ : undefined
52
+ }
53
+ // Anything with a `server` directory in its path, and `placeOf` decides by its position — a
54
+ // load it passes on falls through to the next plugin's, or the bundler's own.
55
+ const filter = new RegExp(`/${SERVER_DIRECTORY}/`)
26
56
  return {
27
57
  name: 'abide-fence',
28
58
  setup(build) {
29
- for (const at of roots)
30
- build.onLoad(
31
- {
32
- filter: new RegExp(`^${RegExp.escape(at)}/server/`),
33
- },
34
- (args) => {
35
- const source = args.path.slice(at.length + 1)
36
- const wrapper = modules.get(
37
- `${WIRE_DIRECTORY}/${source}`,
38
- )
39
- if (wrapper === undefined)
40
- throw new Error(
41
- `abide: ${source} is server code, and the browser's bundle imports it. A browser may import a handler from a module under server/rpc and nothing else under server/. RULEBOOK 16.29.`,
42
- )
43
- return { contents: wrapper, loader: 'ts' }
44
- },
45
- )
59
+ for (const namespace of FENCED)
60
+ build.onLoad({ filter, namespace }, (args) => {
61
+ const position = positionOf(args.path)
62
+ if (
63
+ position === undefined ||
64
+ placeOf(position) !== 'server'
65
+ )
66
+ return undefined
67
+ throw new Error(
68
+ `abide: ${position.replace(/::\w+$/, '')} is under a server directory, and the browser's bundle imports it. RULEBOOK 16.29.`,
69
+ )
70
+ })
46
71
  },
47
72
  }
48
73
  }