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