@bakery-framework/core 2.0.0-alpha.8 → 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
@@ -4,7 +4,7 @@ import { fs } from '../../utils'
4
4
  * Route mounts: map a URL prefix onto a directory outside the app's serve root.
5
5
  *
6
6
  * Handlers resolve files under `Bakery.serveRoot`, so a plugin wanting to serve
7
- * its own pages or client assets had no choice but to hand-roll it — reading
7
+ * its own pages or client assets had no choice but to hand-roll it: reading
8
8
  * files itself, bundling on the fly, and reimplementing the caching and
9
9
  * containment the handler pipeline already does. `DashboardHandler` is the
10
10
  * worked example of that cost.
@@ -17,7 +17,7 @@ import { fs } from '../../utils'
17
17
  * the search dir *and* the root, so traversal cannot escape it.
18
18
  */
19
19
  export interface RouteMount {
20
- /** URL prefix, leading slash, no trailing slash — e.g. `/_dashboard`. */
20
+ /** URL prefix, leading slash, no trailing slash, e.g. `/_dashboard`. */
21
21
  prefix: string
22
22
  /** Absolute directory the prefix resolves against. */
23
23
  dir: fs.AbsolutePath
@@ -1,4 +1,5 @@
1
1
  import { LRUCache } from '../../cache/lru'
2
+ import { hostKey } from '../../core/bakery'
2
3
  import type { Handler } from './$base'
3
4
 
4
5
  /**
@@ -51,8 +52,8 @@ export class HandlerMap<T extends typeof Handler = typeof Handler> extends Map<
51
52
  * `delete` and `clear` invalidate too, and neither used to.
52
53
  *
53
54
  * `list()` memoizes the sorted handler array and only `set` cleared it, so a
54
- * removed handler stayed in the list — and therefore stayed *in the request
55
- * pipeline* — until something happened to add one. Registration is
55
+ * removed handler stayed in the list (and therefore stayed *in the request
56
+ * pipeline*) until something happened to add one. Registration is
56
57
  * add-only in a served process, which is why this never bit: it is reachable
57
58
  * only by code that unregisters, and the first thing to do that was a test.
58
59
  *
@@ -98,7 +99,7 @@ export class HandlerMap<T extends typeof Handler = typeof Handler> extends Map<
98
99
  * `canHandle` without the microtask hop when the answer is synchronous.
99
100
  *
100
101
  * Most canHandles are plain predicates, and `await`ing their boolean cost a
101
- * microtask hop (~426ns) per probe, 2–4 probes per request. Returns a
102
+ * microtask hop (~426ns) per probe, 2 to 4 probes per request. Returns a
102
103
  * boolean for a sync answer and the promise itself for an async one, so
103
104
  * callers only `await` a value that is actually a promise:
104
105
  *
@@ -126,8 +127,17 @@ export class HandlerMap<T extends typeof Handler = typeof Handler> extends Map<
126
127
  // must not run twice. That constraint is what the branching encodes.
127
128
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: cache-path + side-effect-sensitive probe loop
128
129
  async resolve(path: string, req?: Request, ...rest: any[]) {
129
- const host = req?.__hostname || ''
130
- const pathId = `${this.id}:${host}:${path}`
130
+ // The **resolved** host, not the header. `__hostname` is whatever arrived
131
+ // in `Host`, and `routeCache` is one bounded LRU shared by every tenant, so
132
+ // a client varying that header minted an entry per spelling and walked the
133
+ // real hosts' entries out of the cache for as long as it kept asking. The
134
+ // file cache carried the same bug and was fixed; this one kept it.
135
+ //
136
+ // `hostKey` resolves through the multi-host config the way every other
137
+ // per-tenant key does, so an unknown or unconfigured host collapses to a
138
+ // single entry instead of one per spelling. The shape is unchanged when
139
+ // there is no host: `hostKey` returns the bare path.
140
+ const pathId = `${this.id}:${hostKey(path)}`
131
141
  const cached: any = HandlerMap.routeCache.get(pathId)
132
142
 
133
143
  // Only tracked once there is a cache hit to skip past. Middleware has
@@ -138,7 +148,7 @@ export class HandlerMap<T extends typeof Handler = typeof Handler> extends Map<
138
148
  if (cached) {
139
149
  probed = new Set()
140
150
  // A cache hit skips every handler above the cached one. Ask the
141
- // gatekeepers that outrank it first — the same handlers, in the same
151
+ // gatekeepers that outrank it first: the same handlers, in the same
142
152
  // order, a cold resolve would have reached before the cached one.
143
153
  const rank = this.order().get(cached) ?? Number.POSITIVE_INFINITY
144
154
  for (const gate of this.gatekeepers()) {
@@ -15,14 +15,14 @@ export type RouteScanOptions = {
15
15
  }
16
16
 
17
17
  // `[!.]` keeps catch-alls (`[...name]`) out of the single-param globs: they
18
- // are matched separately, and last — a catch-all is the weakest route form,
18
+ // are matched separately, and last. A catch-all is the weakest route form,
19
19
  // consulted only after specific files, single-param siblings and child-index
20
20
  // descent have all missed.
21
21
  const catchAllGlob = (ext: string) => new Bun.Glob(`[[]...*${ext || '.*'}`)
22
22
 
23
23
  // The single-param route forms, in the order they are tried: `[name].ext`
24
24
  // first, then the literal-asterisk `*.ext`. Built here rather than twice inside
25
- // `routeGlobs` — the `dynamicOnly` branch and the combined branch returned
25
+ // `routeGlobs`: the `dynamicOnly` branch and the combined branch returned
26
26
  // character-identical pairs, and the two must stay in step or a route form
27
27
  // resolves under one caller and not the other. A function, not a hoisted
28
28
  // constant: `ext` varies per handler, and `staticOnly` returns before it needs
@@ -33,22 +33,22 @@ const catchAllGlob = (ext: string) => new Bun.Glob(`[[]...*${ext || '.*'}`)
33
33
  // a path separator: Bun read `\*.*` as a drive-absolute pattern, ignored the
34
34
  // `cwd` in `GETFILE` entirely, and matched files at `C:\`. A route lookup under
35
35
  // a serve root six levels down returned `C:\$WINRE_BACKUP_PARTITION.MARKER`,
36
- // and `fs.isForbidden` — whose walk is bounded by `startsWith(root)` — waved it
36
+ // and `fs.isForbidden` (whose walk is bounded by `startsWith(root)`) waved it
37
37
  // through because an out-of-root path skipped the loop and answered "allowed".
38
38
  // That clamp now fails closed, so this is belt and braces; both halves are
39
39
  // pinned, and neither test can see the other's bug.
40
40
  //
41
41
  // Measured with a scan whose cwd was a temp directory holding one file: `[*]`
42
42
  // yields nothing, `\*` yields four files from the drive root. The escape is
43
- // also unreachable on Windows in the direction it was meant for — `*` is a
43
+ // also unreachable on Windows in the direction it was meant for: `*` is a
44
44
  // reserved character in a Windows filename, so a route file literally named
45
45
  // `*.ts` can only exist on POSIX, where both spellings match it identically
46
46
  // (verified on Linux). The character class costs nothing and means the same
47
47
  // thing on both platforms.
48
48
  //
49
49
  // Exported only as a test seam: with the clamp in place no test driving
50
- // `getRoute` can tell the two spellings apart — verified by reverting this line
51
- // and watching all 17 pass — so the pattern has to be asserted directly.
50
+ // `getRoute` can tell the two spellings apart (verified by reverting this line
51
+ // and watching all 17 pass), so the pattern has to be asserted directly.
52
52
  export const dynamicGlobs = (ext: string) => [
53
53
  new Bun.Glob(`[[][!.]*${ext || '.*'}`),
54
54
  new Bun.Glob(`[*]${ext || '.*'}`),
@@ -88,8 +88,7 @@ const routeGlobs = (
88
88
  * The catch-all fallback for one directory level: `dir/[...name].ext`,
89
89
  * containment-checked like every other candidate. Skipped for `staticOnly`
90
90
  * (the caller wants a literal file), and it *yields to any real file*: when
91
- * the request's remaining segments name an existing file under `dir` —
92
- * whatever its extension — the catch-all declines, so a lower-priority
91
+ * the request's remaining segments name an existing file under `dir` ( * whatever its extension) the catch-all declines, so a lower-priority
93
92
  * handler (TSHandler, StaticHandler) can serve the file itself. A directory
94
93
  * is not a file and does not trigger the yield. `findDynamicRoute` applies
95
94
  * the same rule on the cached path; the two must agree.
@@ -102,7 +101,7 @@ const routeGlobs = (
102
101
  * `provides.ts` at `/teacher/provides` and `/teacher/provides.js`, so neither
103
102
  * spelling named a file on disk and a Vue catch-all above it (priority 58 vs
104
103
  * 50) served HTML to a browser that asked for a module. The probe now also
105
- * tries the registered dynamic extensions against the extensionless base —
104
+ * tries the registered dynamic extensions against the extensionless base:
106
105
  * the same mapping the serving handlers apply, read from the live registry so
107
106
  * a plugin's extension (`.vue`) counts without core naming it.
108
107
  *
@@ -118,7 +117,7 @@ export function servedSourceExists(target: string): boolean {
118
117
  try {
119
118
  exts = (handler as { config?: { ext?: unknown } }).config?.ext
120
119
  } catch {
121
- // A config getter that needs state this process lacks — a handler with
120
+ // A config getter that needs state this process lacks: a handler with
122
121
  // no ext table cannot claim a source file either way.
123
122
  continue
124
123
  }
@@ -147,13 +146,13 @@ async function getCatchAllRoute(
147
146
  const target = fs.resolve(dir, restSegments.join('/'))
148
147
  // Stat only inside `dir`. A rest containing `..` resolves outside it, and
149
148
  // statting there would make this yield a boolean existence probe for any
150
- // path on the filesystem — the 404-vs-render difference is observable.
151
- // URL parsing normalises `..` and `%2e%2e` away, so no HTTP request
149
+ // path on the filesystem: the 404-vs-render difference is observable.
150
+ // URL parsing normalizes `..` and `%2e%2e` away, so no HTTP request
152
151
  // reaches here with one; this closes the door for any caller that skips
153
- // that normalisation. An escape skips the yield rather than refusing, so
152
+ // that normalization. An escape skips the yield rather than refusing, so
154
153
  // the catch-all answers exactly as it did before the yield rule existed.
155
154
  // `dir` comes from `fs.resolve`, so the separator-suffixed prefix test is
156
- // exact — plain `startsWith(dir)` would also accept a sibling directory
155
+ // exact: plain `startsWith(dir)` would also accept a sibling directory
157
156
  // whose name merely begins with it.
158
157
  if (
159
158
  (target === dir || target.startsWith(`${dir}/`)) &&
@@ -169,7 +168,7 @@ async function getCatchAllRoute(
169
168
 
170
169
  // A bare-directory request (`/docs` with no index) reaches here with no
171
170
  // rest segments, and only the `[...name!]` spelling opted into claiming
172
- // it — the plain form keeps requiring at least one segment.
171
+ // it: the plain form keeps requiring at least one segment.
173
172
  if (!restSegments.length && !info.optionalCatchAll) return null
174
173
 
175
174
  return info
@@ -183,7 +182,7 @@ export async function getRoute(
183
182
  root?: fs.AbsolutePath,
184
183
  options: RouteScanOptions = {},
185
184
  ): Promise<Handler.Route.Info | null> {
186
- // Both used to default to `Bakery.serveRoot` independently — two reads of
185
+ // Both used to default to `Bakery.serveRoot` independently. Two reads of
187
186
  // the config getter (an AsyncLocalStorage getStore each) when a caller
188
187
  // omits both. One read now covers whichever is missing; the values are
189
188
  // unchanged.
@@ -234,7 +233,7 @@ export async function getRoute(
234
233
  // statement is `isForbidden(dir, root)` with these exact arguments, and its
235
234
  // `null` reaches the same `return null` this function ends on. Checking
236
235
  // here as well made every level of a nested route pay two identical
237
- // directory tree-walks — and `isForbidden` walks from the file to the root
236
+ // directory tree-walks, and `isForbidden` walks from the file to the root
238
237
  // doing an existsSync at each level, so that is the most expensive thing
239
238
  // this function does. `src/tests/forbidden.test.ts` pins both branches.
240
239
  if (first !== 'index') {
@@ -246,7 +245,7 @@ export async function getRoute(
246
245
  // `first === 'index'` is the bare-directory request (`/docs` arrives here
247
246
  // as an injected 'index' segment, after no index file matched). The plain
248
247
  // `[...name]` pattern requires at least one rest segment and cannot claim
249
- // it; `[...name!]` exists to — `getCatchAllRoute` tells them apart.
248
+ // it; `[...name!]` exists to: `getCatchAllRoute` tells them apart.
250
249
  if (!options.staticOnly) {
251
250
  return await getCatchAllRoute(
252
251
  ext,
@@ -263,7 +262,7 @@ export async function getRoute(
263
262
  if (route) return route
264
263
 
265
264
  // The walk above descends one real segment at a time and dead-ends when
266
- // the next directory does not exist — which for a catch-all request
265
+ // the next directory does not exist, which for a catch-all request
267
266
  // (`/docs/a/b/c` against `docs/[...slug].tsx`) is the common case. Each
268
267
  // level unwinds through here, so the deepest existing directory gets the
269
268
  // first chance to claim the rest.
@@ -4,14 +4,13 @@ import { fs } from '../../utils/fs'
4
4
  import { resolveMount } from './$mounts'
5
5
 
6
6
  /**
7
- * Literal file resolution — the static counterpart to `getRoute`.
7
+ * Literal file resolution: the static counterpart to `getRoute`.
8
8
  *
9
9
  * `getRoute` answers "which file backs this *route*", with extensionless URLs,
10
10
  * dynamic `[id]` segments and glob scanning. This answers the simpler question
11
11
  * "which file is at this path, and am I allowed to serve it".
12
12
  *
13
- * It exists because four handlers were each spelling that out themselves —
14
- * StaticHandler, PublicHandler, ImageHandler and NMHandler — in four slightly
13
+ * It exists because four handlers were each spelling that out themselves ( * StaticHandler, PublicHandler, ImageHandler and NMHandler) in four slightly
15
14
  * different ways, and only some of them checked everything. Once route mounts
16
15
  * arrived the drift got worse: a mount-aware StaticHandler sitting next to a
17
16
  * mount-blind PublicHandler is the kind of inconsistency that turns into a bug
@@ -19,7 +18,7 @@ import { resolveMount } from './$mounts'
19
18
  *
20
19
  * **Three of those four go through this; NMHandler does not, on purpose.** Its
21
20
  * entry point is resolved by `Bun.build`, not by the filesystem, so
22
- * `/_nm/pkg/sub` legitimately means `pkg/sub/index.js` — a path this function
21
+ * `/_nm/pkg/sub` legitimately means `pkg/sub/index.js`: a path this function
23
22
  * answers `null` for, because it is a directory. It therefore repeats the
24
23
  * containment test and calls `fs.isForbidden` itself; the reasoning, and the
25
24
  * test that pins the divergence, are in `handlers/assets/nm.ts`. It is the one
@@ -43,7 +42,7 @@ export interface StaticTarget {
43
42
  * mounted plugin cannot be traversed out of.
44
43
  *
45
44
  * Roots are tried in order, and a root that fails containment is skipped
46
- * rather than aborting — mirroring ImageHandler, which legitimately looks in
45
+ * rather than aborting: mirroring ImageHandler, which legitimately looks in
47
46
  * both the serve root and the public root.
48
47
  */
49
48
  export async function getStatic(
@@ -64,23 +63,23 @@ export async function getStatic(
64
63
  const file = fs.resolve(root, candidate.relative)
65
64
 
66
65
  // Two checks, not one: the prefix test catches a resolved path that
67
- // escaped the root, and isForbidden additionally honours `.forbidden`
68
- // markers. It does *not* apply the blocked globs — `Bakery.config.blocked`
66
+ // escaped the root, and isForbidden additionally honors `.forbidden`
67
+ // markers. It does *not* apply the blocked globs: `Bakery.config.blocked`
69
68
  // is matched against the request path in `router.ts` and again in
70
- // `handlers/assets/static.ts`, never against a resolved file path — so a
71
- // caller reaching for `getStatic` as a one-stop authorisation check is
69
+ // `handlers/assets/static.ts`, never against a resolved file path, so a
70
+ // caller reaching for `getStatic` as a one-stop authorization check is
72
71
  // getting containment and `.forbidden` only.
73
72
  //
74
- // Containment stays first — nothing stats a path that escaped the root.
73
+ // Containment stays first: nothing stats a path that escaped the root.
75
74
  // After that the order is by cost: `isForbidden` walks every directory
76
75
  // between the file and the root, so running it before the existence check
77
- // paid for a whole tree-walk on every candidate that simply is not there —
76
+ // paid for a whole tree-walk on every candidate that simply is not there,
78
77
  // which is most of them, since a miss here is how each root in the list,
79
78
  // and every extensionless probe, gets ruled out. All checks are pure and
80
79
  // all `continue`, so the candidate chosen is unchanged.
81
80
  //
82
81
  // One stat answers both questions. This used to be `fs.exists` (a stat)
83
- // then `fs.isDir` (which is `fs.exists` plus a second stat of its own) —
82
+ // then `fs.isDir` (which is `fs.exists` plus a second stat of its own):
84
83
  // up to three stats per candidate at ~28us each on Bun/Windows. A failed
85
84
  // stat means "nothing servable here" and falls through to the next
86
85
  // candidate, exactly as the exists check did.
@@ -46,19 +46,19 @@ export class ApiHandler extends DynamicHandler {
46
46
  if (result !== null && result !== undefined) return result
47
47
 
48
48
  // Two different faults used to share one bare 404, and neither channel
49
- // named the file — so "No response from handler" sent the developer
49
+ // named the file, so "No response from handler" sent the developer
50
50
  // hunting for a route that was sitting on disk the whole time.
51
51
  //
52
52
  // `null` and `undefined` are what tell them apart, and the split is exact
53
53
  // rather than incidental: `DynamicHandler.executeModule` *throws* when the
54
54
  // import fails (that is the 500 path), returns a literal `null` for
55
55
  // "module loaded, no `default`", and otherwise returns whatever the
56
- // handler produced — `undefined` when it produced nothing.
56
+ // handler produced, `undefined` when it produced nothing.
57
57
  if (result === null) {
58
58
  // A 500, not a 404: the route resolved, the file exists, and the server
59
59
  // could not answer with it. That is a server fault, and the message says
60
- // what to add. PROD still redacts it — `publicBody` replaces any 5xx
61
- // body — so naming the file here discloses nothing to a client.
60
+ // what to add. PROD still redacts it (`publicBody` replaces any 5xx
61
+ // body), so naming the file here discloses nothing to a client.
62
62
  handlerLog.API_NO_DEFAULT({ file: filePath })
63
63
  return response.error(
64
64
  `API route has no export default: ${info.path}`,
@@ -88,7 +88,7 @@ export class ApiErrorHandler extends ErrorHandler {
88
88
 
89
89
  static handle(_p: string, _r: Request, error: Handler.Error.Data) {
90
90
  // `publicBody`, not `errorBody`: in production the latter is the stack of
91
- // whatever threw. The full trace still reaches the log — `handleRequestError`
91
+ // whatever threw. The full trace still reaches the log: `handleRequestError`
92
92
  // passes the unredacted data to `config.onError` before this runs.
93
93
  return response.json.error(error.errorCode, ErrorHandler.publicBody(error))
94
94
  }
@@ -78,8 +78,8 @@ async function sharedHandler(
78
78
  // through `{{...}}` *and* through the `__PAGE_PARAMS__` script injected
79
79
  // into every page, so the raw stack was published in PROD even by a
80
80
  // template that never mentioned it. Guarded because `errorData` is
81
- // `undefined` for an ordinary page — `DEFAULT_ERROR` only exists on the
82
- // error handler — and the spread below tolerates that where the helper,
81
+ // `undefined` for an ordinary page (`DEFAULT_ERROR` only exists on the
82
+ // error handler), and the spread below tolerates that where the helper,
83
83
  // deliberately strict about the shape it redacts, does not.
84
84
  // Annotated because `beginPageRoute` hands back real `Handler.Error.Data`
85
85
  // where this used to read an untyped `(this as any).DEFAULT_ERROR`: the
@@ -63,7 +63,7 @@ export class ProxyHandler extends Handler {
63
63
 
64
64
  const resHeaders = new Headers(proxyRes.headers)
65
65
  // Bun already decompressed the body, so the upstream's content-encoding is
66
- // wrong — and so is its content-length, which described the compressed size.
66
+ // wrong, and so is its content-length, which described the compressed size.
67
67
  resHeaders.delete('content-encoding')
68
68
  resHeaders.delete('content-length')
69
69
  return new Response(proxyRes.body, {
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * WebSocket clients subscribed to the live log stream.
3
3
  *
4
- * `LiveReloadHandler` owns membership — it adds a socket on
4
+ * `LiveReloadHandler` owns membership: it adds a socket on
5
5
  * `subscribe_logger` and removes it on close. Consumers (the dashboard's
6
6
  * log broadcast, the analytics `activeLoggers` gauge) only read.
7
7
  *
8
8
  * This lives in core rather than a plugin deliberately: it used to be
9
9
  * defined in `plugins/analytics/core`, which made core import from a
10
- * plugin — a backwards edge that blocks packaging core on its own.
10
+ * plugin, a backwards edge that blocks packaging core on its own.
11
11
  */
12
12
  export const connectedLoggers = new Set<any>()
@@ -46,7 +46,7 @@ export function setLogCallback(cb: (entry: LoggerEntry) => void) {
46
46
  *
47
47
  * Three call sites used to spell this as `Promise.try(() =>
48
48
  * onLogCallback?.(…)).catch(() => {})`, which allocated two promises and
49
- * scheduled a microtask for *every* line — including every line logged while
49
+ * scheduled a microtask for *every* line, including every line logged while
50
50
  * no sink was registered at all. `Try` swallows a synchronous throw and a
51
51
  * rejected promise alike (see `utils/isomorphic/try.ts`) without allocating
52
52
  * anything on the synchronous path, which is the only path a sink typed
@@ -61,7 +61,7 @@ function emit(entry: LoggerEntry): void {
61
61
  }
62
62
 
63
63
  // Hoisted: both were rebuilt on every call, and neither depends on the input.
64
- // The colour table is a 23-property literal and the pattern a regex literal,
64
+ // The color table is a 23-property literal and the pattern a regex literal,
65
65
  // so a process logging steadily paid for both on every line.
66
66
  const COLORS: MapOf<string> = {
67
67
  r: '\x1b[31m', // Red
@@ -299,7 +299,7 @@ type Messages<T extends MapOf<string>> = {
299
299
  const RX_PARAM = /\{([^}]+)\}/g
300
300
 
301
301
  type ParsedMessage = {
302
- /** The string this was parsed from — the cache's validity check. */
302
+ /** The string this was parsed from: the cache's validity check. */
303
303
  raw: string
304
304
  level: LogLevels
305
305
  template: string
@@ -332,7 +332,7 @@ export function messageLogger<T extends MapOf<string>>(
332
332
  targetMsgs: T,
333
333
  ) {
334
334
  /**
335
- * One entry per *declared* key, so it is bounded by the message table — the
335
+ * One entry per *declared* key, so it is bounded by the message table: the
336
336
  * tables in `serve-log.ts` are literals of a dozen-odd entries each.
337
337
  * Undeclared props are deliberately left uncached: the trap fabricates a
338
338
  * message for those, and a caller reading arbitrary property names off the
@@ -10,10 +10,10 @@ const serveMsgs = {
10
10
  // the one recorded after the last successful sync, so the boot-time schema
11
11
  // sync is skipped. `--sync` bypasses the skip.
12
12
  SCHEMA_SYNC_SKIP:
13
- 'I Schema unchanged since last sync — %yskipping schema sync%* (%c--sync%* forces it)',
13
+ 'I Schema unchanged since last sync: %yskipping schema sync%* (%c--sync%* forces it)',
14
14
  // The skip above is precisely when drift goes unnoticed: schema.ts has not
15
15
  // changed, so no sync runs, so nothing looks at the database. Something else
16
- // altered it — a hand-run ALTER, another environment pointed at the same URL,
16
+ // altered it: a hand-run ALTER, another environment pointed at the same URL,
17
17
  // a restore from an older dump.
18
18
  SCHEMA_DRIFT:
19
19
  'W %yDatabase no longer matches the last schema Bakery applied%*: {reason}. Run %cdb:history%* to see what was applied, or %cdb:sync --dry-run%* to see what the difference means.',
@@ -27,26 +27,32 @@ const serveMsgs = {
27
27
  'I Synced %ytsconfig.json%* paths with %yserver.config.ts%*!',
28
28
  TSCONFIG_PROJECTS_WRITTEN:
29
29
  'I Wrote %y{count}%* tsconfig project(s) to %y.cache/tsconfig/%*',
30
+ // One-time repair. Previous releases wired the generated projects into the
31
+ // app's tsconfig.json as `references`, which broke `tsc -p <app>`
32
+ // (TS6305/6306/6310). Named so the rewrite of a tracked file comes with a
33
+ // line saying why it happened.
34
+ TSCONFIG_REFERENCES_REMOVED:
35
+ 'I Removed generated %yreferences%* from %ytsconfig.json%*, the %y.cache/tsconfig/%* projects are standalone, and referencing them broke %ytsc -p%*',
30
36
  // A plugin asking for a project name that is taken. Named rather than
31
37
  // silent: the symptom otherwise is one plugin's types quietly not applying,
32
38
  // discovered much later and blamed on the wrong thing.
33
39
  TSCONFIG_PROJECT_CLASH:
34
- 'W Plugin %y{plugin}%* wants tsconfig project %y{project}%*, which already exists — %rskipped%*',
40
+ 'W Plugin %y{plugin}%* wants tsconfig project %y{project}%*, which already exists: %rskipped%*',
35
41
  // Degrades to "no types" rather than failing the boot: a missing declaration
36
42
  // is a worse editor experience, not a broken server.
37
43
  TSCONFIG_FILE_UNRESOLVED:
38
- 'W Could not resolve tsconfig files entry %y{entry}%* — %rskipped%*',
44
+ 'W Could not resolve tsconfig files entry %y{entry}%*: %rskipped%*',
39
45
  MANUAL_RELOAD: 'I %yManual reload%* triggered from client logger!',
40
46
  CONFIG_IMPORT_ERR: 'E Failed to import %yserver.config.ts%*: %r{error}%*',
41
47
  // Multi-line on purpose: a present-but-broken config booting on defaults is
42
- // the kind of failure a single scrolled-away line hides. DEV only — in PROD
48
+ // the kind of failure a single scrolled-away line hides. DEV only: in PROD
43
49
  // the same condition throws out of initConfig() instead of logging.
44
50
  CONFIG_BROKEN:
45
- 'E %rserver.config.ts is present but failed to load — booting on built-in defaults (no plugins, no hosts, default port)%*\n file: %y{file}%*\n{error}',
51
+ 'E %rserver.config.ts is present but failed to load: booting on built-in defaults (no plugins, no hosts, default port)%*\n file: %y{file}%*\n{error}',
46
52
  // The banner restatement of CONFIG_BROKEN, so the warning survives console
47
53
  // scrollback and sits next to the URLs the developer actually reads.
48
54
  CONFIG_BROKEN_BANNER:
49
- 'W %rserver.config.ts failed to load%* %y— running on built-in defaults. See the import error above.%*',
55
+ 'W %rserver.config.ts failed to load%* %y, running on built-in defaults. See the import error above.%*',
50
56
  WEBSOCKET_ERR: 'E WebSocket error from %y{ip}%*: %r{error}%*',
51
57
  RATE_LIMITED: 'W Rate limited: %y{ip}%*',
52
58
  // Emitted instead of RATE_LIMITED when sampling (cli/rate-limit.ts) held
@@ -57,32 +63,39 @@ const serveMsgs = {
57
63
  // limit silently 429s load tests and shared-NAT offices, so its existence
58
64
  // gets one announcement. An app-configured value prints nothing.
59
65
  RATE_LIMIT_DEFAULT:
60
- 'I Rate limit: %y{max}%* burst / %y{refill}%* req/s per IP (default) — set %crateLimit: false%* to disable',
66
+ 'I Rate limit: %y{max}%* burst / %y{refill}%* req/s per IP (default). Set %crateLimit: false%* to disable',
61
67
  // Multi-worker port sharing rides on kernel-level SO_REUSEPORT load
62
68
  // balancing, which only Linux provides; elsewhere N sockets either fail to
63
69
  // bind or never receive balanced traffic.
64
70
  CLUSTER_CLAMPED:
65
71
  'W [Cluster] %y{platform}%* has no kernel-level SO_REUSEPORT load balancing (Linux-only), so %y{requested}%* workers cannot share one port. Running a single worker.',
66
72
  WORKER_RESPAWN:
67
- 'W [Cluster] Worker %g{id}%* exited unexpectedly — restarting in %y{delay}%*ms (consecutive failure #%y{failures}%*).',
73
+ 'W [Cluster] Worker %g{id}%* exited unexpectedly: restarting in %y{delay}%*ms (consecutive failure #%y{failures}%*).',
68
74
  // Thread-worker only: the master should hand over the shared memory pool
69
75
  // before the worker starts serving; past the deadline the worker serves on
70
76
  // its local pool rather than deadlock.
71
77
  SHARED_POOL_TIMEOUT:
72
- 'W [Cluster] No shared memory pool from the master within %y{timeout}%*ms — serving on a worker-local pool; cross-worker counters and rate limits will not be shared until it arrives.',
78
+ 'W [Cluster] No shared memory pool from the master within %y{timeout}%*ms: serving on a worker-local pool; cross-worker counters and rate limits will not be shared until it arrives.',
73
79
  // The version wipe could not empty the cache directory, so no "current"
74
80
  // marker was written and the next boot will try again. Worth a line rather
75
81
  // than silence: something is holding those files open, and a stale compiled
76
82
  // page surviving an upgrade is the failure the wipe exists to prevent.
83
+ // SQLite would not take WAL for this file, so it is running the DELETE
84
+ // journal instead. Worth one line rather than silence: on a local disk WAL
85
+ // is 100x faster per write and 29x on a transaction, and sessions write on
86
+ // the request path, so this line is the difference between "this
87
+ // filesystem cannot do better" and a deployment quietly paying that.
88
+ JOURNAL_WAL_REFUSED:
89
+ 'W SQLite refused WAL for %y{file}%* (answered %y{answer}%*): running the %y{mode}%* journal instead. Expected on a network path; on a local disk it costs every write.',
77
90
  CACHE_WIPE_INCOMPLETE:
78
- 'W Cache directory could not be cleared for the version change — %y{dir}%* still contains %y{files}%*. Retrying on next start; close anything holding those files.',
91
+ 'W Cache directory could not be cleared for the version change: %y{dir}%* still contains %y{files}%*. Retrying on next start; close anything holding those files.',
79
92
  } as const
80
93
 
81
94
  export const serveLog = messageLogger(new Logger('serve'), serveMsgs)
82
95
 
83
96
  const handlerMsgs = {
84
97
  // Emitted by `DynamicHandler.executeModule`, which every route handler goes
85
- // through — an API route, a `.tsx` page and an `error-*.tsx` all land here.
98
+ // through: an API route, a `.tsx` page and an `error-*.tsx` all land here.
86
99
  // It used to say "API module" while printing a `.tsx` path, alongside a
87
100
  // `TSX_IMPORT_ERR` that no code ever reached.
88
101
  API_IMPORT_ERR: 'E Failed to import route module (%y{file}%*): %r{error}%*',
@@ -90,7 +103,7 @@ const handlerMsgs = {
90
103
  // default. `ApiHandler` used to answer that with a bare 404 naming nothing,
91
104
  // which reads as "your route file is missing" for a file that is right there.
92
105
  API_NO_DEFAULT:
93
- 'E API route has no %cexport default%* (%y{file}%*) — the module loaded but exposes no handler',
106
+ 'E API route has no %cexport default%* (%y{file}%*): the module loaded but exposes no handler',
94
107
  // A handler ran and returned neither a value nor a Response. Still a 404 (see
95
108
  // `ApiHandler.handle`), but the log names the file rather than leaving the
96
109
  // developer to guess which route answered.
@@ -106,13 +119,13 @@ const handlerMsgs = {
106
119
  BUNDLE_SIDE_EFFECTS_REPAIRED:
107
120
  'I %y{file}%* tree-shook to an empty export list because its package declares %ysideEffects: false%*; re-bundled through a re-export shim, which keeps the code.',
108
121
  BUNDLE_EMPTY_EXPORTS:
109
- 'E %y{file}%* bundled to an export list with no code behind it — every name it exports is undefined, so it is rejected rather than served. The bundler reported success, and re-bundling through a re-export shim did not recover it. Import the specific module you need instead of the package root.',
122
+ 'E %y{file}%* bundled to an export list with no code behind it: every name it exports is undefined, so it is rejected rather than served. The bundler reported success, and re-bundling through a re-export shim did not recover it. Import the specific module you need instead of the package root.',
110
123
  BUNDLE_CJS_INTEROP:
111
124
  'I Generated named exports for the CommonJS package %y{file}%*, so a named import of it works in the browser.',
112
125
  BUNDLE_CJS_PROBED:
113
126
  'I Could not read %y{file}%* export names statically, so they were probed by importing it in a short-lived child process.',
114
127
  BUNDLE_CJS_DEFAULT_ONLY:
115
- 'W %y{file}%* assigns %ymodule.exports%* wholesale and its members could not be read, so its browser bundle exports only %ydefault%* — a named import from it fails in the browser with "does not provide an export named …", and nothing fails here. Import the default and read the property off it, or use an ESM build.',
128
+ 'W %y{file}%* assigns %ymodule.exports%* wholesale and its members could not be read, so its browser bundle exports only %ydefault%*: a named import from it fails in the browser with "does not provide an export named …", and nothing fails here. Import the default and read the property off it, or use an ESM build.',
116
129
  } as const
117
130
 
118
131
  export const handlerLog = messageLogger(new Logger('handlers'), handlerMsgs)
@@ -131,6 +144,7 @@ const pluginMsgs = {
131
144
  UNHANDLED_ERR: 'E Unhandled Plugin Error: %r{error}%*',
132
145
  ANALYTICS_STORE_ERR: 'E Analytics store init failed: %r{error}%*',
133
146
  DASHBOARD_BUNDLE_ERR: 'E Failed to bundle %ydashboard.js%*: %r{error}%*',
147
+ EXPLORER_QUERY_ERR: 'E Explorer %y{op}%* failed: %r{error}%*',
134
148
  } as const
135
149
 
136
150
  export const pluginLog = messageLogger(new Logger('plugins'), pluginMsgs)
@@ -142,7 +156,7 @@ export const errorMsg = (err: any) => err?.stack || err?.message || String(err)
142
156
  *
143
157
  * `BuildMessage` and `ResolveMessage` are the two things Bun throws for a
144
158
  * syntax error and an unresolvable import. Neither is `instanceof Error`,
145
- * neither carries a `stack`, and both report *zero* own enumerable keys — so
159
+ * neither carries a `stack`, and both report *zero* own enumerable keys, so
146
160
  * nothing that inspects a thrown value by spreading it or by `instanceof` sees
147
161
  * anything at all. `message`, `name` and `position` are the accessors that
148
162
  * actually answer.
@@ -188,7 +202,7 @@ function positionLines(position: Diagnostic['position']): string[] {
188
202
  * `Error` and useless for the diagnostics above: they have no stack, so it
189
203
  * degrades to a bare "Expected identifier but found end of file" with no file
190
204
  * and no line. This walks `position` instead, and recurses through the
191
- * `AggregateError.errors` array that `import()`ing a broken `.tsx` throws —
205
+ * `AggregateError.errors` array that `import()`ing a broken `.tsx` throws:
192
206
  * that one *is* an `Error`, but it has no stack either, so the summary line
193
207
  * ("4 errors building …") was all anyone ever saw.
194
208
  *
@@ -217,7 +231,7 @@ export function errorDetail(err: any): string {
217
231
  *
218
232
  * For the log lines whose template already names the file. Bun's own
219
233
  * `position.file` is `input.ts` whenever `transform()` was handed a source
220
- * string rather than a path — which is every compile this codebase does — so
234
+ * string rather than a path (which is every compile this codebase does), so
221
235
  * printing it beside the real path would contradict it.
222
236
  */
223
237
  export function errorWithPosition(err: any): string {
@@ -2,7 +2,7 @@
2
2
  * The plugin-authoring surface, as one barrel.
3
3
  *
4
4
  * Every other public directory in core has an `index.ts` and `plugins/` did
5
- * not, so `@bakery-framework/core/plugins` pointed straight at `routes.ts` — which meant
5
+ * not, so `@bakery-framework/core/plugins` pointed straight at `routes.ts`, which meant
6
6
  * `definePlugin` and `ServerPlugin` were only reachable through
7
7
  * `@bakery-framework/core/plugins/types`, a second subpath for one concept. It also broke
8
8
  * under TypeScript's `paths` resolution, which maps `@bakery-framework/core/plugins` to
@@ -2,8 +2,8 @@
2
2
  * Declarative endpoint tables for plugins.
3
3
  *
4
4
  * The dashboard already used a `Record<path, handler>` map; analytics used a
5
- * hand-rolled `if (path === … && method === …)` chain. This generalises the
6
- * dashboard's shape — the one that was already working — so a third plugin does
5
+ * hand-rolled `if (path === … && method === …)` chain. This generalizes the
6
+ * dashboard's shape (the one that was already working), so a third plugin does
7
7
  * not arrive with a fourth style of dispatch.
8
8
  *
9
9
  * Scope is deliberately narrow: exact-match paths only, no params or wildcards.
@@ -16,7 +16,7 @@ import { response } from '../utils/http/response'
16
16
  import type { ValidResponses } from './types'
17
17
 
18
18
  /**
19
- * Handlers may return anything the router knows how to serialize — the
19
+ * Handlers may return anything the router knows how to serialize: the
20
20
  * dashboard's return `JsonResponseData` and `BunFile` as well as `Response`.
21
21
  * `ValidResponses` (= `Handler.Response`) is the framework's name for that set.
22
22
  */
@@ -27,7 +27,7 @@ export type PluginRoute = (req: Request, url: URL) => ValidResponses
27
27
  * method-qualified path (`'POST /api/_x/reset'`). A method-qualified entry wins
28
28
  * over a bare one for the same path.
29
29
  *
30
- * The two forms are guarded differently — see `guardFor` below. A bare key is
30
+ * The two forms are guarded differently. See `guardFor` below. A bare key is
31
31
  * same-origin-only; a method-qualified one gets the ordinary CSRF check.
32
32
  */
33
33
  export type PluginRouteTable = Record<string, PluginRoute>
@@ -35,7 +35,7 @@ export type PluginRouteTable = Record<string, PluginRoute>
35
35
  /**
36
36
  * What a dispatch built from `T` actually resolves to: the union of that
37
37
  * table's own handler return types, awaited, with the `undefined`/`void` cases
38
- * replaced by `null` — exactly what `dispatch` does at runtime with `?? null`.
38
+ * replaced by `null`. Exactly what `dispatch` does at runtime with `?? null`.
39
39
  *
40
40
  * Keyed on the table type rather than flattened to `ValidResponses` because
41
41
  * `ValidResponses` contains `object`, which absorbs `Response`, `Bun.BunFile`
@@ -59,7 +59,7 @@ export type PluginRouteResult<T extends PluginRouteTable> =
59
59
  * unsafe ones same-origin. `'GET /api/_x/public'` is therefore also the way to
60
60
  * declare an endpoint that *is* meant to answer cross-origin.
61
61
  *
62
- * A **bare** key matches every method, which means the author did not say —
62
+ * A **bare** key matches every method, which means the author did not say,
63
63
  * and that is exactly how the dashboard's mutating endpoints ended up
64
64
  * answering a `GET`. `checkCsrf` is no help there: it waves GET through by
65
65
  * definition, so a cross-site `<img src="/api/_dashboard/execute-action">`
@@ -76,7 +76,7 @@ function guardFor(qualified: boolean) {
76
76
 
77
77
  export function routeTable<T extends PluginRouteTable>(routes: T) {
78
78
  /**
79
- * Declared as an overload — the same shape `handleRequest` uses in
79
+ * Declared as an overload: the same shape `handleRequest` uses in
80
80
  * `router.ts`. The signature carries the precise per-table union; the
81
81
  * implementation only ever sees the `PluginRoute` *constraint*, so on its own
82
82
  * it could infer no better than the wide `ValidResponses`.