@bakery-framework/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +89 -0
  3. package/package.json +69 -0
  4. package/src/cache/index.ts +8 -0
  5. package/src/cache/lru.ts +41 -0
  6. package/src/cache/shared-db.ts +51 -0
  7. package/src/cache/string.ts +150 -0
  8. package/src/cache/tiered.ts +493 -0
  9. package/src/client/globals.d.ts +74 -0
  10. package/src/client/livereload.ts +437 -0
  11. package/src/client/utils.ts +315 -0
  12. package/src/compiler/compiler.ts +263 -0
  13. package/src/compiler/dev-service.ts +660 -0
  14. package/src/compiler/index.ts +2 -0
  15. package/src/compiler/prompt-tracker.ts +36 -0
  16. package/src/compiler/tsconfig-sync.ts +71 -0
  17. package/src/core/bakery.ts +96 -0
  18. package/src/core/cache-version.ts +119 -0
  19. package/src/core/config.ts +296 -0
  20. package/src/core/context.ts +121 -0
  21. package/src/core/index.ts +61 -0
  22. package/src/core/init.ts +90 -0
  23. package/src/core/jsx.ts +152 -0
  24. package/src/core/paths.ts +24 -0
  25. package/src/core/plugins.ts +120 -0
  26. package/src/core/port.ts +73 -0
  27. package/src/global.d.ts +374 -0
  28. package/src/handlers/assets/google-font.ts +225 -0
  29. package/src/handlers/assets/image.ts +136 -0
  30. package/src/handlers/assets/nm.ts +73 -0
  31. package/src/handlers/assets/public.ts +17 -0
  32. package/src/handlers/assets/static.ts +86 -0
  33. package/src/handlers/assets/ts.ts +61 -0
  34. package/src/handlers/assets/tsx.ts +106 -0
  35. package/src/handlers/assets/virtual-asset.ts +104 -0
  36. package/src/handlers/core/$base.ts +256 -0
  37. package/src/handlers/core/$dynamic.ts +285 -0
  38. package/src/handlers/core/$error.ts +301 -0
  39. package/src/handlers/core/$middleware.ts +71 -0
  40. package/src/handlers/core/$mounts.ts +84 -0
  41. package/src/handlers/core/$registry.ts +153 -0
  42. package/src/handlers/core/$routing.ts +205 -0
  43. package/src/handlers/core/$static.ts +100 -0
  44. package/src/handlers/core/$websocket.ts +52 -0
  45. package/src/handlers/index.ts +21 -0
  46. package/src/handlers/routes/api.ts +95 -0
  47. package/src/handlers/routes/html.ts +95 -0
  48. package/src/handlers/routes/livereload.ts +54 -0
  49. package/src/handlers/routes/proxy.ts +74 -0
  50. package/src/logger/clients.ts +12 -0
  51. package/src/logger/index.ts +3 -0
  52. package/src/logger/logger.ts +375 -0
  53. package/src/logger/serve-log.ts +206 -0
  54. package/src/plugins/index.ts +15 -0
  55. package/src/plugins/routes.ts +110 -0
  56. package/src/plugins/types.ts +19 -0
  57. package/src/router.ts +351 -0
  58. package/src/session.ts +556 -0
  59. package/src/shared.d.ts +63 -0
  60. package/src/startup.ts +154 -0
  61. package/src/types.d.ts +111 -0
  62. package/src/utils/common/case.ts +11 -0
  63. package/src/utils/common/index.ts +5 -0
  64. package/src/utils/common/json.ts +35 -0
  65. package/src/utils/common/match.ts +6 -0
  66. package/src/utils/common/misc.ts +53 -0
  67. package/src/utils/common/try.ts +6 -0
  68. package/src/utils/constants.ts +153 -0
  69. package/src/utils/fs.ts +621 -0
  70. package/src/utils/http/body.ts +65 -0
  71. package/src/utils/http/csrf.ts +111 -0
  72. package/src/utils/http/dom.ts +238 -0
  73. package/src/utils/http/escape.ts +8 -0
  74. package/src/utils/http/etag.ts +318 -0
  75. package/src/utils/http/html.ts +525 -0
  76. package/src/utils/http/index.ts +8 -0
  77. package/src/utils/http/ip.ts +32 -0
  78. package/src/utils/http/response.ts +129 -0
  79. package/src/utils/index.ts +4 -0
  80. package/src/utils/isomorphic/case.ts +52 -0
  81. package/src/utils/isomorphic/escape.ts +43 -0
  82. package/src/utils/isomorphic/index.ts +15 -0
  83. package/src/utils/isomorphic/is.ts +36 -0
  84. package/src/utils/isomorphic/match.ts +50 -0
  85. package/src/utils/isomorphic/math.ts +11 -0
  86. package/src/utils/isomorphic/misc.ts +22 -0
  87. package/src/utils/isomorphic/stringify.ts +42 -0
  88. package/src/utils/isomorphic/try.ts +94 -0
  89. package/src/utils/jsonc.ts +10 -0
  90. package/src/utils/shared-pool.ts +193 -0
  91. package/tsconfig.app.json +34 -0
@@ -0,0 +1,493 @@
1
+ import type { Statement } from 'bun:sqlite'
2
+ import { Bakery } from '../core/bakery'
3
+ import { Logger } from '../logger'
4
+ import { LRUCache } from './lru'
5
+ import { cacheDb as db } from './shared-db'
6
+
7
+ export { db }
8
+
9
+ export type Milliseconds = number & {}
10
+
11
+ export interface TieredCacheOptions<V> {
12
+ memoryThreshold: number
13
+ evictRatio?: number
14
+ flushInterval?: Milliseconds
15
+ reviver?: (data: any) => V
16
+ shouldPersist?: (value: V) => boolean
17
+ }
18
+
19
+ type CacheEntry<V> = { value: V; accessedAt: number }
20
+
21
+ interface DbRow {
22
+ key: string
23
+ value: string
24
+ accessedAt: number
25
+ }
26
+
27
+ interface DbCount {
28
+ count: number
29
+ }
30
+
31
+ type Flushable = { flushAllToDisk(): void; close(): void }
32
+ const registry: Flushable[] = []
33
+ export function registerCache(cache: Flushable): void {
34
+ registry.push(cache)
35
+ }
36
+
37
+ /**
38
+ * Test seam — see `__setTestDb` / `__setTestConfig` for the pattern.
39
+ *
40
+ * Detaches the live registry so a test can drive `flushAllCaches()` against its
41
+ * own caches without flushing and closing the process's real ones (`Strings`,
42
+ * the session tier), which every later test file in the run still needs.
43
+ * Returns what it removed; hand it back to `__restoreCaches`.
44
+ */
45
+ export function __detachCaches(): Flushable[] {
46
+ return registry.splice(0, registry.length)
47
+ }
48
+
49
+ export function __restoreCaches(caches: Flushable[]): void {
50
+ registry.length = 0
51
+ registry.push(...caches)
52
+ }
53
+
54
+ export class TieredCache<K extends string | number, V> {
55
+ private memoryStore = new Map<K, CacheEntry<V>>()
56
+ private dirtyKeys = new Set<K>()
57
+ private flushTimer?: ReturnType<typeof setInterval>
58
+ private opts: TieredCacheOptions<V>
59
+ private tableName: string
60
+ private stmt: Record<string, Statement>
61
+ private stmtCache = new LRUCache<string, Statement>(
62
+ import.meta.env.THREAD_WORKER ? 15 : 50,
63
+ // An evicted statement still holds a native handle; close() only finalizes
64
+ // whatever survived to the end.
65
+ (_sql, stmt) => {
66
+ try {
67
+ stmt.finalize()
68
+ } catch {
69
+ // Already finalized, or its database is closed. Either way the native
70
+ // handle this eviction exists to release is gone.
71
+ }
72
+ },
73
+ )
74
+
75
+ constructor(tableId: string, options: TieredCacheOptions<V>) {
76
+ this.tableName = tableId.replace(/[^a-zA-Z0-9_]/g, '')
77
+ const scaledThreshold = Math.max(
78
+ 50,
79
+ Math.floor(
80
+ options.memoryThreshold / (import.meta.env.THREAD_WORKER ? 4 : 1),
81
+ ),
82
+ )
83
+ this.opts = {
84
+ evictRatio: 0.1,
85
+ flushInterval: 10000,
86
+ ...options,
87
+ memoryThreshold: scaledThreshold,
88
+ }
89
+
90
+ db.run(`
91
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
92
+ key TEXT PRIMARY KEY,
93
+ value TEXT,
94
+ accessedAt INTEGER
95
+ )
96
+ `)
97
+
98
+ // prettier-ignore
99
+ this.stmt = {
100
+ insert: db.prepare(
101
+ `INSERT OR REPLACE INTO ${this.tableName} (key, value, accessedAt) VALUES (?, ?, ?)`,
102
+ ),
103
+ delete: db.prepare(`DELETE FROM ${this.tableName} WHERE key = ?`),
104
+ select: db.prepare(
105
+ `SELECT value, accessedAt FROM ${this.tableName} WHERE key = ?`,
106
+ ),
107
+ // `has` and `getAccessedAt` used `select`, which drags the whole JSON
108
+ // blob out of SQLite and marshals it into JS only to be thrown away.
109
+ // Projecting just what each needs is O(1) instead of O(value).
110
+ exists: db.prepare(
111
+ `SELECT 1 AS present FROM ${this.tableName} WHERE key = ?`,
112
+ ),
113
+ accessedAt: db.prepare(
114
+ `SELECT accessedAt FROM ${this.tableName} WHERE key = ?`,
115
+ ),
116
+ keys: db.prepare(`SELECT key FROM ${this.tableName}`),
117
+ all: db.prepare(`SELECT key, value FROM ${this.tableName}`),
118
+ prune: db.prepare(`DELETE FROM ${this.tableName} WHERE accessedAt < ?`),
119
+ expired: db.prepare(
120
+ `SELECT key FROM ${this.tableName} WHERE accessedAt < ?`,
121
+ ),
122
+ expiredParam: db.prepare(
123
+ `SELECT key FROM ${this.tableName} WHERE accessedAt < ? AND (json_array_length(value, ?) IS NULL OR json_array_length(value, ?) = 0)`,
124
+ ),
125
+ pruneParam: db.prepare(
126
+ `DELETE FROM ${this.tableName} WHERE accessedAt < ? AND (json_array_length(value, ?) IS NULL OR json_array_length(value, ?) = 0)`,
127
+ ),
128
+ count: db.prepare(`SELECT COUNT(*) as count FROM ${this.tableName}`),
129
+ }
130
+
131
+ if (this.opts.flushInterval !== undefined) {
132
+ this.flushTimer = setInterval(
133
+ () => this.flushToDisk(),
134
+ this.opts.flushInterval,
135
+ )
136
+ }
137
+
138
+ registerCache(this)
139
+ }
140
+
141
+ get count(): number {
142
+ return (this.stmt.count.get() as DbCount | null)?.count ?? 0
143
+ }
144
+
145
+ get memorySize(): number {
146
+ return this.memoryStore.size
147
+ }
148
+
149
+ get isDirty(): boolean {
150
+ return this.dirtyKeys.size > 0
151
+ }
152
+
153
+ has(key: K): boolean {
154
+ if (this.memoryStore.has(key)) return true
155
+ return this.stmt.exists.get(String(key)) !== null
156
+ }
157
+
158
+ get(key: K): V | undefined {
159
+ if (this.memoryStore.has(key)) {
160
+ const entry = this.memoryStore.get(key)!
161
+ entry.accessedAt = Date.now()
162
+ this.memoryStore.delete(key)
163
+ this.memoryStore.set(key, entry)
164
+ return entry.value
165
+ }
166
+
167
+ const row = this.stmt.select.get(String(key)) as Pick<
168
+ DbRow,
169
+ 'value' | 'accessedAt'
170
+ > | null
171
+ if (!row) return undefined
172
+
173
+ const parsed = JSON.parse(row.value)
174
+ const value: V = this.opts.reviver ? this.opts.reviver(parsed) : parsed
175
+
176
+ this.memoryStore.delete(key)
177
+ this.memoryStore.set(key, { value, accessedAt: Date.now() })
178
+ this.dirtyKeys.delete(key)
179
+ this.enforceMemoryLimit()
180
+
181
+ return value
182
+ }
183
+
184
+ set(key: K, value: V): this {
185
+ this.memoryStore.delete(key)
186
+ this.memoryStore.set(key, { value, accessedAt: Date.now() })
187
+ this.dirtyKeys.add(key)
188
+ this.enforceMemoryLimit()
189
+ return this
190
+ }
191
+
192
+ delete(key: K): boolean {
193
+ const fromRAM = this.memoryStore.delete(key)
194
+ this.dirtyKeys.delete(key)
195
+ const info = this.stmt.delete.run(String(key))
196
+ return fromRAM || info.changes > 0
197
+ }
198
+
199
+ getAccessedAt(key: K): number | undefined {
200
+ if (this.memoryStore.has(key)) return this.memoryStore.get(key)!.accessedAt
201
+ return (
202
+ this.stmt.accessedAt.get(String(key)) as Pick<DbRow, 'accessedAt'> | null
203
+ )?.accessedAt
204
+ }
205
+
206
+ prune(maxAgeMs: number, ignoreJsonArrayPath?: string): number {
207
+ const cutoff = Date.now() - maxAgeMs
208
+
209
+ const expired: { key: string }[] = ignoreJsonArrayPath
210
+ ? (this.stmt.expiredParam.all(
211
+ cutoff,
212
+ ignoreJsonArrayPath,
213
+ ignoreJsonArrayPath,
214
+ ) as any)
215
+ : (this.stmt.expired.all(cutoff) as any)
216
+
217
+ for (const { key } of expired) {
218
+ this.memoryStore.delete(key as K)
219
+ this.dirtyKeys.delete(key as K)
220
+ }
221
+
222
+ const info = ignoreJsonArrayPath
223
+ ? this.stmt.pruneParam.run(
224
+ cutoff,
225
+ ignoreJsonArrayPath,
226
+ ignoreJsonArrayPath,
227
+ )
228
+ : this.stmt.prune.run(cutoff)
229
+
230
+ return info.changes
231
+ }
232
+
233
+ exceedsMemoryLimit(): boolean {
234
+ return this.memoryStore.size > this.opts.memoryThreshold
235
+ }
236
+
237
+ *keys(): IterableIterator<K> {
238
+ const seen = new Set<K>()
239
+ for (const key of this.memoryStore.keys()) {
240
+ yield key
241
+ seen.add(key)
242
+ }
243
+ for (const row of this.stmt.keys.iterate() as IterableIterator<{
244
+ key: string
245
+ }>) {
246
+ const key = row.key as K
247
+ if (!seen.has(key)) yield key
248
+ }
249
+ }
250
+
251
+ *values(): IterableIterator<V> {
252
+ const seen = new Set<K>()
253
+ for (const [key, entry] of this.memoryStore) {
254
+ yield entry.value
255
+ seen.add(key)
256
+ }
257
+ for (const row of this.stmt.all.iterate() as IterableIterator<DbRow>) {
258
+ if (seen.has(row.key as K)) continue
259
+ try {
260
+ const parsed = JSON.parse(row.value)
261
+ yield this.opts.reviver ? this.opts.reviver(parsed) : parsed
262
+ } catch {
263
+ // Skip a corrupt row rather than aborting iteration over the rest of
264
+ // the cache. The cache is disposable; one bad entry is not worth a throw.
265
+ }
266
+ }
267
+ }
268
+
269
+ *entries(): IterableIterator<[K, V]> {
270
+ const seen = new Set<K>()
271
+ for (const [key, entry] of this.memoryStore) {
272
+ yield [key, entry.value]
273
+ seen.add(key)
274
+ }
275
+ for (const row of this.stmt.all.iterate() as IterableIterator<DbRow>) {
276
+ if (seen.has(row.key as K)) continue
277
+ try {
278
+ const parsed = JSON.parse(row.value)
279
+ const value: V = this.opts.reviver ? this.opts.reviver(parsed) : parsed
280
+ yield [row.key as K, value]
281
+ } catch {
282
+ // As above: a corrupt row is skipped, not fatal to the iteration.
283
+ }
284
+ }
285
+ }
286
+
287
+ [Symbol.iterator](): IterableIterator<[K, V]> {
288
+ return this.entries()
289
+ }
290
+
291
+ private getOrPrepareStmt(sql: string): Statement {
292
+ let stmt = this.stmtCache.get(sql)
293
+ if (!stmt) {
294
+ stmt = db.prepare(sql)
295
+ this.stmtCache.set(sql, stmt)
296
+ }
297
+ return stmt
298
+ }
299
+ search(options: {
300
+ search?: string
301
+ page: number
302
+ pageSize: number
303
+ sortBy: string
304
+ sortOrder: 'ASC' | 'DESC'
305
+ }): {
306
+ rows: V[]
307
+ totalRows: number
308
+ page: number
309
+ pageSize: number
310
+ totalPages: number
311
+ } {
312
+ // 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
314
+ // looping over `stmt.insert` instead of `commitKey`, which is exactly the
315
+ // bypass `flushAllToDisk` documents below having already been fixed once.
316
+ // There is nothing search needs that the predicate would deny it: an entry
317
+ // the predicate rejects is one the 10s interval flush deletes anyway, so
318
+ // including it here only made the listing depend on which flush won the
319
+ // race.
320
+ this.flushToDisk()
321
+
322
+ const pattern = options.search ? `%${options.search}%` : ''
323
+ const where = pattern ? 'WHERE key LIKE ? OR LOWER(value) LIKE ?' : ''
324
+ const params: any[] = pattern ? [pattern, pattern] : []
325
+
326
+ let orderBy = ''
327
+ switch (options.sortBy) {
328
+ case 'id':
329
+ orderBy = 'ORDER BY key'
330
+ break
331
+ case 'keys':
332
+ orderBy = `ORDER BY json_array_length(value, '$.persistKeys')`
333
+ break
334
+ case 'created':
335
+ case 'accessed':
336
+ case 'match':
337
+ orderBy = 'ORDER BY accessedAt'
338
+ break
339
+ default:
340
+ orderBy = 'ORDER BY accessedAt'
341
+ }
342
+
343
+ const dir = options.sortOrder === 'ASC' ? 'ASC' : 'DESC'
344
+ orderBy += ` ${dir}, key ${dir}`
345
+
346
+ const countStmt = this.getOrPrepareStmt(
347
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${where}`,
348
+ )
349
+ const totalRows = (countStmt.get(...params) as DbCount | null)?.count ?? 0
350
+
351
+ const totalPages = Math.max(1, Math.ceil(totalRows / options.pageSize))
352
+ const page = Math.min(options.page, totalPages)
353
+ const offset = Math.max(0, (page - 1) * options.pageSize)
354
+
355
+ const dataStmt = this.getOrPrepareStmt(
356
+ `SELECT value FROM ${this.tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`,
357
+ )
358
+ const rows = dataStmt.all(...params, options.pageSize, offset) as Pick<
359
+ DbRow,
360
+ 'value'
361
+ >[]
362
+
363
+ return {
364
+ rows: rows.map(r => {
365
+ const parsed = JSON.parse(r.value)
366
+ return this.opts.reviver ? this.opts.reviver(parsed) : parsed
367
+ }),
368
+ totalRows,
369
+ page,
370
+ pageSize: options.pageSize,
371
+ totalPages,
372
+ }
373
+ }
374
+
375
+ destroyMemoryAndFlush(): void {
376
+ this.flushToDisk()
377
+ this.memoryStore.clear()
378
+ }
379
+
380
+ flushToDisk(): void {
381
+ if (this.dirtyKeys.size === 0) return
382
+ const keys = new Set(this.dirtyKeys)
383
+ this.dirtyKeys.clear()
384
+ db.transaction(() => {
385
+ for (const key of keys) this.commitKey(key)
386
+ })()
387
+ }
388
+
389
+ flushAllToDisk(): void {
390
+ if (this.memoryStore.size === 0) return
391
+ db.transaction(() => {
392
+ // Through `commitKey`, not a raw insert. This is the shutdown flush, and
393
+ // it used to ignore `shouldPersist` — so the interval flush and eviction
394
+ // dropped a non-persisting entry while a clean stop wrote it. For the
395
+ // session tier that predicate is "has persisted keys or has data", so
396
+ // every shutdown wrote up to a thousand empty sessions, which then
397
+ // inflated `Session.count` (read by the dashboard and the analytics
398
+ // gauge) until the 15-minute pruner caught up. `search()` had a third
399
+ // copy of this loop with the same bypass; it now calls `flushToDisk`.
400
+ for (const key of this.memoryStore.keys()) this.commitKey(key)
401
+ this.dirtyKeys.clear()
402
+ })()
403
+ }
404
+
405
+ close(): void {
406
+ clearInterval(this.flushTimer)
407
+ this.flushTimer = undefined
408
+ for (const stmt of this.stmtCache.values()) stmt.finalize()
409
+ this.stmtCache.clear()
410
+ }
411
+
412
+ [Symbol.dispose](): void {
413
+ this.close()
414
+ }
415
+
416
+ private commitKey(key: K): void {
417
+ const entry = this.memoryStore.get(key)
418
+ if (!entry) return
419
+ // `shouldPersist?.(v)` is `undefined` when the option is absent, and
420
+ // `!undefined` is `true` — so a cache configured without a predicate took
421
+ // the delete branch for every key. Since `enforceMemoryLimit` commits as
422
+ // it evicts, that made eviction destroy the value rather than demote it
423
+ // to disk. Absence of the option means "persist everything".
424
+ const shouldPersist = this.opts.shouldPersist
425
+ if (shouldPersist && !shouldPersist(entry.value)) {
426
+ this.stmt.delete.run(String(key))
427
+ } else {
428
+ this.stmt.insert.run(
429
+ String(key),
430
+ JSON.stringify(entry.value),
431
+ entry.accessedAt,
432
+ )
433
+ }
434
+ }
435
+
436
+ private enforceMemoryLimit(): void {
437
+ if (!this.exceedsMemoryLimit()) return
438
+
439
+ const count = Math.floor(
440
+ this.opts.memoryThreshold * (this.opts.evictRatio ?? 0.1),
441
+ )
442
+ const toEvict: K[] = []
443
+ for (const key of this.memoryStore.keys()) {
444
+ if (toEvict.length >= count) break
445
+ toEvict.push(key)
446
+ }
447
+
448
+ db.transaction(() => {
449
+ for (const key of toEvict) {
450
+ this.commitKey(key)
451
+ this.memoryStore.delete(key)
452
+ this.dirtyKeys.delete(key)
453
+ }
454
+ })()
455
+ }
456
+ }
457
+
458
+ const logger = new Logger('tiered-cache')
459
+
460
+ /**
461
+ * Flush every registered cache to disk and release its timers and statements.
462
+ *
463
+ * Registered as a `Bakery.shutdownHooks` entry, which runs *before*
464
+ * `PluginHooks.onShutdown()`. It deliberately does **not** close the shared
465
+ * database: this hook is registered at module-evaluation time and therefore
466
+ * sits at index 0, while a plugin's shutdown hook is registered later during
467
+ * 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
469
+ * every statement threw into an outer catch and up to a minute of page hits
470
+ * plus the whole history delta was discarded on every clean stop, silently.
471
+ *
472
+ * `closeCacheDb()` is the separate final step; `runShutdownSequence()` in the
473
+ * CLI calls it after the plugin hooks.
474
+ */
475
+ export function flushAllCaches(): void {
476
+ logger.log('Flushing caches and shutting down...', 'info')
477
+ for (const cache of registry) {
478
+ cache.flushAllToDisk()
479
+ cache.close()
480
+ }
481
+ }
482
+
483
+ let cacheDbClosed = false
484
+
485
+ /** Close the shared cache database. Must run after every writer is done. */
486
+ export function closeCacheDb(): void {
487
+ if (cacheDbClosed) return
488
+ cacheDbClosed = true
489
+ db.close()
490
+ logger.log('Sync complete. Database closed cleanly.', 'info')
491
+ }
492
+
493
+ Bakery.onShutdown(flushAllCaches)
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The browser globals, as types.
3
+ *
4
+ * Every name here is bound onto `globalThis` by `client/utils.ts`. This file
5
+ * used to *restate* each one's shape by hand, and four of them had drifted from
6
+ * the code they describe:
7
+ *
8
+ * - `Try.throw` was declared with a synchronous overload it never had
9
+ * (`tryThrow` is `Promise.try(...).catch(...)`, so it always returns a
10
+ * Promise) — browser code writing `const n: number = Try.throw(() => 1)`
11
+ * typechecked and silently received a Promise.
12
+ * - `Case` lost its callable form: the implementation is
13
+ * `Object.assign(function Case(type, str) {…}, {…})`, so `Case('kebab', s)`
14
+ * is real but was untypeable.
15
+ * - `request` was `(url, method?, body?)` against an implementation of
16
+ * `(url, init: RequestJson | string = {}, bodyData?)` — the object-init form
17
+ * was unreachable from the types.
18
+ * - `randomId` was `() => string` against an implementation taking `length = 8`.
19
+ *
20
+ * So the shapes are no longer restated: each global is a `typeof import(...)`
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 —
23
+ * it is erased, and adds no runtime module edge into the browser bundle.
24
+ *
25
+ * Two globals genuinely cannot be derived, and stay hand-written:
26
+ * `server` (injected into a Vue SFC's module scope by the compiler, so no
27
+ * module exports it) and `Bakery` (an object literal written inline inside
28
+ * `client/utils.ts`'s `Object.assign(globalThis, …)` call, so there is no
29
+ * exported member to query).
30
+ */
31
+ import type { MapOf } from '../types'
32
+
33
+ declare global {
34
+ /** Server-side exports from `<script server>` — available at runtime in `<script setup>` and templates. */
35
+ const server: { [key: string]: any }
36
+
37
+ const matchDefault: typeof import('../utils/isomorphic/match').matchDefault
38
+ var match: typeof import('../utils/isomorphic/match').match
39
+ var is: typeof import('../utils/isomorphic/is').is
40
+ var Try: typeof import('../utils/isomorphic/try').Try
41
+ var tryCatch: typeof import('../utils/isomorphic/try').tryCatch
42
+ var Case: typeof import('../utils/isomorphic/case').Case
43
+ var Math2: typeof import('../utils/isomorphic/math').Math2
44
+ var throws: typeof import('../utils/isomorphic/misc').throws
45
+ var assert: typeof import('../utils/isomorphic/misc').assert
46
+ var any: typeof import('../utils/isomorphic/misc').any
47
+ var repeat: typeof import('../utils/isomorphic/misc').repeat
48
+ var escapeHTML: typeof import('../utils/isomorphic/escape').escapeHtml
49
+
50
+ var request: typeof import('./utils').request
51
+ var randomId: typeof import('./utils').randomId
52
+
53
+ // Not derivable: written inline in client/utils.ts's Object.assign call
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,
56
+ // `virtual` is `async virtual(path: string)`, `params` is generic.
57
+ var Bakery: {
58
+ version: string
59
+ virtual(path: string): Promise<any>
60
+ params<T = MapOf<any>>(): T
61
+ }
62
+
63
+ // `ImportMeta.env` is declared once, by global.d.ts's ImportMetaEnv. This
64
+ // file used to redeclare it with an incompatible shape, and to carry
65
+ // verbatim copies of Wrapped / MapOf / JsonResponse / ISFunction — all four
66
+ // now live in shared.d.ts, which is where anything the browser and the
67
+ // server both need belongs.
68
+ }
69
+
70
+ // A `declare module '@client/utils'` block used to sit here re-exporting
71
+ // '../utils'. It never resolved (TS2307 — relative specifiers inside an
72
+ // ambient module declaration do not resolve from the containing file), and
73
+ // nothing in the codebase imports '@client/utils' from TypeScript: it is a
74
+ // runtime importMap entry the browser resolves, mapped to /_client/utils.js.