@bakery-framework/core 2.0.0-alpha.9 → 2.0.0-rc.1

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 (78) hide show
  1. package/README.md +8 -8
  2. package/package.json +2 -5
  3. package/src/cache/lru.ts +2 -2
  4. package/src/cache/shared-db.ts +81 -11
  5. package/src/cache/string.ts +4 -0
  6. package/src/cache/tiered.ts +11 -5
  7. package/src/client/globals.d.ts +7 -7
  8. package/src/client/livereload.ts +2 -2
  9. package/src/client/utils.ts +11 -6
  10. package/src/compiler/compiler.ts +48 -44
  11. package/src/compiler/dev-service.ts +29 -30
  12. package/src/compiler/prompt-tracker.ts +2 -2
  13. package/src/compiler/tsconfig-sync.ts +130 -59
  14. package/src/core/bakery.ts +6 -3
  15. package/src/core/cache-version.ts +68 -18
  16. package/src/core/config.ts +16 -16
  17. package/src/core/context.ts +8 -8
  18. package/src/core/define-route.ts +5 -6
  19. package/src/core/index.ts +19 -7
  20. package/src/core/init.ts +38 -35
  21. package/src/core/jsx.ts +19 -4
  22. package/src/core/paths.ts +3 -1
  23. package/src/core/plugins.ts +15 -4
  24. package/src/core/port.ts +6 -6
  25. package/src/global.d.ts +36 -18
  26. package/src/handlers/assets/google-font.ts +6 -6
  27. package/src/handlers/assets/image.ts +36 -3
  28. package/src/handlers/assets/nm.ts +7 -7
  29. package/src/handlers/assets/static.ts +55 -12
  30. package/src/handlers/assets/ts.ts +3 -3
  31. package/src/handlers/assets/tsx.ts +1 -1
  32. package/src/handlers/assets/virtual-asset.ts +1 -1
  33. package/src/handlers/core/$base.ts +24 -8
  34. package/src/handlers/core/$dynamic.ts +66 -23
  35. package/src/handlers/core/$error.ts +29 -17
  36. package/src/handlers/core/$middleware.ts +6 -6
  37. package/src/handlers/core/$mounts.ts +2 -2
  38. package/src/handlers/core/$registry.ts +16 -6
  39. package/src/handlers/core/$routing.ts +18 -19
  40. package/src/handlers/core/$static.ts +11 -12
  41. package/src/handlers/routes/api.ts +5 -5
  42. package/src/handlers/routes/html.ts +2 -2
  43. package/src/handlers/routes/proxy.ts +1 -1
  44. package/src/logger/clients.ts +2 -2
  45. package/src/logger/logger.ts +4 -4
  46. package/src/logger/serve-log.ts +32 -18
  47. package/src/plugins/index.ts +1 -1
  48. package/src/plugins/routes.ts +7 -7
  49. package/src/plugins/types.ts +13 -11
  50. package/src/router.ts +12 -12
  51. package/src/session.ts +41 -23
  52. package/src/shared.d.ts +5 -5
  53. package/src/startup.ts +3 -3
  54. package/src/types.d.ts +10 -10
  55. package/src/utils/common/case.ts +1 -1
  56. package/src/utils/common/match.ts +1 -1
  57. package/src/utils/common/misc.ts +1 -1
  58. package/src/utils/constants.ts +15 -15
  59. package/src/utils/fs.ts +97 -39
  60. package/src/utils/http/authorize.ts +24 -17
  61. package/src/utils/http/cors.ts +6 -6
  62. package/src/utils/http/credential.ts +4 -4
  63. package/src/utils/http/csrf.ts +6 -7
  64. package/src/utils/http/dom.ts +9 -9
  65. package/src/utils/http/etag.ts +54 -13
  66. package/src/utils/http/html.ts +29 -20
  67. package/src/utils/http/ip.ts +1 -1
  68. package/src/utils/http/sse.ts +11 -11
  69. package/src/utils/http/url.ts +4 -4
  70. package/src/utils/http/validate.ts +3 -4
  71. package/src/utils/isomorphic/escape.ts +1 -1
  72. package/src/utils/isomorphic/is.ts +2 -2
  73. package/src/utils/isomorphic/misc.ts +1 -1
  74. package/src/utils/isomorphic/stringify.ts +2 -3
  75. package/src/utils/isomorphic/try.ts +1 -1
  76. package/src/utils/shared-pool.ts +23 -16
  77. package/tsconfig.app.json +3 -3
  78. package/tsconfig.vue.json +3 -3
package/README.md CHANGED
@@ -10,7 +10,7 @@ runtime depends on Bun APIs throughout. It will not run on Node.
10
10
  bun add @bakery-framework/core @bakery-framework/cli
11
11
  ```
12
12
 
13
- `@bakery-framework/cli` owns the `bakery` binary that actually starts a server — core on
13
+ `@bakery-framework/cli` owns the `bakery` binary that actually starts a server: core on
14
14
  its own is the library it starts.
15
15
 
16
16
  ## Usage
@@ -55,7 +55,7 @@ Pages are rendered server-side through Bakery's own `createElement`, so an app's
55
55
 
56
56
  `extends` alone is not enough. Bun's runtime does not follow `extends` into a
57
57
  package specifier, so without the three options repeated here every `.tsx` route
58
- fails with `Cannot find module 'react/jsx-dev-runtime'` — while `tsc` stays
58
+ fails with `Cannot find module 'react/jsx-dev-runtime'`, while `tsc` stays
59
59
  perfectly happy, because it *does* follow the extends.
60
60
 
61
61
  The fastest way to get a correct setup is to let the scaffolder write one:
@@ -71,19 +71,19 @@ bun create bakery my-app
71
71
  10 req/s). The startup banner says so; `rateLimit: false` turns it off.
72
72
  - The export map is curated: only the subpaths it names can be imported, and
73
73
  everything else is private. If you need something that is not exposed, open an
74
- issue rather than reaching for a deep path — it will not resolve.
74
+ issue rather than reaching for a deep path: it will not resolve.
75
75
 
76
76
  Full documentation lives in the
77
77
  [repository](https://github.com/obillekyle/bakery).
78
78
 
79
79
  ## License
80
80
 
81
- MIT with the Commons Clause v1.0 — see [LICENSE](./LICENSE). In short: use,
81
+ MIT with the Commons Clause v1.0. See [LICENSE](./LICENSE). In short: use,
82
82
 
83
- **Not an OSI-approved licence.** The Commons Clause removes the right to *sell*
84
- the software — meaning to charge for a product or service whose value derives
83
+ **Not an OSI-approved license.** The Commons Clause removes the right to *sell*
84
+ the software: meaning to charge for a product or service whose value derives
85
85
  substantially from it, hosting and support included. Everything else the MIT
86
- licence grants is unchanged: use it, modify it, ship it inside your own product.
87
- If your organisation only permits OSI-approved dependencies, this will not pass
86
+ license grants is unchanged: use it, modify it, ship it inside your own product.
87
+ If your organization only permits OSI-approved dependencies, this will not pass
88
88
  that check.
89
89
  modify and distribute freely; selling the software itself is not granted.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/core",
3
- "version": "2.0.0-alpha.9",
3
+ "version": "2.0.0-rc.1",
4
4
  "description": "Bakery framework core: handlers, router, config, session, caches, logger, compiler.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -30,19 +30,16 @@
30
30
  "exports": {
31
31
  ".": "./src/core/index.ts",
32
32
  "./compiler": "./src/compiler/index.ts",
33
- "./core": "./src/core/index.ts",
34
33
  "./handlers": "./src/handlers/index.ts",
35
34
  "./logger": "./src/logger/index.ts",
36
35
  "./utils": "./src/utils/index.ts",
37
36
  "./utils/common": "./src/utils/common/index.ts",
38
37
  "./utils/http": "./src/utils/http/index.ts",
39
- "./utils/isomorphic": "./src/utils/isomorphic/index.ts",
40
38
  "./types": "./src/types.d.ts",
41
39
  "./package.json": "./package.json",
42
40
  "./startup": "./src/startup.ts",
43
41
  "./tsconfig.app.json": "./tsconfig.app.json",
44
42
  "./session": "./src/session.ts",
45
- "./jsx": "./src/core/jsx.ts",
46
43
  "./plugins": "./src/plugins/index.ts",
47
44
  "./cache/lru": "./src/cache/lru.ts",
48
45
  "./cache/shared-db": "./src/cache/shared-db.ts",
@@ -68,6 +65,6 @@
68
65
  "!src/tests"
69
66
  ],
70
67
  "engines": {
71
- "bun": ">=1.3.14"
68
+ "bun": ">=1.4.0"
72
69
  }
73
70
  }
package/src/cache/lru.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export class LRUCache<K, V> extends Map<K, V> {
2
2
  /**
3
3
  * Called with the entry dropped to make room. Needed when values own a
4
- * resource that must be released — an evicted prepared statement, for
4
+ * resource that must be released: an evicted prepared statement, for
5
5
  * instance, otherwise leaks until the process exits.
6
6
  */
7
7
  private readonly onEvict?: (key: K, value: V) => void
@@ -17,7 +17,7 @@ export class LRUCache<K, V> extends Map<K, V> {
17
17
 
18
18
  get(key: K): V | undefined {
19
19
  const val = super.get(key)
20
- // `has` only to disambiguate a stored `undefined` from a miss — on the
20
+ // `has` only to disambiguate a stored `undefined` from a miss: on the
21
21
  // common hit this is two map operations instead of four.
22
22
  if (val === undefined && !super.has(key)) return undefined
23
23
  super.delete(key)
@@ -2,14 +2,16 @@ import { Database } from 'bun:sqlite'
2
2
  import { dirname } from 'node:path'
3
3
  import { Bakery } from '../core/bakery'
4
4
  import { checkCacheVersion } from '../core/cache-version'
5
+ import { serveLog } from '../logger/serve-log'
5
6
  import { fs } from '../utils'
7
+ import { Try } from '../utils/common/try'
6
8
 
7
9
  /**
8
- * The tiered cache's spill-to-disk store — sessions and LRU overflow.
10
+ * The tiered cache's spill-to-disk store: sessions and LRU overflow.
9
11
  *
10
- * Under `Bakery.dataDir`, beside `server.db`. Rows here are *rebuildable* — a
12
+ * Under `Bakery.dataDir`, beside `server.db`. Rows here are *rebuildable*: a
11
13
  * session is a cookie plus whatever the app hung off it, and the LRU tier
12
- * refills from source on the next miss — but rebuildable is not the same as
14
+ * refills from source on the next miss, but rebuildable is not the same as
13
15
  * disposable-on-a-schedule, which is what living in `cacheDir` amounted to.
14
16
  * See the reversal below.
15
17
  *
@@ -17,7 +19,7 @@ import { fs } from '../utils'
17
19
  * this paragraph is the reversal.**
18
20
  *
19
21
  * The file lived under `Bakery.cacheDir`, so `checkCacheVersion` deleted it on
20
- * every version bump and every dev<->prod switch — logging every user out. The
22
+ * every version bump and every dev<->prod switch: logging every user out. The
21
23
  * argument for that was sound: a cache format written by an older framework
22
24
  * must never be read by a newer one, and the version wipe is what guarantees
23
25
  * it.
@@ -26,7 +28,7 @@ import { fs } from '../utils'
26
28
  * releases were cut by hand and rare; releases are now computed from commit
27
29
  * messages and published automatically, so a patch ships whenever a `fix:`
28
30
  * lands. Logging out every user of every app on every patch is a far worse
29
- * trade than it was, and it was never the *goal* — only the consequence of
31
+ * trade than it was, and it was never the *goal*: only the consequence of
30
32
  * keying durability on a version number that had nothing to do with the stored
31
33
  * format.
32
34
  *
@@ -35,7 +37,7 @@ import { fs } from '../utils'
35
37
  * a schema mismatch drops the tables, a framework bump does not.
36
38
  *
37
39
  * Two consequences worth knowing. An app with no ORM now creates a `bakery/`
38
- * directory where it previously created none — it is storing durable data, so
40
+ * directory where it previously created none: it is storing durable data, so
39
41
  * that is honest, but it means "no `bakery/` directory" is no longer evidence
40
42
  * that `initDB` never ran. And the LRU spill tier shares this file, so overflow
41
43
  * entries also survive; they are still rebuildable, just no longer discarded on
@@ -43,7 +45,7 @@ import { fs } from '../utils'
43
45
  *
44
46
  * **The `await` below is what makes that true, and it must stay here.** This
45
47
  * module opens the database at *import* time, so running the check anywhere
46
- * else — it used to run from `initConfig()` — leaves the process holding a
48
+ * else (it used to run from `initConfig()`) leaves the process holding a
47
49
  * handle inside the directory the wipe is about to delete. On Windows that
48
50
  * delete fails with `EBUSY`, node's recursive walk stops at the locked entry,
49
51
  * and whatever it had not reached yet survives. Awaiting here orders the two by
@@ -68,8 +70,8 @@ export const cacheDb = new Database(dbFilePath, { create: true })
68
70
  /**
69
71
  * Drop everything if the stored schema version is not this one.
70
72
  *
71
- * The safety property the cache wipe provided — a newer framework never reads
72
- * an older format — is preserved exactly, just keyed on the thing that actually
73
+ * The safety property the cache wipe provided (a newer framework never reads
74
+ * an older format) is preserved exactly, just keyed on the thing that actually
73
75
  * governs compatibility. A mismatch logs users out, which is the same outcome
74
76
  * as before; the difference is that it now happens when the format changes
75
77
  * rather than when any version number does.
@@ -90,8 +92,76 @@ if (!stored) {
90
92
  for (const { name } of tables) cacheDb.run(`DROP TABLE IF EXISTS "${name}"`)
91
93
  cacheDb.run('UPDATE __schema SET version = ?', [SCHEMA_VERSION])
92
94
  }
93
- const journalMode = process.platform === 'win32' ? 'DELETE' : 'WAL'
94
- cacheDb.run(`PRAGMA journal_mode = ${journalMode};`)
95
+ /**
96
+ * **WAL first, DELETE only where WAL is actually refused.**
97
+ *
98
+ * This was `process.platform === 'win32' ? 'DELETE' : 'WAL'`, a rule whose
99
+ * reason was never written down. The standing hypothesis was that a Windows
100
+ * network path cannot host WAL, and that much is true: WAL needs a shared
101
+ * `-shm` mapping, which SMB and WebDAV do not provide. The rule was wrong in
102
+ * its scope rather than its premise. It applied to every Windows install,
103
+ * including the ordinary case of a local disk where WAL works perfectly.
104
+ *
105
+ * The cost of being wrong was paid on the request path, because sessions
106
+ * write there. Measured on this machine, four interleaved rounds with a
107
+ * CPU-bound control that stayed flat at 29-34 ms:
108
+ *
109
+ * single autocommit write DELETE 3629 us WAL 36 us 100x
110
+ * 100-row transaction DELETE 3.91 ms WAL 0.13 ms 29x
111
+ *
112
+ * So the rule is replaced by an attempt and a check, which is what makes it
113
+ * safe to change without knowing why it was there. SQLite refuses WAL in
114
+ * exactly two observable ways, and both are handled here:
115
+ *
116
+ * - it **returns the unchanged mode** in the pragma's own result row, which
117
+ * is what an in-memory or anonymous-temp database does (`memory`); and
118
+ * - it **throws**, which is what a read-only database does.
119
+ *
120
+ * Measured, not assumed: both shapes were reproduced before this was written.
121
+ * A pragma that silently did nothing would make the fallback unsafe, and it
122
+ * does not - the returned row is the authority, so a filesystem that cannot
123
+ * host WAL lands on DELETE and says so once instead of being guessed at in
124
+ * advance.
125
+ *
126
+ * Returning the effective mode rather than the requested one matters for the
127
+ * same reason: the log line has to name what the database is actually doing.
128
+ *
129
+ * Exported as a test seam rather than driven through the module's own
130
+ * import-time call: this file opens the real cache database when it loads, so
131
+ * the only way to assert on a *refusal* is to hand the function a database
132
+ * that produces one. `:memory:` is such a case and is what the test uses.
133
+ */
134
+ export function applyJournalMode(db: Database, file: string): string {
135
+ const asked = Try.return(
136
+ () =>
137
+ db
138
+ .query<{ journal_mode: string }, []>('PRAGMA journal_mode = WAL;')
139
+ .get()?.journal_mode ?? '',
140
+ (error: Error) => `threw: ${error.message}`,
141
+ )
142
+ if (asked.toLowerCase() === 'wal') return 'wal'
143
+
144
+ // Wrapped as well. A database that refuses WAL by throwing will refuse the
145
+ // fallback the same way when the mode it is already in is not DELETE, and
146
+ // an unopenable cache is a far worse outcome than an untuned one.
147
+ const fell = Try.return(
148
+ () =>
149
+ db
150
+ .query<{ journal_mode: string }, []>('PRAGMA journal_mode = DELETE;')
151
+ .get()?.journal_mode ?? 'unknown',
152
+ () =>
153
+ db.query<{ journal_mode: string }, []>('PRAGMA journal_mode;').get()
154
+ ?.journal_mode ?? 'unknown',
155
+ )
156
+ serveLog.JOURNAL_WAL_REFUSED({
157
+ file,
158
+ answer: asked || '(no row)',
159
+ mode: fell,
160
+ })
161
+ return fell
162
+ }
163
+
164
+ applyJournalMode(cacheDb, dbFilePath)
95
165
  cacheDb.run('PRAGMA synchronous = NORMAL;')
96
166
  cacheDb.run('PRAGMA temp_store = memory;')
97
167
  // `core/init.ts` installs THREAD_WORKER as an accessor holding a **boolean**,
@@ -41,6 +41,10 @@ class StringCache {
41
41
 
42
42
  if (flushIntervalMs !== undefined) {
43
43
  this.flushTimer = setInterval(() => this.flushToDisk(), flushIntervalMs)
44
+ // Unref'd for the reason `tiered.ts` gives: `Strings` is constructed at
45
+ // module scope, so importing `compiler/compiler.ts` pinned the event
46
+ // loop open for the life of the process.
47
+ this.flushTimer.unref?.()
44
48
  }
45
49
 
46
50
  registerCache(this)
@@ -35,7 +35,7 @@ export function registerCache(cache: Flushable): void {
35
35
  }
36
36
 
37
37
  /**
38
- * Test seam — see `__setTestDb` / `__setTestConfig` for the pattern.
38
+ * Test seam. See `__setTestDb` / `__setTestConfig` for the pattern.
39
39
  *
40
40
  * Detaches the live registry so a test can drive `flushAllCaches()` against its
41
41
  * own caches without flushing and closing the process's real ones (`Strings`,
@@ -133,6 +133,12 @@ export class TieredCache<K extends string | number, V> {
133
133
  () => this.flushToDisk(),
134
134
  this.opts.flushInterval,
135
135
  )
136
+ // A flush timer must not be the reason a process cannot exit. Every
137
+ // cache here is module-level, so importing `session.ts` (or anything
138
+ // reaching it) left an interval holding the loop open forever: a script
139
+ // or a test that imported core printed its answer and then hung. The CLI
140
+ // never noticed because it calls `process.exit` on every path.
141
+ this.flushTimer.unref?.()
136
142
  }
137
143
 
138
144
  registerCache(this)
@@ -310,7 +316,7 @@ export class TieredCache<K extends string | number, V> {
310
316
  totalPages: number
311
317
  } {
312
318
  // Paging happens in SQL, so the dirty memory tier has to reach disk first.
313
- // That was a third, hand-written copy of `flushToDisk` — same shape, but
319
+ // That was a third, hand-written copy of `flushToDisk`: same shape, but
314
320
  // looping over `stmt.insert` instead of `commitKey`, which is exactly the
315
321
  // bypass `flushAllToDisk` documents below having already been fixed once.
316
322
  // There is nothing search needs that the predicate would deny it: an entry
@@ -390,7 +396,7 @@ export class TieredCache<K extends string | number, V> {
390
396
  if (this.memoryStore.size === 0) return
391
397
  db.transaction(() => {
392
398
  // Through `commitKey`, not a raw insert. This is the shutdown flush, and
393
- // it used to ignore `shouldPersist` — so the interval flush and eviction
399
+ // it used to ignore `shouldPersist`, so the interval flush and eviction
394
400
  // dropped a non-persisting entry while a clean stop wrote it. For the
395
401
  // session tier that predicate is "has persisted keys or has data", so
396
402
  // every shutdown wrote up to a thousand empty sessions, which then
@@ -417,7 +423,7 @@ export class TieredCache<K extends string | number, V> {
417
423
  const entry = this.memoryStore.get(key)
418
424
  if (!entry) return
419
425
  // `shouldPersist?.(v)` is `undefined` when the option is absent, and
420
- // `!undefined` is `true` — so a cache configured without a predicate took
426
+ // `!undefined` is `true`, so a cache configured without a predicate took
421
427
  // the delete branch for every key. Since `enforceMemoryLimit` commits as
422
428
  // it evicts, that made eviction destroy the value rather than demote it
423
429
  // to disk. Absence of the option means "persist everything".
@@ -465,7 +471,7 @@ const logger = new Logger('tiered-cache')
465
471
  * database: this hook is registered at module-evaluation time and therefore
466
472
  * sits at index 0, while a plugin's shutdown hook is registered later during
467
473
  * plugin setup. Closing the handle here invalidated it for every plugin that
468
- * writes on the way out — the analytics flush bound the same `cacheDb`, so
474
+ * writes on the way out: the analytics flush bound the same `cacheDb`, so
469
475
  * every statement threw into an outer catch and up to a minute of page hits
470
476
  * plus the whole history delta was discarded on every clean stop, silently.
471
477
  *
@@ -7,19 +7,19 @@
7
7
  *
8
8
  * - `Try.throw` was declared with a synchronous overload it never had
9
9
  * (`tryThrow` is `Promise.try(...).catch(...)`, so it always returns a
10
- * Promise) — browser code writing `const n: number = Try.throw(() => 1)`
10
+ * Promise): browser code writing `const n: number = Try.throw(() => 1)`
11
11
  * typechecked and silently received a Promise.
12
12
  * - `Case` lost its callable form: the implementation is
13
13
  * `Object.assign(function Case(type, str) {…}, {…})`, so `Case('kebab', s)`
14
14
  * is real but was untypeable.
15
15
  * - `request` was `(url, method?, body?)` against an implementation of
16
- * `(url, init: RequestJson | string = {}, bodyData?)` — the object-init form
16
+ * `(url, init: RequestJson | string = {}, bodyData?)`. The object-init form
17
17
  * was unreachable from the types.
18
18
  * - `randomId` was `() => string` against an implementation taking `length = 8`.
19
19
  *
20
20
  * So the shapes are no longer restated: each global is a `typeof import(...)`
21
21
  * of the module that actually provides it, which fixes all four at once and
22
- * cannot drift again. `typeof import(...)` inside a `.d.ts` is a type query —
22
+ * cannot drift again. `typeof import(...)` inside a `.d.ts` is a type query:
23
23
  * it is erased, and adds no runtime module edge into the browser bundle.
24
24
  *
25
25
  * Two globals genuinely cannot be derived, and stay hand-written:
@@ -31,7 +31,7 @@
31
31
  import type { MapOf } from '../types'
32
32
 
33
33
  declare global {
34
- /** Server-side exports from `<script server>` — available at runtime in `<script setup>` and templates. */
34
+ /** Server-side exports from `<script server>`: available at runtime in `<script setup>` and templates. */
35
35
  const server: { [key: string]: any }
36
36
 
37
37
  const matchDefault: typeof import('../utils/isomorphic/match').matchDefault
@@ -52,7 +52,7 @@ declare global {
52
52
 
53
53
  // Not derivable: written inline in client/utils.ts's Object.assign call
54
54
  // rather than exported, so there is no module member to take `typeof` of.
55
- // Verified against that literal — `version` is the BAKERY_VERSION define,
55
+ // Verified against that literal: `version` is the BAKERY_VERSION define,
56
56
  // `virtual` is `async virtual(path: string)`, `params` is generic.
57
57
  var Bakery: {
58
58
  version: string
@@ -62,13 +62,13 @@ declare global {
62
62
 
63
63
  // `ImportMeta.env` is declared once, by global.d.ts's ImportMetaEnv. This
64
64
  // file used to redeclare it with an incompatible shape, and to carry
65
- // verbatim copies of Wrapped / MapOf / JsonResponse / ISFunction — all four
65
+ // verbatim copies of Wrapped / MapOf / JsonResponse / ISFunction: all four
66
66
  // now live in shared.d.ts, which is where anything the browser and the
67
67
  // server both need belongs.
68
68
  }
69
69
 
70
70
  // A `declare module '@client/utils'` block used to sit here re-exporting
71
- // '../utils'. It never resolved (TS2307 — relative specifiers inside an
71
+ // '../utils'. It never resolved (TS2307: relative specifiers inside an
72
72
  // ambient module declaration do not resolve from the containing file), and
73
73
  // nothing in the codebase imports '@client/utils' from TypeScript: it is a
74
74
  // runtime importMap entry the browser resolves, mapped to /_client/utils.js.
@@ -26,7 +26,7 @@ function hideOverlay() {
26
26
 
27
27
  /**
28
28
  * Full-viewport dev overlay for server-pushed errors and a dead dev server.
29
- * Built strictly with createElement/textContent — the title and body arrive
29
+ * Built strictly with createElement/textContent: the title and body arrive
30
30
  * over the wire and may contain markup-shaped text (stack traces quoting
31
31
  * generics, user file names); nothing here may pass through innerHTML.
32
32
  */
@@ -416,7 +416,7 @@ function connect() {
416
416
  // stopped dev server once a second, indefinitely and in lockstep.
417
417
  const delay = Math.min(1000 * 2 ** reconnectAttempts, 30_000)
418
418
  reconnectAttempts += 1
419
- // A dead dev server used to mean silent reconnect attempts — the page just
419
+ // A dead dev server used to mean silent reconnect attempts: the page just
420
420
  // quietly stopped reloading. After a few failures (~7s of downtime with
421
421
  // the backoff above) say so; onopen dismisses it and reloads on reconnect.
422
422
  if (reconnectAttempts > 3) {
@@ -44,10 +44,10 @@ function processGetBody(
44
44
 
45
45
  // `randomId` and `request` are exported purely so `client/globals.d.ts` can
46
46
  // declare the globals as `typeof import('./utils').randomId` / `.request`
47
- // instead of restating their signatures — which is how both had drifted from
47
+ // instead of restating their signatures, which is how both had drifted from
48
48
  // this file. Nothing imports them; the bundle entry's exports are inert.
49
49
  //
50
- // `randomId` moved to `utils/isomorphic/misc.ts` — it was never
50
+ // `randomId` moved to `utils/isomorphic/misc.ts`: it was never
51
51
  // browser-specific, and a server block calling the "browser global" got a
52
52
  // ReferenceError. Imported (not re-exported directly) because the globals
53
53
  // object below needs the local binding too.
@@ -60,7 +60,7 @@ type RequestJson = RequestInit & { body?: any }
60
60
  /**
61
61
  * What `request()` throws for a non-2xx envelope.
62
62
  *
63
- * It used to throw a bare `Error` carrying only the envelope's `message` —
63
+ * It used to throw a bare `Error` carrying only the envelope's `message`,
64
64
  * which made every structured failure unusable: a 409 whose `data` lists the
65
65
  * conflicting rows, a 400 carrying per-field validation issues. Callers had to
66
66
  * drop to raw `fetch` precisely for the requests where the framework's
@@ -144,7 +144,12 @@ export async function request(
144
144
  return data
145
145
  }
146
146
 
147
- export function formatHTML(html: string, indentWidth: number = 2): string {
147
+ /**
148
+ * File-local. It was exported and nothing imported it: `client/utils.ts` is
149
+ * not a published subpath, so the `export` widened nothing a consumer could
150
+ * reach and only made the name look like part of a surface.
151
+ */
152
+ function formatHTML(html: string, indentWidth: number = 2): string {
148
153
  if (!html) return ''
149
154
 
150
155
  const cleanHtml = html
@@ -195,7 +200,7 @@ Object.assign(globalThis, {
195
200
  Bakery: {
196
201
  // **Cast rather than `import.meta.env` directly.** `ImportMeta.env` is
197
202
  // declared by `bun-types`, and this file is compiled by the *client*
198
- // tsconfig, which deliberately has none — reaching for a Bun-provided type
203
+ // tsconfig, which deliberately has none: reaching for a Bun-provided type
199
204
  // here would undo the split it exists to enforce.
200
205
  //
201
206
  // Declaring `ImportMeta` in client/globals.d.ts instead would clash with
@@ -326,7 +331,7 @@ if (typeof document !== 'undefined') {
326
331
 
327
332
  // Prerender the same filtered list as prefetch, not every link on the page.
328
333
  // A `source: 'document'` rule with href_matches '/*' and eager eagerness
329
- // ignored the exclusions above and fully loaded — and ran the JS of — every
334
+ // ignored the exclusions above and fully loaded (and ran the JS of) every
330
335
  // same-origin link, including things like /logout or a destructive GET.
331
336
  const urlList = Array.from(urls)
332
337
  specScript.textContent = JSON.stringify({