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

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 +54 -8
  18. package/src/core/define-route.ts +5 -6
  19. package/src/core/index.ts +29 -8
  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/src/startup.ts CHANGED
@@ -39,7 +39,7 @@ let pluginSetup: Promise<void> | null = null
39
39
  * Calling both meant every non-clustered worker set its plugins up twice.
40
40
  * Handler and mount registration is idempotent so that went unnoticed, but the
41
41
  * analytics plugin registers a shutdown hook and kicks off `loadAnalyticsData()`
42
- * from `setup()` — so it got two of each. Memoising the promise (not a boolean)
42
+ * from `setup()`, so it got two of each. Memoizing the promise (not a boolean)
43
43
  * also makes a concurrent second caller await the first run rather than race it.
44
44
  */
45
45
  export function setupPlugins(): Promise<void> {
@@ -96,7 +96,7 @@ export async function runStartupBanner(): Promise<void> {
96
96
  const host = Bakery.config.host
97
97
 
98
98
  // Ground truth first. The banner's job is to print where the server is
99
- // listening, and `Bakery.server.port` is what the OS actually gave us —
99
+ // listening, and `Bakery.server.port` is what the OS actually gave us,
100
100
  // which is the only correct answer under `PORT=0`, and the only *honest*
101
101
  // one if the bound port and the requested one ever part company again.
102
102
  // `resolvePort` covers the callers that print a banner without a server
@@ -134,7 +134,7 @@ export async function runStartupBanner(): Promise<void> {
134
134
  // constant, and any user-supplied value replaces the reference. The
135
135
  // default limiter 429s load tests and shared-NAT offices with nothing
136
136
  // anywhere saying it exists, so this line is its one announcement. An
137
- // app-configured value prints nothing — their choice, their knowledge.
137
+ // app-configured value prints nothing: their choice, their knowledge.
138
138
  if (Bakery.config.rateLimit === DEFAULT_RATE_LIMIT) {
139
139
  serveLog.RATE_LIMIT_DEFAULT(DEFAULT_RATE_LIMIT)
140
140
  }
package/src/types.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * These were declared in `declare global`, which is fine inside one repo and a
5
5
  * hazard the moment the packages are published: the names are generic enough
6
6
  * that a consuming app is likely to define its own `MapOf`, and a global type
7
- * alias cannot be merged or opted out of — the app just gets a redeclaration
7
+ * alias cannot be merged or opted out of, the app just gets a redeclaration
8
8
  * error with nowhere to put the fix.
9
9
  *
10
10
  * Moving them cost 24 files an `import type` line and cost consumers nothing:
@@ -12,7 +12,7 @@
12
12
  * that they were always internal plumbing rather than DX.
13
13
  *
14
14
  * `verbatimModuleSyntax` makes the compiler reject a value-position import of
15
- * these, so nothing can accidentally introduce a runtime module edge — which
15
+ * these, so nothing can accidentally introduce a runtime module edge, which
16
16
  * matters because `client/utils.ts` is compiled into the browser bundle.
17
17
  */
18
18
  export type MapOf<T> = { [key: string]: T }
@@ -20,7 +20,7 @@ export type MapOf<T> = { [key: string]: T }
20
20
  /** A value, or a function producing it. */
21
21
  export type Wrapped<T, Args extends any[] = []> = T | ((...args: Args) => T)
22
22
 
23
- /** Awaited or not — the shape almost every framework hook returns. */
23
+ /** Awaited or not: the shape almost every framework hook returns. */
24
24
  export type MixedPromise<T> = Promise<T> | T
25
25
 
26
26
  type ResolveMatchReturn<V, R> = R extends any
@@ -54,12 +54,12 @@ export type Match<D extends symbol> = {
54
54
  /**
55
55
  * The `body` a route module's default export receives: declared params merge
56
56
  * over a permissive base. `body.id` is `string` once you declare it, while
57
- * `body.anythingElse` stays reachable as `any` — the parse rules in
57
+ * `body.anythingElse` stays reachable as `any`: the parse rules in
58
58
  * `utils/http/body.ts` mean the framework genuinely cannot know every key, so
59
59
  * locking the object down would be a lie in the other direction.
60
60
  *
61
61
  * There is no filename-literal inference (`[id].ts` does not conjure
62
- * `{ id: string }` on its own — that needs codegen); the contract is that the
62
+ * `{ id: string }` on its own. That needs codegen); the contract is that the
63
63
  * author declares the shape once, at the `defineRoute` / `html` call, instead
64
64
  * of annotating the whole signature or settling for `any`.
65
65
  */
@@ -71,13 +71,13 @@ export type RouteBody<P = {}> = P & MapOf<any>
71
71
  * directory under the `!` form).
72
72
  *
73
73
  * Exists so a route over mixed or unknown segments can say
74
- * `defineRoute<MapOf<RouteParam>>` instead of hand-writing the union — and so
74
+ * `defineRoute<MapOf<RouteParam>>` instead of hand-writing the union, and so
75
75
  * the union has one definition to change if it ever grows.
76
76
  */
77
77
  export type RouteParam = string | string[]
78
78
 
79
79
  /**
80
- * What a route module may actually return — read off `processResponse`
80
+ * What a route module may actually return. Read off `processResponse`
81
81
  * (`router.ts`) and `ApiHandler.handle`: a `Response`, a `BunFile` (streamed
82
82
  * with an ETag), the JSON envelope, a plain data object or array (JSON-encoded
83
83
  * as-is), a string (HTML-sniffed) or number, or nothing (204; a 404 under
@@ -85,10 +85,10 @@ export type RouteParam = string | string[]
85
85
  *
86
86
  * Deliberately not `object` (which `Handler.Response` uses): `object` absorbs
87
87
  * `Response`, `BunFile` and `JsonResponse` into one member and the union stops
88
- * saying anything — the same trap `routeTable()`'s dispatch avoids. `MapOf<any>`
88
+ * saying anything, the same trap `routeTable()`'s dispatch avoids. `MapOf<any>`
89
89
  * is the narrowest thing that covers "a plain data object"; a target index
90
90
  * signature of `any` does still accept class instances, but that only matters
91
- * for inference targets — this union is a constraint on what an author may
91
+ * for inference targets: this union is a constraint on what an author may
92
92
  * return, and nothing consumes a route module's return type downstream, so the
93
93
  * members stay legible instead of collapsing.
94
94
  */
@@ -107,7 +107,7 @@ export type RouteResponse = MixedPromise<
107
107
 
108
108
  /**
109
109
  * A typed route module default export. `$dynamic.executeModule` calls it as
110
- * `(req, body, Bakery.server)` — the third argument is optional here because
110
+ * `(req, body, Bakery.server)`: the third argument is optional here because
111
111
  * `Bakery.server` is itself optional before `Bun.serve` runs, and almost no
112
112
  * handler wants it.
113
113
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `Case` moved to `utils/isomorphic/case` — the client bundle carried an
2
+ * `Case` moved to `utils/isomorphic/case`: the client bundle carried an
3
3
  * identical copy. `toHash` stays here: it calls `Bun.hash`, so it is server-only
4
4
  * and must not be pulled into the isomorphic layer.
5
5
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Moved to `utils/isomorphic/match` so the browser bundle and the server share
3
- * one implementation — the prototype-chain fix (`Object.hasOwn` rather than
3
+ * one implementation: the prototype-chain fix (`Object.hasOwn` rather than
4
4
  * `in`) previously had to be applied to both copies separately.
5
5
  */
6
6
  export * from '../isomorphic/match'
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `is`, `Math2` and `throws` moved to `utils/isomorphic/` — the client bundle
2
+ * `is`, `Math2` and `throws` moved to `utils/isomorphic/`: the client bundle
3
3
  * carried its own copies of all three. They are re-exported here so existing
4
4
  * `@server/utils` importers are unaffected.
5
5
  *
@@ -12,7 +12,7 @@ export const DEFAULT_RATE_LIMIT = { max: 100, refill: 10 }
12
12
  * Files that must never be served off disk.
13
13
  *
14
14
  * These are matched against the file a *file-serving* handler is about to
15
- * return, not against every request path — see `servesFiles` in `router.ts`.
15
+ * return, not against every request path. See `servesFiles` in `router.ts`.
16
16
  * An API route or a proxied path is a name, not a file, and never had any
17
17
  * business being tested against this list.
18
18
  *
@@ -22,8 +22,8 @@ export const DEFAULT_RATE_LIMIT = { max: 100, refill: 10 }
22
22
  * on that attempt.
23
23
  *
24
24
  * Note what is *not* here: an extension-wide ban on JSON. It caught every
25
- * JSON document an app might legitimately publish — a web app manifest, a
26
- * `.well-known` document — and since `blocked` in config can only ever append,
25
+ * JSON document an app might legitimately publish: a web app manifest, a
26
+ * `.well-known` document, and since `blocked` in config can only ever append,
27
27
  * an app had no way to opt back out. What the list is actually protecting is
28
28
  * the handful of JSON files that describe the project, so those are named.
29
29
  */
@@ -46,9 +46,9 @@ export const DEFAULT_BLOCKED_GLOBS = [
46
46
  // trailing `**` collapses to a single segment: `{**/node_modules/**}` matches
47
47
  // `/node_modules/a` and then stops, so `/node_modules/pkg/index.js` was not
48
48
  // blocked by anything. Each pattern still matched on its own, which is why
49
- // it survived — the defect only exists in the form the config actually uses.
49
+ // it survived: the defect only exists in the form the config actually uses.
50
50
  // `.cache` is the framework's disposable working dir and `bakery/` holds the
51
- // database and its backups — the visible one is the precious one, so that a
51
+ // database and its backups: the visible one is the precious one, so that a
52
52
  // dotfile sweep cannot reach the data. `bakery/` being un-dotted is why it
53
53
  // needs naming here explicitly: `*.db` covers the database file itself, but
54
54
  // not the `.ts` schema snapshots under `bakery/backups/`.
@@ -67,17 +67,17 @@ export const DEFAULT_BLOCKED_GLOBS = [
67
67
  /**
68
68
  * A request path folded to the form the *filesystem* would resolve it to.
69
69
  *
70
- * Two normalisations, both of them things the OS does and the glob does not:
70
+ * Two normalizations, both of them things the OS does and the glob does not:
71
71
  *
72
72
  * - **Case.** NTFS and APFS are case-insensitive, so `/PACKAGE.JSON` opens the
73
- * same bytes as `/package.json` — but `Bun.Glob` is case-sensitive, so only
73
+ * same bytes as `/package.json`, but `Bun.Glob` is case-sensitive, so only
74
74
  * the second one was blocked. Every file this list protects was reachable by
75
75
  * holding down shift, and the static pipeline was verified to serve
76
76
  * `/STYLES/GLOBAL.CSS` and `/styles/global.css` as the same 170 bytes.
77
77
  * - **Trailing dots and spaces.** Win32 strips both from each path component,
78
78
  * so `/package.json.` and `/package.json ` are the same file too.
79
79
  *
80
- * `.`, `..` and any all-dots component are left intact — trimming those would
80
+ * `.`, `..` and any all-dots component are left intact: trimming those would
81
81
  * rewrite a relative segment into something else, and containment is
82
82
  * `fs.isForbidden`'s job, not this function's.
83
83
  */
@@ -93,14 +93,14 @@ export function normalizeBlockedPath(path: string): string {
93
93
  * Each path component truncated at its first `:`.
94
94
  *
95
95
  * Win32 resolves `x.ts::$DATA` to the bytes of `x.ts` and
96
- * `dir::$INDEX_ALLOCATION` to `dir` — NTFS alternate-data-stream syntax — and
96
+ * `dir::$INDEX_ALLOCATION` to `dir` (NTFS alternate-data-stream syntax), and
97
97
  * `new URL()` keeps the suffix in `pathname`, so a glob anchored on the real
98
98
  * name missed while the filesystem opened the file anyway. The directory form
99
99
  * matters independently: it defeats the `dir/**` patterns that the file form
100
100
  * cannot reach.
101
101
  *
102
102
  * Only ever used as an *extra* match candidate (see `matchBlocked`), never as
103
- * a replacement — colons are legal in POSIX filenames, and a real file named
103
+ * a replacement: colons are legal in POSIX filenames, and a real file named
104
104
  * `a:b.db` must stay blocked by `*.db` rather than being tested as `a`.
105
105
  */
106
106
  function stripStreamSuffix(path: string): string {
@@ -117,18 +117,18 @@ function stripStreamSuffix(path: string): string {
117
117
  /**
118
118
  * Whether `path` is refused by the blocked globs.
119
119
  *
120
- * Matched twice: once as written, once normalised. Both are needed, and for
121
- * different reasons. The normalised pass is what closes the case and
120
+ * Matched twice: once as written, once normalized. Both are needed, and for
121
+ * different reasons. The normalized pass is what closes the case and
122
122
  * trailing-dot variants above. The verbatim pass is what keeps an app's own
123
- * `blocked: ['**\/Secrets.txt']` working — `initConfig` appends user patterns
123
+ * `blocked: ['**\/Secrets.txt']` working, `initConfig` appends user patterns
124
124
  * to this list untouched, so folding only the path would quietly stop matching
125
125
  * the very patterns the app wrote.
126
126
  *
127
127
  * On a case-*sensitive* filesystem this over-blocks: a file genuinely named
128
128
  * `/PACKAGE.JSON`, distinct from `/package.json`, is refused. That is the
129
- * intended direction to be wrong in, and it is the reason this normalises the
129
+ * intended direction to be wrong in, and it is the reason this normalizes the
130
130
  * path rather than resolving the file. The router runs this check before it
131
- * knows which file — often before there is a file at all — so "ask the
131
+ * knows which file (often before there is a file at all), so "ask the
132
132
  * filesystem for the real name" is not available at the point the answer is
133
133
  * needed, and would cost a `realpath` per request where it was.
134
134
  */
package/src/utils/fs.ts CHANGED
@@ -15,7 +15,7 @@ import { gzip as zlibGzip } from 'node:zlib'
15
15
  import { LRUCache } from '../cache/lru'
16
16
  // A cycle on paper (`core/context` imports this file for `getAppVersion`),
17
17
  // harmless in practice: neither module touches the other's bindings during
18
- // evaluation — `hostStore` is only read inside `isForbidden`, at request time.
18
+ // evaluation. `hostStore` is only read inside `isForbidden`, at request time.
19
19
  import { hostStore } from '../core/context'
20
20
  import type { MixedPromise } from '../types'
21
21
  import { is, Try } from './common'
@@ -42,7 +42,7 @@ const gzipAsync = promisify(zlibGzip) as (
42
42
  * 1MB | 5804us | 6480us | 14469us | 17114us
43
43
  * (32KB gzip: median of 5 x 200-iteration runs; single runs were noisy)
44
44
  *
45
- * At 32KB the sync cost crosses the dispatch overhead for zstd — the
45
+ * At 32KB the sync cost crosses the dispatch overhead for zstd, the
46
46
  * first-preference codec. gzip's raw-latency break-even sits nearer 64KB,
47
47
  * but the sync figure is a *stall* shared by every concurrent request while
48
48
  * the async figure is latency private to one caller, so the tie goes to
@@ -54,8 +54,8 @@ export const ASYNC_COMPRESSION_MIN = 32 * 1024
54
54
  * `compress` is the sync variant (blocks the event loop for the durations
55
55
  * above); `compressAsync` runs on a worker pool and only parks the caller.
56
56
  * zstd: `Bun.zstdCompress` (native async, Bun >= 1.1.21). gzip: Bun 1.3.14
57
- * has no async `Bun.gzip`, so `node:zlib`'s callback `gzip` — which runs on
58
- * the libuv pool — is promisified instead. Both async variants were verified
57
+ * has no async `Bun.gzip`, so `node:zlib`'s callback `gzip` (which runs on
58
+ * the libuv pool) is promisified instead. Both async variants were verified
59
59
  * byte-identical to their sync counterparts on this Bun, so either path
60
60
  * yields the same cached artifacts and the same response bodies.
61
61
  */
@@ -82,13 +82,13 @@ function toArray<T>(val?: MixedArray<T>): T[] {
82
82
  }
83
83
 
84
84
  /**
85
- * Memoised: `resolve` measures ~16-20us on Bun/Windows (see `isForbidden`) and
85
+ * Memoized: `resolve` measures ~16-20us on Bun/Windows (see `isForbidden`) and
86
86
  * a CPU profile of the request path put `safeResolve` + `path.resolve` at over
87
87
  * 40% of per-request self time. Caching is safe because the function is purely
88
- * lexical — `path.resolve` reads no filesystem state, only `process.cwd()`.
88
+ * lexical: `path.resolve` reads no filesystem state, only `process.cwd()`.
89
89
  * Relative arguments make entries cwd-dependent, and while nothing in the
90
90
  * runtime calls `chdir`, tests do (config.broken.test.ts boots from temp
91
- * fixture dirs) — so the cache is invalidated wholesale when cwd moves. The
91
+ * fixture dirs), so the cache is invalidated wholesale when cwd moves. The
92
92
  * check is one `process.cwd()` read per call, measured at 12ns in Bun (it is
93
93
  * cached natively), against the ~16us a resolve costs. The cache is an LRU,
94
94
  * not a Map, because arguments derive from request paths (convention 6);
@@ -238,7 +238,7 @@ export namespace Glob {
238
238
  }
239
239
 
240
240
  export namespace FileSystem {
241
- // Naming, not constraint — `string & {}` is `string`. These document what a
241
+ // Naming, not constraint: `string & {}` is `string`. These document what a
242
242
  // parameter means at its declaration, which earns their keep. Four more
243
243
  // (`RequestPath`, `DirectoryPath`, `FileName`, `FileExtension`) documented
244
244
  // nothing, because nothing ever referenced them.
@@ -267,7 +267,7 @@ export namespace FileSystem {
267
267
 
268
268
  /**
269
269
  * Reads as UTF-8 text. Without the encoding this returns a Buffer, which
270
- * every caller then hands to `JSON.parse` — legal at runtime because it
270
+ * every caller then hands to `JSON.parse`: legal at runtime because it
271
271
  * coerces, but a type error at each call site, papered over in one of them
272
272
  * with `as any`.
273
273
  */
@@ -281,9 +281,29 @@ export namespace FileSystem {
281
281
  return (await Try(() => Bun.file(path).stat()))?.isDirectory() || false
282
282
  }
283
283
 
284
+ /**
285
+ * Handed a `BunFile`, this used to take the path back off it and build a
286
+ * **second** one to stat. A `BunFile` caches its own stat, so the instance
287
+ * the caller already has is free to read and the fresh one is a syscall:
288
+ * one question about one file cost two stats and an allocation.
289
+ *
290
+ * It shows up wherever a caller asks both questions, which is the common
291
+ * shape - `validCache` below, `nm.ts`'s `exists(nmFile) ? nmFile.lastModified`,
292
+ * and both mtime comparisons in `image.ts`. Measured on this machine, four
293
+ * interleaved rounds against a CPU-bound control flat at 27-31 ms:
294
+ *
295
+ * validCache, file present 33.0 us -> 17.3 us 1.9x
296
+ * validCache, file absent 34.9 us -> 17.4 us 2.0x
297
+ *
298
+ * The predicate is unchanged, and the two clauses in it are not
299
+ * interchangeable. `lastModified` on a **missing** file answers roughly
300
+ * `Date.now()` rather than 0, which is what the `< Date.now()` guard is
301
+ * for; dropping it reports every absent file as present. The `size > 0`
302
+ * fallback then covers a file whose mtime is in the future, which clock
303
+ * skew on a network share produces.
304
+ */
284
305
  export function exists(path: string | Bun.BunFile): boolean {
285
- path = typeof path === 'string' ? path : path.name || ''
286
- const file = Bun.file(path)
306
+ const file = typeof path === 'string' ? Bun.file(path) : path
287
307
  const lastMod = file.lastModified
288
308
  return Boolean((lastMod && lastMod < Date.now()) || file.size > 0)
289
309
  }
@@ -293,7 +313,7 @@ export namespace FileSystem {
293
313
  }
294
314
 
295
315
  /**
296
- * True only for an existing regular *file* — a directory answers false.
316
+ * True only for an existing regular *file*: a directory answers false.
297
317
  * Sync because `findDynamicRoute` (a sync hot-path scan) is a caller; one
298
318
  * stat, no BunFile allocation.
299
319
  */
@@ -302,12 +322,12 @@ export namespace FileSystem {
302
322
  }
303
323
 
304
324
  /**
305
- * Parent directory of an **already-normalised** absolute path — the output
325
+ * Parent directory of an **already-normalized** absolute path: the output
306
326
  * shape `safeResolve` produces: forward slashes, no `.`/`..` segments, no
307
327
  * trailing slash except at a root.
308
328
  *
309
329
  * Only correct for that input shape, which is why it is not exported.
310
- * `isForbidden` is the sole caller and it normalises before the first call.
330
+ * `isForbidden` is the sole caller and it normalizes before the first call.
311
331
  */
312
332
  function parentOf(path: string): string {
313
333
  const idx = path.lastIndexOf('/')
@@ -319,7 +339,7 @@ export namespace FileSystem {
319
339
  // Both of these branches are defensive rather than covered. `isForbidden`
320
340
  // stops as soon as the walk leaves `root`, and every root it is called
321
341
  // with is a real serve directory well below a filesystem root, so the walk
322
- // never reaches one — dropping them both leaves the equivalence test in
342
+ // never reaches one: dropping them both leaves the equivalence test in
323
343
  // `fs.test.ts` green. They are kept so `parentOf` is correct as written
324
344
  // rather than correct only for its current caller.
325
345
  if (path.charCodeAt(idx - 1) === 58 /* ':' */) return path.slice(0, idx + 1)
@@ -330,7 +350,7 @@ export namespace FileSystem {
330
350
  * True when `pathToCheck`, or any directory between it and `root`, holds a
331
351
  * `.forbidden` marker.
332
352
  *
333
- * This is a security guard, and it runs several times per request — from
353
+ * This is a security guard, and it runs several times per request, from
334
354
  * `router.ts` before anything else, then again from `getStatic`, `getRoute`
335
355
  * (once per level of a nested route) and the dynamic-route cache. Each call
336
356
  * walks every directory between the target and the serve root, so the
@@ -338,26 +358,26 @@ export namespace FileSystem {
338
358
  *
339
359
  * The walk used to call `safeResolve` twice per level, once to build
340
360
  * `<dir>/.forbidden` and once to step to the parent. Both are `node:path`
341
- * `resolve`, which on Bun/Windows measures **~16-20us** — roughly 100x
361
+ * `resolve`, which on Bun/Windows measures **~16-20us**: roughly 100x
342
362
  * `path.dirname` (~0.2us), independent of the filesystem and of where the
343
363
  * cwd lives, and not explained by native-call overhead (`Bun.nanoseconds()`
344
364
  * is 0.045us). A depth-4 walk therefore paid ten `resolve` calls.
345
365
  *
346
- * Since `curr` is normalised once on entry, every value it takes afterwards
366
+ * Since `curr` is normalized once on entry, every value it takes afterwards
347
367
  * is already in `resolve`'s own output shape, so both operations are exact
348
368
  * string edits. That takes a depth-4 walk from ten `resolve` calls to two
349
369
  * and leaves one `existsSync` per level as the only remaining syscall.
350
370
  * `fs.test.ts` pins the two implementations as equivalent.
351
371
  *
352
- * The entry normalisation stays: `pathToCheck` reaches here straight from a
372
+ * The entry normalization stays: `pathToCheck` reaches here straight from a
353
373
  * URL pathname in `router.ts`, before any containment check, and collapsing
354
374
  * its dot segments is what makes the rest of the walk safe.
355
375
  *
356
- * The remaining `existsSync` per level is deliberately **not** memoised
376
+ * The remaining `existsSync` per level is deliberately **not** memoized
357
377
  * *across requests*, which is the opposite call to the one `getMimeType`
358
- * below makes. The difference is not the key — both are ultimately derived
378
+ * below makes. The difference is not the key: both are ultimately derived
359
379
  * from a request path and both would need an `LRUCache` to stay bounded
360
- * under convention 6 — it is what a stale entry costs. A stale MIME type
380
+ * under convention 6: it is what a stale entry costs. A stale MIME type
361
381
  * serves the wrong `Content-Type`; a stale *negative* here serves a file an
362
382
  * operator has marked forbidden, which is the exact outcome the marker
363
383
  * exists to prevent.
@@ -367,7 +387,7 @@ export namespace FileSystem {
367
387
  * a cached "not forbidden" would persist for the life of the process; a TTL
368
388
  * would only bound that window, not close it, and an operator dropping a
369
389
  * `.forbidden` file to pull content offline has no way to learn how long the
370
- * window is. The LRU also fails exactly when it would matter — a client
390
+ * window is. The LRU also fails exactly when it would matter: a client
371
391
  * requesting varied paths evicts the useful upper-level entries, so the
372
392
  * hit rate collapses under the load that motivated caching in the first
373
393
  * place. `fs.test.ts` has a regression test that fails if this is added.
@@ -376,8 +396,8 @@ export namespace FileSystem {
376
396
  * 4-6x per request over overlapping subtrees (router, `getStatic`, every
377
397
  * level of `getRoute`, the dynamic-route cache), each walk re-stating the
378
398
  * same directories at ~50us per miss on Bun/Windows. A marker dropped
379
- * *mid-request* has no meaningful semantics — the operator's guarantee is
380
- * "the next request sees it" — so the raw probe results live on the current
399
+ * *mid-request* has no meaningful semantics (the operator's guarantee is
400
+ * "the next request sees it"), so the raw probe results live on the current
381
401
  * `hostStore` value (`HostContext.forbiddenProbes`), which worker.ts creates
382
402
  * per request and never reuses. No store (tests, direct calls, startup)
383
403
  * means no caching, bit-for-bit today's behavior.
@@ -399,7 +419,7 @@ export namespace FileSystem {
399
419
  //
400
420
  // The walk below is bounded by `curr.startsWith(normalizedRoot)`, so an
401
421
  // out-of-root path skipped the loop body entirely and fell through to
402
- // `return false` — "not forbidden". That is a guard failing *open* on the
422
+ // `return false`, "not forbidden". That is a guard failing *open* on the
403
423
  // one input it most needs to refuse, and it made every caller that relied
404
424
  // on this for containment (all of them: `constants.ts` said so in as many
405
425
  // words) incapable of catching an escape.
@@ -409,12 +429,12 @@ export namespace FileSystem {
409
429
  // read the pattern as drive-absolute, ignored `cwd`, and matched files at
410
430
  // `C:\`. `getRoute` resolved one, asked this function, was told "allowed",
411
431
  // and returned an `Info` pointing six levels above the serve root. The glob
412
- // is fixed too, but the glob was only how it was reached — a resolved file
432
+ // is fixed too, but the glob was only how it was reached: a resolved file
413
433
  // outside the root has to be refused here whatever produced it.
414
434
  //
415
435
  // The separator suffix matters: a bare `startsWith` would also accept a
416
436
  // sibling directory whose name merely begins with the root's. A root that
417
- // is already a filesystem root ends in `/` and must not gain a second one —
437
+ // is already a filesystem root ends in `/` and must not gain a second one:
418
438
  // `C://` matches nothing, which would make every path under `C:/` read as
419
439
  // an escape. `fs.test.ts` drives both through its root corpus.
420
440
  const rootPrefix = normalizedRoot.endsWith('/')
@@ -452,19 +472,53 @@ export namespace FileSystem {
452
472
  return false
453
473
  }
454
474
 
455
- let probeForbidden: (path: string) => boolean = nodeExistsSync
475
+ /**
476
+ * `statSync` with ENOENT suppressed, not `existsSync`.
477
+ *
478
+ * Node's `existsSync` is a `stat` with a `try`/`catch` around it, and on
479
+ * Bun/Windows the throw-and-catch for a path that is not there is most of
480
+ * what it costs. Asking for the stat directly and reading `undefined` for a
481
+ * miss skips that. Measured on this machine, four interleaved rounds
482
+ * against a CPU-bound control that stayed flat at 27-30 ms:
483
+ *
484
+ * existsSync 30.00 us guarded statSync 17.52 us 1.71x
485
+ *
486
+ * A miss is the case that matters: almost no directory holds a `.forbidden`
487
+ * marker, so every level of every walk takes this path. At four unique
488
+ * levels per request that is 50 us, at six 75 us.
489
+ *
490
+ * The `catch` is load-bearing rather than defensive habit. `throwIfNoEntry:
491
+ * false` suppresses **ENOENT only**, so a directory the process cannot stat
492
+ * still throws EACCES or EPERM where `existsSync` answers false - and a
493
+ * throw here would escape through `isForbidden` into the request. Answering
494
+ * false matches the behavior this replaces exactly; the two forms agree on
495
+ * a missing marker, a present one, a marker that is a *directory*, a path
496
+ * several levels below anything that exists, a directory, and the empty
497
+ * string.
498
+ */
499
+ function statProbe(path: string): boolean {
500
+ try {
501
+ return nodeStatSync(path, { throwIfNoEntry: false }) !== undefined
502
+ } catch {
503
+ // See above: a stat that fails for any reason other than absence is
504
+ // reported as absent, which is what `existsSync` did.
505
+ return false
506
+ }
507
+ }
508
+
509
+ let probeForbidden: (path: string) => boolean = statProbe
456
510
 
457
511
  /**
458
512
  * Test seam (convention 9): lets `fs.test.ts` count the syscalls the
459
513
  * per-request dedup above is supposed to be saving, without module-mocking
460
- * `node:fs` — which is process-global and never unwinds.
514
+ * `node:fs`, which is process-global and never unwinds.
461
515
  */
462
516
  export function __setForbiddenProbe(fn: (path: string) => boolean) {
463
517
  probeForbidden = fn
464
518
  }
465
519
 
466
520
  export function __resetForbiddenProbe() {
467
- probeForbidden = nodeExistsSync
521
+ probeForbidden = statProbe
468
522
  }
469
523
 
470
524
  export async function mkdir(path: string) {
@@ -508,6 +562,10 @@ export namespace FileSystem {
508
562
  return compressable.has(cleanExt)
509
563
  }
510
564
 
565
+ /**
566
+ * Two reads of one `BunFile`, which is one stat: `exists` no longer
567
+ * re-wraps the instance, so the second read comes off the cached stat.
568
+ */
511
569
  function validCache(file: Bun.BunFile, sourceMtime: number | null): boolean {
512
570
  return exists(file) && (!sourceMtime || sourceMtime <= file.lastModified)
513
571
  }
@@ -521,11 +579,11 @@ export namespace FileSystem {
521
579
  * so N concurrent first-hits on a cold cache each saw "not cached", each ran
522
580
  * `compiler()`, and each wrote the same three paths. Measured: 8 concurrent
523
581
  * callers produced 8 compiler invocations. Every `.ts`/`.tsx`/`.vue` asset
524
- * goes through here, so a traffic burst on a cold cache — or a cluster whose
525
- * workers share one cache directory — paid the full transpile *plus* gzip
582
+ * goes through here, so a traffic burst on a cold cache (or a cluster whose
583
+ * workers share one cache directory) paid the full transpile *plus* gzip
526
584
  * *and* zstd once per concurrent request.
527
585
  *
528
- * Same memoised-promise idiom as `initDB`, `setupPlugins` and
586
+ * Same memoized-promise idiom as `initDB`, `setupPlugins` and
529
587
  * `getTranspiler`, keyed rather than singular, and deleted in a `finally` so
530
588
  * a failed build is retried by the next caller rather than remembered. It is
531
589
  * bounded by construction (convention 6): an entry exists only while its own
@@ -533,7 +591,7 @@ export namespace FileSystem {
533
591
  *
534
592
  * The key is the path alone, not the path plus `sourceMtime`. A caller that
535
593
  * joins mid-build can therefore receive a build started for a marginally
536
- * older mtime — a window only as wide as one compile, which the next request
594
+ * older mtime: a window only as wide as one compile, which the next request
537
595
  * re-validates and rebuilds. Including the mtime would close that window by
538
596
  * letting two builds of the same file race each other onto the same three
539
597
  * output paths, which is the worse trade.
@@ -590,7 +648,7 @@ export namespace FileSystem {
590
648
  if (content == null) return null
591
649
 
592
650
  // Only on the build path: a warm hit was paying this stat on every
593
- // request for nothing — the directory necessarily exists if the cached
651
+ // request for nothing, the directory necessarily exists if the cached
594
652
  // file it holds just validated.
595
653
  await mkdir(cacheDir)
596
654
 
@@ -617,7 +675,7 @@ export namespace FileSystem {
617
675
 
618
676
  // The mirror of the compressed branch's check above. Without it a
619
677
  // non-compressible entry (a .woff2 from the gstatic proxy, say) was
620
- // rebuilt — for fonts, re-fetched upstream — on every single request.
678
+ // rebuilt (for fonts, re-fetched upstream) on every single request.
621
679
  if (validCache(rawFile, sourceMtime)) return rawFile
622
680
 
623
681
  const content = await compiler()
@@ -629,8 +687,8 @@ export namespace FileSystem {
629
687
 
630
688
  /**
631
689
  * The extension→MIME mapping is static, but resolving it allocates a
632
- * `Bun.BunFile` every call — ~1.4us, on the path of every static-file and
633
- * compressed-asset response. Memoised behind an `LRUCache` rather than a
690
+ * `Bun.BunFile` every call, ~1.4us, on the path of every static-file and
691
+ * compressed-asset response. Memoized behind an `LRUCache` rather than a
634
692
  * plain Map because `ext` is ultimately derived from the request path, and
635
693
  * an unbounded map keyed on client-supplied input is exactly what
636
694
  * convention 6 forbids.