Roadmap
What's done, what's next, and where Bosia is headed.
Track what's done, what's next, and where we're headed. Current version: 1.2.9
bosia 1.2.7 (2026-10-17) — WebSockets
- 🟠 No WebSocket support:
Bun.servehad nowebsocket,fetchdroppedserver.+server.tsexportswebsocket; upgrade runs after hooks,ws.data=upgrade()result. - 🔴 GET skips CSRF, so a cross-site page could open a socket with the user's cookies.
checkUpgradeOrigin: foreign Origin → 403, no Origin (CLI/device) passes. - 🟡 Shutdown closes sockets with 1001; dev proxy connects to the app first (refusal = failed handshake, like prod), rebuild → 1012.
- 🟡
$typeswas generated only for page dirs;+server.ts-only dirs getParams+WebSocketHandlernow. Workers guard flagswebsocketexports. - 🟡 Review: one shared
UPGRADEDResponse let hooks mutate or hide it, and a hook's new Request broke the upgrade (501). Now a per-request AsyncLocalStorage flag. - 🟡 Review: an upgrade pending at shutdown got no 1001 → it opens, then closes 1001.
stop(true)cut drained responses; now a 500ms graceful stop, then forced (SSE can't hang it). - 🟡 Review: chained
X-Forwarded-Proto/-Host(https, http) gave a wrong origin → 403 on posts and upgrades. First value of each is used. - 🟡 Review: dev proxy threw on odd close codes and mapped client 1001 to 1000; HEAD on a websocket-only route got 405 with an empty
Allow. - 🟡 Review: route handlers sat on
ws.data, so one object from two routes (a cached user) ran the wrong handlers. Now a per-socket map. Origin check moved before the hooks. - ⚪ Review:
WS_*size errors namedBODY_SIZE_LIMITand called 0 "Infinity"; a requested subprotocol failed silently (now a warning). One redirect and one seconds parser. - 🟡 Review:
X-Forwarded-Proto: wss(Traefik) → 403;UpgradewithoutSec-WebSocket-Keyranupgrade(); upgrade context outlived its request; hook throw after 101 left the socket open. - 🟡 Review: forwarded host kept the inner port (
app.com:3000); a malformed one half-applied. One strictapplyForwarded()(proxy.ts); CSRF comparesurl.originonly. - 🔴 On
maintoo: page cache key was path + query only, buturlfollowsHost/X-Forwarded-*. A spoofed one poisoned pages renderingurl.origin. Key now hasurl.origin. - 🟡 Any
Host(or forwarded host) is accepted, so random hosts each get a cache entry and flood the LRU (same as random queries today). AddORIGIN/ALLOWED_HOSTSand reject the rest. - 🟡
X-Forwarded-Proto: constructorhit an inherited key ofSCHEMESand dropped a valid forwarded host. Now aMap(proxy.ts), as is cookies'sameSitetable (same bug). - ⚪ Review: a path with a raw
|(/files/a|b) was indexed as/files/a, soinvalidate("/files/a|b")missed it.pathOfKeycuts at?or|o=. - ⚪ Review: a data request's
_invalidatedmask went raw into the cache key (1?xbroke path invalidation). Only a0/1last value counts now; others run every loader (a page's own?_invalidated=cache-buster stays valid). - ⚪ Review: incoming cookies sat on a plain object, so
get("constructor")returnedObject. Now null-prototype;__proto__still dropped (serializers reject it),getAll()still plain. - ⚪ Repeated cookie names: last wins (server and client). Neither order stops cookie tossing; document
__Host-names for session cookies. - ⚪ Client
readDocumentCookies(loaderCache.ts) uses a plain object: a loader that readconstructorseesObject. Match the server (null prototype, drop__proto__). - ⚪
_invalidated=1,=11,=111…give the same response but separate cache/dedup entries. Key on the mask sized to the layout count. - ⚪ The path index uses the path as it arrived:
invalidate("/files/a|b")misses a request sent as/files/a%7Cb(same for/caf%C3%A9). Normalize the encoding on both sides. - ⚪ The loader mask shares the
_invalidatedname with the page's own query: a page's 0/1?_invalidated=is read as the mask, and loaders never see it. Move the mask to a header or__bosia_mask. - ⚪ The path index re-parses each cache key (
pathOfKey). Store the path on the entry if keys grow free-text parts. - ⚪
sameSite: "none"over plain HTTP (dev) is sent withoutSecure, so Chrome drops the cookie silently. Warn, or keepSecureforNone. - ⚪ Graceful shutdown is untested on Windows (no SIGTERM;
kill()terminates). CI skips that test there. Cover Ctrl+C (SIGINT) if Windows hosts matter. - ⚪ Dev proxy: a handshake in flight during a rebuild gets 502 (or 1011), not 1012. Needs an in-flight set mapped to 1012.
- ⚪ Not in v1: subprotocol selection (
protocolson the export), Workers (WebSocketPair + Durable Objects), dev-proxy backpressure and 101 cookies.
bosia 1.2.7 (2026-10-17) — goto resolves relative URLs
- 🟡
goto("?page=2"),goto("b"), same-origin full URLs, relativeuse:enhanceand loader redirects matched no route: full reload (since 0.5.10).router.navigateresolves them (toRouterPath). - 🟡 Review:
goto("//other.site/x")stayed a raw path; with a root catch-all route it matched andpushStatethrew. Other origins now go out as a full URL. - ⚪ Review: a root path skipped the URL parser, so
goto("/café")matchedslug = "café"(server:caf%C3%A9),/a/../band/\hostmatched raw. Every URL is parsed now. - 🔴 Review:
goto("javascript:…")(alsodata:,vbscript:) went towindow.locationand ran: XSS viagoto(params.get("next")).toRouterPaththrows on them likeredirect(). - 🟠 Review:
redirect()tested the raw string:java\tscript:,\x01javascript:passed,htt\tps://evil.comand/\evil.comcounted as relative. Parsed withnew URLnow; oneDANGEROUS_PROTOCOL(basePath.ts) for both sides. - 🟡 Review: same-origin
blob:URLs becamehttps://host/uuid(404); only http(s) counts as in-app. Error saysnavigation:, notgoto():. Docs say "rejects", skill no longer calls?next=safe. - 🔴 Review: parsing against the
http://bosia.invalidsentinel lethttp:evil.compass as a path (an https page sends it to http://evil.com), and the sentinel host itself. Relative now also means it fails to parse alone. - ⚪ Review: an unparseable location throws
invalid URL, withallowExternaltoo (was passed through). Security guide documents both. - 🔴 Review: a same-origin
//path (/.//evil.com) came out oftoRouterPathas//evil.com, read as a host: off-site. It stays a full URL now; link clicks passanchor.href,readDataResponsereturnsres.url. - 🔴 Review: popstate, hydration and the hash-link branch stored raw
location.pathname; Back onto//evil.com/xhard-loaded evil.com. All go throughlocationPath()now; hover prefetch skips//links. - 🔴 Review: on
maintoo, a catch-all route 308'd//evil.com/x/toLocation: //evil.com/x, and SSRpage.urlread evil.com. The server now 308s any//xpath to/xbefore routing. - 🟡 Review: hydrate's stale-chunk reload (raw pathname) and trailing-slash
replaceState(throws) on a//xpage fixed;buildTargettakes params fromnew URL, so a full-URL route keeps them. - 🟠 Review 8–10: app shells (
capacitor://) stay in-app: scheme + host match, empty path =/; client URLs resolve againstlocation.href(location.origincan be "null": threw); links take the page's scheme. - 🟡 Review 8: the
//308 used idempotentwithBase:/sso//sso/xwent to/sso/x.URL.canParse(no Safari 16) swapped for try/new URL. Stale reload appends_vas text (searchParams re-encoded the query), dropping the last one. - 🟡 Review 11:
_vstrip ended at the digits (?q=a&_v=12abc→?q=aabc); link clicks and hover prefetch check scheme + host like toRouterPath (origin "null" let other shell hosts in); hydration reads an empty path as/. - 🟡 Review 12: hover prefetch skips
rel="external"links (a/logoutloader ran on hover). Comments fixed (sandboxed iframes aren't "null"; renderer).redirect()scheme error no longer says same-origin URLs pass. - 🟡 Review 13:
file:pages (no host) counted every file as in-app (click intercepted, prefetch fetched over file://); a URL with no host is never ours.rel="External"matches case-insensitively. - 🟡 Review 14/15: one
appLinkPath(anchor)for click + prefetch (no-href<a>skipped); same-page, prefetch and hydrate comparelocationPath()._vdrops whole&parts;relsplits on ASCII space. - ⚪
goto("settings")resolves againstlocation.href, links againstdocument.baseURI: with<base href="/app/">in app.html they disagree (main reloaded, so the browser used the base). Resolve against baseURI, compare host with the page. - ⚪ The
//x308 (server.ts) returns before CORS, hooks andfinalizeResponse: no security headers, a cross-origin preflight to//api/xfails. Same as the base 404 and 413 next to it. - ⚪ Trailing-slash 308 (server.ts) and SSR route (renderer.ts) use idempotent
withBase: under/sso, app route/sso/x/308s to/sso/x(app/x). Alsoredirect(303, "/sso/x")(errors.ts). Needs abase + pathhelper. - ⚪ Prefetch is keyed
pathname + search, consumed bycurrentRoute(hash, canonical slash):/docs#introor/about/links fetch twice. - ⚪
goto("")on/a#xdrops the hash: a new history entry and a scroll to top. - 🟡 A loader redirect
navigate()refuses (hookResponsewith a bad/javascript:Location) throws in App.svelte's redirect branch:.catchreleases waiters, but logs an uncaught error and the address bar keeps the new URL over the old page. - ⚪
redirect(" /x")validates trimmed but rebases/sends untrimmed: underBASE_PATHit skips the prefix. Send the trimmed value. - ⚪
redirect()gives//evil"protocol-relative" but/\evil"external URL"; the raw//check could go.DANGEROUS_PROTOCOLsits in basePath.ts;toRouterPathparsespageHreftwice. - ⚪ A relative
use:enhanceredirect ("profile") resolves against the current page; HTTP resolves it against the form's action URL (/account/settings→/account/profile).
bosia 1.2.6 (2026-10-16) — templates type-check under isolated installs
- 🟡 Owner check runs again ignoring case:
/blog/[slug]←Newnext to liveblog/newfails (a macOS/Windows host would serveblog/New.htmlat/blog/new). - 🟡 Review: case pass is a rule, not first-match: a route spelling a static part in another case clashes if it wins a spelling (ranks higher, or the spelling misses this route). Params-only overlaps allowed.
- 🟡 Review 4: exact owner is the server's
findMatchagain (one pass had swapped it for a second matcher); case pass walks routes once, part count first;matchPartsFoldedin matcher.ts. - ⚪ Review 3: error names the spelling at risk (
would also answer /BLOG/NEW … which /[x]/NEW serves); no case report when the target's own route doesn't answer it. - ⚪ Review 4: NFC folding taken out (NTFS doesn't merge forms; the ❌ showed two same-looking names); route names lowercased once; case scan stops at its first hit. Still a build error on every host.
- 🟡 Review 5:
ß(→SS) no longer counts as having a case (false failure); spellings keep each name's length; an API slug is checked against live pages too (/API/ALL). - ⚪ Review 5:
matchPartsFoldedmoved out of runtime matcher.ts into prerender.ts; if it can't find a route findMatch matched, the build throws an internal error instead of skipping the case check. - ⚪ "Only a last
[...rest]takes several parts" lives in compilePattern,matchPartsFoldedand the part-count filter; one segment parser would serve all (with the shared route-order helper). - ⚪ Case check walks every route per target; bucket route shapes by part count once (rest routes apart) if a big
entries()build gets slow. - ⚪ Case check folds with
toLowerCaseonly: non-ASCII route names (ΟΔΟΣ/οδοσ,ſ, NFD forms) aren't compared; add a real case-fold table if anyone uses them. - 🟡 All 4 template tsconfigs used
"types": ["bun-types"], a transitive dep of@types/bun; isolated installs (workspaces, pnpm) failedcheckwith TS2688. Now"types": ["bun"].
Unreleased (2026-10-15) — perf audit fixes
Load test (4 vCPU sandbox, autocannon 50 conns): cached SSR ~19k rps, prerendered ~23k, API hit ~20k vs raw
Bun.serve~41k. A 200MB file from a+server.tsGET grew RSS by ~300MB; a tap during a hover prefetch sent the data request twice.
- 🔴 API cache read the whole cloned body (
arrayBuffer()) before its size check: a big file from a+server.tsGET sat in RAM per download, coalesced waiters waited the whole download, and an endless non-SSE stream held the miss gate forever.readBodyForCachestops at the cap, cancels the copy, gives up after 10s, and encodes string chunks. - 🟠 SSR cached pages whose loader
setHeaders({ "cache-control": "private" | "no-store" | "no-cache" }); the data endpoint and API already skipped them. OneisNoStoreCacheControlfor all three. - 🟠 CORS replaced
Vary: Accept-EncodingwithVary: Origin, so a CDN could send brotli to a client that didn't ask.Originis merged in now; a loader'ssetHeaders({ vary })no longer replaces it either (varyWithEncoding), and cached API hits keep the handler's ownVary. - 🟠 A
Varythe key can't honor (anything but Accept-Encoding:Cookie, a handler's ownOrigin,Accept-Language,*) skips the cache on SSR, data and API (varyBlocksCache). An API response the handler already encoded (Content-Encoding) isn't cached: hits replayed it as plain bytes. - 🟠 Response cache had only an entry cap (500 × up to 2MB × raw+gzip+brotli). New
CACHE_MAX_BYTES(100MB,0= count only) evicts oldest by bytes; bodies that can't fit skip compression. - 🟠 A click while its hover/touch prefetch was in flight sent a second
/__bosia/datarequest. The click now waits for the in-flight prefetch and gets what it said (data, redirect, error; fetches only on a network failure); a navigation drops older in-flight prefetches. Prefetches aren't used when the nav isfresh(dirty keys/URLs too, not just a tick); redirects and errors aren't kept for later clicks; a prefetch is used only when its_invalidatedmask equals the navigation's (the loader cache may have changed since the hover). - 🟡 The API miss gate is held while the cache copy is read, up to 10s for a slow small stream (was: until the stream ended). Waiters then run the handler anyway. Release earlier, or skip caching long-lived content types.
- ⚪ One body may take at most a quarter of
CACHE_MAX_BYTES(also withCACHE_MAX_BODY_BYTES=0), so raw + two compressed copies fit and one entry can't flush the cache. - ⚪
mergeVary(cors.ts) andvaryWithEncoding(html.ts) share logic;getCorsHeadersstill returnsVarythatfinalizeResponseskips. - ⚪ A re-hover right after a navigation re-sends a prefetch whose earlier fetch was dropped by
clearPrefetches(). - 🟠 Verified 2026-10-15: with
LOAD_TIMEOUTunset (= no timeout; the 5000 fallback was only for invalid values), one hung loader broke that exact URL until restart: the leader never released the miss gate, later requests waited on it, and clients sawECONNRESETat ~12s fromIDLE_TIMEOUT. NowLOAD_TIMEOUT/METADATA_TIMEOUTdefault to 30000/10000 (5000/3000 failed healthy pages: a page's timer coversawait parent()), parsed strictly (10swas 10ms). NewCACHE_WAIT_TIMEOUT(max(60000, metadata + load + 10s), warns when set lower; one timer per gate, counted from the leader's start) bounds waiters on any leader (a hung API handler,LOAD_TIMEOUT=0). Waiters of an expired gate (flagged by its timer, so no clock re-check spin) re-claim once (claimMiss, shared by SSR/data/API): the first replaces the gate as astaleleader and starts a new data-endpointdedupcall, replacing only a call that began within 1s of the stuck gate (a fractional-ms age check alone let 19/20 join the stuck one); the rest wait on it. After a second expiry a waiter builds alone; one whose client left returns 499 without building. Masked-out layouts that read an ignored param mark every layer sent below them (and the page)queryAware. Waiting uses one timer per waiter (gate anddedupjoin): on Workers a timer dies with the request that set it. Expired gates of keys that never come back are swept past 2× the timeout once 256+ are open.dedupjoiners give up atmaxAgeMstoo (_freshandcache = falserequests skip the gate). The derived default is clamped to 2^31-1. - ⚪ A hung loader still gets
ECONNRESETat ~12s (IDLE_TIMEOUT10) before the 30s timeout's 500.MAX_INFLIGHTis unbounded by default; a timed-out loader keeps running in the background (noAbortSignalfor loaders). - 🟡 Verified 2026-10-15: coalesce waiters stampede when the leader skips the write (error, cookie, too big). 100 concurrent SSR requests to a loader that fails after 1s: 100 loader runs (1 + 99 at once after the leader fails), and 99 responses take 2s instead of 1s. The data endpoint shares the result through
dedup: 2 runs, but the same doubled latency. Fix: share the leader's outcome (incl. errors) with its waiters, or hand leadership to one waiter. - 🟡 Viewport prefetch has no concurrency cap / idle scheduling /
saveDatacheck; nav clears results, and unobserved links never prefetch again. - 🟡 Superseded navigations keep their data fetch running (no
AbortController). - 🟡 Template
hooks.server.ts(demo/shop/store)console.logged every request: on a piped stdout (docker logs, systemd) +14µs CPU per request (25 → 39µs; to a file0). Removed from templates, demo apps and docs examples. New opt-in870 of 3000 lines lost on a slow pipe, both); console.log lost none. Pipe CPU: none 21µs, console.log per request 32µs, plugin 24µs.bosia/plugins/access-log: batched writes (100ms / 64KB, flush on exit) ≈ +2µs,filter/format/write/flushMs; Workers writes each line via console.log. Logs from a newBosiaApp.onResponsehook (final response, once per request, observe-only, throws swallowed, also when an error hook throws), so plugin order can't skip, duplicate or mis-status a line. Oneconsole.logper batch:process.stdout.writelost its queue at exit andfs.writeSync(1)did partial writes on Bun's non-blocking stdout ( - ⚪ access-log on Workers: no
ctx.waitUntilplumbing, so an asyncwritecan be cut off and batching is off (one console.log per request);performance.now()is frozen during CPU there, somsreads 0. - 🟠 Query strings fragment the cache. Every query value was its own entry, including ones that never change the page (
utm_*,fbclid,gclidfrom ads and social links). Measured:/blogcached19k rps vs12×). New/blog?q=<random>~1.6k rps (CACHE_IGNORE_PARAMS(default:utm_*and common click ids;refleft out, it's often a referral code): SSR and data requests read the stripped URL's entry first, then their own full-URL entry as before; they never write the stripped one (a component may renderpage.url). An entry isqueryAwarewhen a loader ormetadata()read an ignored param or the whole query (search,href, iteratingsearchParams) and isn't served to such requests. API keys and dedup keep the full URL. - 🟡 Pre-existing: a build that started before an
invalidate()and finishes after it still writes its (pre-change) result; most visible with a slow API handler. A global or per-entry invalidation sequence was tried (review rounds 6–8) and dropped: it emptied the cache for apps that invalidate on every POST and needed a bounded log. Revisit with per-tag versions stored on the entry. - 🟡 With
METADATA_TIMEOUTnow on by default, a page whosemetadata()failed or timed out was cached without its title/meta/canonical for everyone. Such responses are served but not cached (SSR and data). - 🟡 Prerender would write a page whose
metadata()timed out (now 10s by default) without its title/meta. Resolved by 1.2.5: the prerender server runs withLOAD_TIMEOUT/METADATA_TIMEOUT=Infinity, which the new parser still reads as off. - ⚪ A loader
cache-control: s-maxagestill lets a CDN keep a response whosemetadata()failed. - ⚪ Tracking params: with no usable plain entry (evicted, or
queryAware— e.g.metadata()buildscanonical: url.href), each uniquefbclid/gclidstill writes a single-use entry and churns the LRU. Skipping writes for tracked URLs would undo the repeated-newsletter-link case; maybe skip only for click-id params (*clid), keeputm_*. - ⚪ Slow loaders:
LOAD_TIMEOUT(30s) andCACHE_WAIT_TIMEOUT(60s) outlast Bun's defaultIDLE_TIMEOUT(10s), so the client of a 15s loader is cut off before either applies; documented, raiseIDLE_TIMEOUTtoo. - 🟠 Client nav skipped a loader that read the whole query (
url.search,Object.fromEntries(url.searchParams)):?page=1→2kept page 1. Deps now carry a"*"search param; the client compares the full query. - 🟠 Review: the
cacheregex scan missed export forms (destructuring,export let cache;,;in a string), leaking per-user pages. Scan removed;routeCacheFlagreads the real exports each request. - 🟡
// export const prerender = trueprerendered the page. Theprerendercheck now skips comments (one regex, notBun.Transpiler: it runs macros);ssr/actionsstill read raw source (fail-safe). - ⚪
stripCommentsdoesn't know regex literals or${}quotes:/'/before a// export const prerender = truestill prerenders. Gone: the analyse step readsmod.prerender. - ⚪ Serializing the
urlproxy afterload()returns (return { link: url },console.log(url)) callstoJSON/toStringand adds"*"to deps late; server and client may disagree. - 🟡 Review: client nav compared only the first value of a tracked param, so
getAll("tag")/has("tag", "b")kept old data on?tag=a&tag=b→?tag=a&tag=c. The snapshot now holds every value. - 🟡
export const cache = falsein+layout.server.ts/+layout.sveltewas ignored.routeCacheFlagnow reads every layout module too: a layout opt-out covers all pages below it. - 🟡 Client loader cache didn't track
parent(). Deps now carryuses_parent;maskBitsFor(shared by nav + prefetch) re-runs aparent()caller when a layout above it re-runs. - 🟡
metadata()deps stayed on the server: aload()readingmetadatakept old data (title/meta were fine).metadata()now gets tracked params/url/cookies/fetch, merged into the page deps when read. - ⚪ A
metadata()that timed out keeps running; reads it makes afterload()readmetadatamiss the page deps. - ⚪ The
metadatagetter merges deps on any read, so{...event}or logging the event re-runsload()on metadata()'s deps too (extra re-runs, never stale). - 🟡 Prerender/ssr/actions/trailingSlash were read by regex (missed
export { p as prerender },let). NewanalyseRoutes()bundles + imports route server modules; a throw fails the build. - ⚪ Dev re-reads flags only when a route server file's mtime moves, so a flag imported from
src/libstays stale until that file is saved or dev restarts (documented). Key on the bundle's inputs (content hash) instead. - 🟡 Review: dynamic
entries()imported the raw source (no$env, top-level code ran twice). Prerender now uses the module analyse loaded. - 🟡 Review: analyse hardening — 30s import timeout; deps bundled like the server (no NODE_PATH gap); dev reuses exports by mtime; build-wide unhandledRejection names the cause.
- ⚪ Review: analyse bundle errors keep file:line (
Bun.inspect); skip warnings are prod-only; Tailwind starts before analyse. - 🟡 Review 2: analyse used a raw path +
?v=(Windows; Bun ignores the query on file URLs) → file URL in a numbered dir per run. Dev has its own dir/cache, warns on a throw. - 🟡 Review 2: dev cache stats before bundling (no lost saves), re-imports only changed files; rejection hook only while modules load; Tailwind killed on early exit.
- 🔴 Review 3:
entries()sawimport.meta.dirunder.bosia/analyse. A source-path rewrite broke strings and misseddist/server; dropped. Both point at the bundle; docs say useprocess.cwd(). - ⚪ Review 3: dev wipes the analyse cache on start; cached manifest is dev-only; Tailwind killed on any exit (process
exithook); prod skips mtime stats; sharedROUTES_DIR/reason. - 🟠 Review 4: a refused top-level
db.connect()rejected after the hook was gone → build-wide handler fails with a message; prod warns onimport.meta.dir/urlin route files. - 🟡 Review 4:
.envedits re-read dev flags; a file deleted mid-rebuild no longer crashes dev; docs: liveload()must not readsrc/(images shipdist/only). - ⚪ A rejection after the analyse imports (late
db.connect(), timer) fails the build without naming the route. Fixed by the subprocess item. - 🟠 Review 5: late-rejection handler exited mid-build (dev too; prerender server orphaned) → dev warns, build fails at the end. A failed dev import isn't retried until saved (no 30s per rebuild).
- 🟡 Review 5: dev re-reads all flags via
BOSIA_ANALYSE_FRESH(start,.env), not a cache wipe a running build could undo; warning also catchesimport.meta.path/filename. - 🔴 Review 6: analyse after
postScanclobbered plugin-set flags and bundled plugin routes outside src/routes → back beforepostScan; prerender raw-imports unanalysed (plugin) routes. - 🟡 Review 6: only a timed-out dev import waits for a save (a throw retries);
freshkeeps last flags as fallback and forces a scan; late rejection checked before prerender + postBuild. - 🔴 Review 7: postScan-added routes read
prerenderfrom their module again; analyse stubsbosia:routes; a failed fresh dev build re-arms only if analyse didn't finish. - 🟠 Review 7: plugin docs: analyse runs before postScan (rejection stack attribution reverted in review 8).
- 🟠 Review 8: plugin routes read
prerenderfrom source text again (no unbundled import); dev warns on any rejection with its stack; fresh-read throws retried; fresh re-arm via a marker file. - 🟠 Review 9: postScan routes get
prerenderset (by route, not file) before routes.ts is written: clones of a scanned route prerender; client sees them as static. - 🟠 Review 10: one pass after postScan: clones reuse analyse's exports, a plugin's
truegets the ssr/actions check, no server file = not static, trailingSlash defaults to never. - 🟡 Review 11: one
flagPage()for analyse and post-postScan (clones get file trailingSlash); swapped files re-checked (APIs too); one skip warning per file; docs. - 🟡 Analyse the files postScan adds (bundle root = project dir; plugin files can sit outside src/routes), then
flagPage()every touched route. Removes the text check + snapshot rules and fixes:- Plugin-only files judged by regex: miss
let/re-exports/trailingSlash; a stray quote fools stripComments; an added route can't opt out of a file'sprerender = true. - Touched routes keep a stale
trailingSlash: a swapped server file, a clone moved under new layouts, or a plugin-added layout server (layout changes aren't re-checked). - Dev cached-manifest rebuilds skip analyse, so clones fall back to the regex and their flags can flip between rebuilds. Or: seed
analysedExportsfrom the dev cache. - One skip warning per file names only the first route; other clones are dropped silently. List all patterns in that one warning.
- Plugin-only files judged by regex: miss
- ⚪ The
bosia:routesstub is empty during analyse, and prerender callsentries()on those modules: anentries()built from the route table lists nothing. - ⚪ Dev's fresh-done signal is a marker file (
.bosia/analyse-dev.fresh); a field in the dev cache would avoid the extra state. - ⚪ The
import.metawarning scans route files only; a$libhelper usingimport.meta.dirinentries()still lists nothing silently. Scan non-node_modules inputs of the analyse bundle. - ⚪ User server Bun plugins run twice per build (analyse + server), so a stateful plugin (collects files, emits a manifest) sees analyse entries. Fixed by bundling routes once.
- 🟡 Analyse imports user modules into the build process (timeout can't kill a hung import; handles leak into prerender). Run it in a subprocess returning JSON exports +
entries(). - ⚪ Route server modules are bundled twice per build (analyse, then server). Build the server first and read exports from
dist/server, as SvelteKit does. - ⚪
bosia syncwrites.bosia/routes.tswith placeholdertrailingSlash/prerender(no analyse, sobun run checkin CI needs no DB). Build overwrites it. - ⚪
detectPrerenderRoutesre-reads every page server the scanner already checked. It now usesroute.prerender(pages + APIs); the skip warning moved to the analyse step. - ⚪
routeCacheFlagtreats a module that fails to import ascache = falsewithout logging, and re-imports it twice per request (once()drops rejections); the cause is never shown. - ⚪
routeCacheFlagimports the page and layout modules before every cache lookup (data endpoint too); on a miss they no longer overlap metadata/layouts. Save the flag per route after a clean import (docs fix it at load). - ⚪ A
+page.server.tsor+page.sveltethat throws on import gives a plain-text 500, not+error.svelte(SSR); already so on main. - ⚪ Plain-URL lookup +
queryAwarecode is copied in renderer.ts and server.ts; a tracked data request computes the identity hash 3× (cacheKey, plain key, dedup key). One helper in cache.ts. - ⚪ Tracking-param gaps: a component
handlehook or pluginrenderusing the param isn't tracked (served the param-less copy);new URL(path, url)readstoString: re-runs on any query change (accepted; useurl.origin). - ⚪ Cache-write gzip + brotli q5 run sync on the main thread (up to 2MB bodies). Use async zlib in the deferred write.
- ⚪ A 404 scans every route 3× (
??=rescans thenullmatch;renderErrorPagematches again). - ⚪ Data endpoint calls
computeCacheKeytwice (sort + sha256 each). SSR?_invalidatedwrites an entry no normal request reads. - ⚪
app.htmlsegments re-interpolated per request; public env script andsafeJsonStringifyescape map rebuilt per call; plugin fragments awaited one by one in two passes. - ⚪ Permanent
MutationObserverondocument.bodyeven with no viewport links; scroll/snapshot maps never trimmed;use:enhancedouble submit sends two POSTs;loaderCache.layoutskeeps every layout visited. - ⚪ Files over
ASSET_CACHE_MAX_FILE_BYTESretrycache.fill()(stat + promise) every request. - ⚪
parseByteslives in bothcache.tsandassetCache.ts(assetCache is Bun-only, cache.ts also runs on Workers).
bosia 1.2.5 (2026-10-15) — prerender checks the server's reply
- 🟡
prerenderOnesaved any reply and logged ✅. Nowredirect: "manual"; a page needs 2xx HTML, an API 2xx, not 204/205 (prerenderReplyProblem). Bad replies and timeouts fail the build, one ❌ each. - 🟡 A slug can't freeze a live route: each target is matched like
matchRoutes(an API wins);/blog/[slug]←newwithsrc/routes/blog/newfails as "served by". Replaces the clash ❌ for that case. - ⚪ The trailing-slash 308 runs before the prerendered lookup:
/always/one308s to/always/one/,/about/(never) to/about. - ⚪ Kept the
isStaticPathpage rule in every slash mode (review 2): the reply check catches.pngtoo, but later and less clearly, andalwaysnever fetches/logo.png. - ⚪
test/prerender-server.test.tsbuilds and boots real fixtures: 308s on saved pages, and a failing build listing every bad reply. - ⚪ docs:
[...slug]skipsid(theidlanding owns/id); theidlanding is prerendered on purpose.content/docs/id/index.mdis still never shown. - 🟡 Review: a prerender server that fails to start fails the build; it used to log ❌ and ship with every prerendered page missing.
- 🟡 Review: the data payload gets the same check (
redirect: "manual", 200); a page is saved only with its data, else navigation on a static host broke. - ⚪ Review: a failed 404 probe (wrangler
404-page) is listed with the page ❌s instead of thrown first, so one build shows all. - ⚪ Review: a bad reply's body is cancelled (socket back to the pool);
describeErrorkeeps an error'scodeandcause. - ⚪ Review: docs skip every locale index via
LOCALES(not a hard-codedid); routing and cloudflare skills say what now fails. - 🟡 Review 2 (regression): one
AbortSignal.timeoutwas shared by the page and data fetches, so both loader runs had one PRERENDER_TIMEOUT. Now one per fetch. - 🟡 Review 2: a redirected data request is a 200 JSON
{ redirect }(server.ts), so it passed the status check and was saved.prerenderDataProblemrejectsredirect/errorpayloads. - ⚪ Review 2: page and data fetched side by side (
Promise.allSettled; an unread reply is cancelled);describeErrorreads plain-object causes, 3 deep. - ⚪ Review 2: the 404 probe shares
prerenderReplyProblem(status 404) and still follows redirects: a catch-allalwayspage 308s it, and its 404 is the right 404.html. - 🟡 Review 3: the prerender child inherited
MAX_INFLIGHT(.env), so with the page+data fetch pair its own requests got 503s and the build failed at random. Child now sets it toinfinity(Bun reloads .env if removed). - ⚪ Review 3: a failed page aborts its data fetch (shared
AbortController); cancel() errors are ignored so they can't replace the real ❌; onefetchTexthelper; describeError shows an Error's name and number codes. - ⚪ Review 3: one server-start throw; parallel test allows 1s; docs skip a locale slug only if
src/routes/<locale>exists. - ⚪ Review 4: prerender child SIGKILLed 1s after SIGTERM (its drain waited 10s for aborted loaders);
IDLE_TIMEOUT=255andLOAD_TIMEOUT=PRERENDER_TIMEOUTin the child (Bun's idle timer is coarse, ~4-5s at 1). - 🟡 Review 5: prerender child gets
IDLE_TIMEOUT=0(off, not 255) andMETADATA_TIMEOUT=PRERENDER_TIMEOUT; a timed-out metadata() was swallowed and the page saved without its title. - 🟡 Review 5:
PRERENDER_CONCURRENCYcaps requests (shared slots), so side-by-side page+data no longer doubles loader runs. 404 probe follows one same-origin redirect only. stopdata test allows 9s. - 🟡 Review 6: per-fetch limit on the ready check (a stuck hook hung it); PRERENDER_TIMEOUT/CONCURRENCY must be whole 1..2^31-1 (0.5 = no workers, >2^31 fires setTimeout at once). Probe errors use
fetchFailure. - 🟡 Review 7: a page or API reply may be any 2xx again (exact 200 broke a 203 API that
mainsaved). - 🟡 Review 8: a 204/205 reply fails prerender (any-2xx saved it as an empty page).
- 🟡 Review 13: a server that exits with every page saved no longer fails the build (the 404 probe says it stopped). The exit is checked after a broken or timed-out page, Workers too; a probe timeout keeps its hint.
- ⚪ Review 12: crash line says
e.g. /p/x(with several in flight, the first to fail isn't the culprit); a test pins hex/binary/octal as accepted, as in 1.2.4. - 🟡 Review 12: a server that dies with no broken page (its pages timed out, or during the 404 probe) is still reported. The child's LOAD/METADATA_TIMEOUT are off (
Infinity): equal to PRERENDER_TIMEOUT they raced it. - 🟡 Review 11: a timeout keeps its own line when the server later dies; only broken connections fold into the crash line. All-fail test: 150 pages, under 10s.
- 🟡 Review 10: crash is checked once after all pages (1s grace), not a 100ms race per fetch, so a slow-reaped crash is still one line. Settings use Number() again (
1e3,5000.0worked in 1.2.4). - ⚪ Review 9:
fetchFailuregets the checked timeout; roadmap says finish order, not worker-pool. - 🟡 Review 9: a stopped data fetch no longer holds its slot for a 100ms crash check (every-page-fails build at 1 slot: 8.9s → 0.5s).
- ⚪ Review 8: a crashed prerender server is one ❌ with its exit code; request slots are the only limit (no worker pool); reply check takes
expect: "ok" | 404; text{error}shown; 1.2.5 changelog lists only changes from 1.2.4. - ⚪ Review 7: 404 probe follows only its own slash form (
/loginis named); DOMException number codes dropped; parallel-start test allows 1.2s. A failed data fetch still waits for its page (kept: stopping it made the ❌ line random). - ⚪ Each prerendered page runs its loaders twice (HTML, then data JSON): double API calls, and the two can disagree. Render once and take the JSON from the SSR payload.
- ⚪ Prerender child overrides live-traffic env one by one (MAX_INFLIGHT, IDLE/LOAD/METADATA_TIMEOUT); a new server limit must be added here too. One
BOSIA_PRERENDER=1flag the server reads would cover all. - ⚪ Review 4: data fetch errors carry
data(timeouts too); 404 probe drops its body viadropBody; ready poll usesHEALTH_PATH, says "not ready within 10s"; docs check<locale>/+page.svelte. - ⚪ The 10s prerender server ready deadline is fixed; make it an env var if a slow hooks
init()hits it. - ⚪
findMisownedTargetscopiesmatchRoutes' API-first order; move it into a pure helper both call, so a routing change can't skip the check. - ⚪ The owner check ignores an API's
x.jsonalias (/api/x.jsonserved by/api/[slug]); a narrow case, add it if anyone hits it. - ⚪
PrerenderEntriesErrornow also carries reply/timeout/server-start failures; rename toPrerenderError(exported name). - ⚪ When the prerender server died, page timeout lines still say "increase PRERENDER_TIMEOUT"; drop the hint when the crash is the cause.
- ⚪ A 404 probe that times out because a hook blocked and then crashed the server shows the timeout hint, not the exit code; check the exit after a probe timeout too.
- ⚪ The prerender child turns LOAD/METADATA_TIMEOUT off with the exact string
Infinity(renderer's parseTimeout);0orinfinitymeans 5000. Share one documented "off" value. - ⚪ The "no load or metadata limit" test checks env strings only; test the effect (a 300ms loader with
.envLOAD_TIMEOUT=100 is still saved). - ⚪ A stopped data fetch's AbortError is classed as a broken connection; harmless now (the page's error wins), but check
stop.signal.abortedfirst. - ⚪ A timed-out prerender fetch frees its slot while its loader keeps running, so a hung API call can exceed PRERENDER_CONCURRENCY; note it in the docs or wait for the loader.
- ⚪ The "dies after its pages time out" test comment says the exit lands during the 404 probe; it's now caught by the check after the pages.
- ⚪ The ❌ reply lines print in the order fetches finish (broken-connection lines last), not route order; sort them if it confuses anyone.
- ⚪ Any non-timeout fetch error (a bad gzip body too) counts as a broken connection, so it folds into the crash line if the server also dies; match reset/refused codes only.
- ⚪ The prerender server gets SIGKILL 1s after SIGTERM even on success, cutting an app's own slow
process.on("SIGTERM")handler; only force it when the build failed. - ⚪ "Did the child exit within N ms" is written out in dev.ts twice, beside prerender's local
exitsWithin; share one helper. - ⚪ Env integers are parsed by hand in 4 places (prerender, renderer, cache, assetCache) with different rules; share one
parseIntEnv(name, {min, max, fallback}).
bosia 1.2.4 (2026-10-14) — prerender `entries()` value fixes
- 🟡 Prerender
entries()values: unreserved URL chars only (A-Za-z0-9-_.~,/in catch-alls); no%/space/control/non-ASCII, so URL, file and static-host lookup agree. A part ending in.fails (.., and Windows drops trailing dots). - ⚪ Param names are ASCII TS identifiers (
^[A-Za-z_$][A-Za-z0-9_$]*$); one error message replaces the=/?/dots hints; no Windows-only test case; routeTypes keeps its quoted fallback for cached manifests. - ⚪ Prerender lists every bad
entries()route at once (Promise.allSettled), one❌line each; values are JSON-quoted in errors. - 🟡 Prerender fails on output paths equal ignoring case (
About/aboutare one file on macOS/Windows); values ending.html/.jsonand Windows device names (con,nul) fail too. - ⚪ Combined
PrerenderEntriesErrorkeeps every route's error incause. Strict name/value rules carry "decided 2026-10-14" comments in code. - ⚪ A non-entries error (unreadable route file) still drops the collected
entries()errors. - 🟡 A last-part
indexvalue fails: written as<parent>/index.html, it replaced the parent URL's page (even a live one)..html/.jsonchecks only the last part. - ⚪ Clash check dedupes repeated values, compares every folder level ignoring case (
A/xvsa/y) and includes__bosia/datafiles (/vs/index). - 🟡 Clash check: a file and a folder at one path clash (
a.jsonvsa.json/edit); dedupe keys on slash mode; the saved404.htmlis owned. Values can't start with.. - 🟡 Dedupe only within one route: a static
/aboutand slugaboutin the same slash mode were merged, dropping a page. Only a slug404is barred from 404.html; a static/404page may replace it. - ⚪
PARAM_NAMEis shared by scanner and routeTypes;[id?]tested via the regex (Windows can't make that folder). - ⚪ Clash message names the source route (
/about (from /[slug])); a clashing target still records all its files, so later clashes show in the same build. - ⚪ A dotted last part other than
.html/.json(report.pdf) is written asreport.pdf.html; a static host asked for/files/report.pdfmay not find it. - 🟡
entries()values can't give URLs the server answers itself, checked on the fetched URL with server.ts rules:/_health,/dist/,/__bosia/, andisStaticPathendings (.png) for pages. Case-sensitive like the server. - ⚪ Static prerender routes skip the value rules:
src/routes/_healthsaves the health JSON; a staticlogo.pngorindexfolder too. Scanner check; separate branch. - ⚪ A
public/file at a prerendered path (public/about.htmlvs slugabout) is silently replaced bygenerateStaticSite. Seed the clash check frompublic/. - ⚪ A static folder
indexat a route's end (src/routes/blog/index) innevermode writesblog/index.html, replacing/blog/. Scanner check; separate branch. - ⚪ Device-name check rejects
com0/lpt0, which Windows doesn't reserve; usecom[1-9]|lpt[1-9]. - 🟡
isStaticPathmoved to side-effect-freestaticPath.ts: importing it fromhtml.tsloaded that module in the build beforeloadEnv(), pinning BASE_PATH to"". Test guards build.ts's imports; html.ts no longer re-exports it. - ⚪ Page values are checked with and without the trailing
/: in always modelogo.pngpassed as/logo.png/, but/logo.png404s before the 308. - ⚪ A slug
__bosia-404-probethat a loader answers with 200 breaks the 404 probe. Reserve the probe path. - 🟡 URLs can't start with
dist/__bosiain any case: on macOS/WindowsDIST/swallowed thedist/clientbundle copied after it. PurecheckPrerenderPaths(). - 🟡
test/prerender-invariants.test.ts: sweeps values × patterns × modes; checks outcomes (server reach via realisStaticPath, a case-insensitive disk model of static/), not the rules. - ⚪
test/prerender-entries.test.ts: one regression table (accept/reject/clash) holding every review finding; add a row before each fix, never delete one. - ⚪
/dist/,/__bosia/and/_healthlive once instaticPath.ts(isFrameworkPath,HEALTH_PATH); server.ts and prerender share them. - ⚪
~is allowed in values, so on Windows with 8.3 namesGUIDES~1/introcan point at another route'sguides-old/folder. Clash check can't see it. - ⚪ Clash errors run only after every
entries()call passes; a bad value plus a clash needs two builds to see both. - ⚪ The prerender ready poll (
prerender.ts) anddev.tsstill hard-code/_health; useHEALTH_PATHfromstaticPath.ts.
bosia 1.2.3 (2026-10-13) — same-page refresh keeps scroll
- 🟠
invalidate*()/use:enhanceon the same URL jumped to top:settleScrollread a leftoverrouter.isPush. The nav effect now setsnavPendingon a route change; it survives superseded/redirected runs. - 🟡 An error page replacing the page (or the page replacing it) on a same-URL re-run scrolls to top.
- 🟠
use:enhanceredirects go through internalgo(..., invalidateAll): every loader re-runs (logout must refresh layouts), a same-page redirect refetches, and a staleformis cleared. - 🟡
goto(sameUrl, { invalidateAll: true })bumps the tick and refetches; it used to resolve with stale data. - 🟡
router.navigate()returnsstarted/same/cancelled/left;goto()marks dirty, bumps, and setsnoScrollonly for a nav that happens. Test:goto.test.ts. - 🟡 Hash-only shortcut drains
goto()resolvers (a#hashredirect hunguse:enhance); everydirty.allsetter also bumps the tick, so it refetches. - 🟢
/about/on/aboutreturnssameafter canonicalizing; no duplicate history entry. - 🟠 A loader redirect that doesn't start a nav (cancelled/same/left) drains resolvers; awaited
use:enhancehung on it. - 🟡
go()resolves only its own Promise on a no-op; draining the queue released a nav still loading. - 🟡 Back to the settled page mid-nav takes the hash shortcut: it now clears the stuck progress bar and skeleton.
- ⚪ Error ↔ page swap on the same URL scrolls to top even with
noScrollor a#hash. - 🟠 An invalidated run cancelled or loader-redirected before applying data sets
owedRefresh; the next run refetches every loader (logout via a redirecting/kept stale layouts). - ⚪ A loader redirect that is cancelled (or canonicalizes to
same) leaves the URL on the new route with the old page shown; the awaitedgoto()resolves anyway (pre-existing). - 🟠 A rejected route chain (stale chunk, throwing module) tears down the loading UI, drains resolvers, then rethrows so
hydrate.tsstill runs its cache-busting reload. - 🟡 Reload fallbacks use
hardLoad():href= path without its#hash(with it,hrefonly scrolled;reload()could re-POST a native form entry). - 🟡 An
invalidate()/goto(sameUrl, { invalidateAll })from a child'sonMountrefetches; hydration's first effect absorbed the tick and hung thegoto(). - 🟢 A loader redirect that doesn't start a nav resets
suppressScroll. - 🟡 No-match exit (server-rendered 404) drains resolvers; a same-URL
goto(invalidateAll)there hung. Error-chunk reload skips cancelled runs. - 🟡
goto()to a URL still loading (same, resolvers queued) waits for it; it resolved before the data showed. No-match exits tear down the loading UI viaabandonRun(). - 🟡 A child's
onMountgoto('/other')before App's first effect: hydration now books the server's route (router.hydratedRoute, set in hydrate.ts after canonicalizing, before any await) and runs/otheras a normal nav; it hung before. - 🟡 A
+loading.sveltechunk resolving after its run settled no longer re-shows the skeleton over the loaded page (pre-existing). - ⚪ A
use:enhanceredirect cancelled bybeforeNavigateclearsformand doesn't refetch, though the action mutated. Needsgo()to expose its NavResult. - 🟡 Loader redirects
replacethe redirecting entry, so Back no longer bounces off it. The hop keeps the nav type, butpopstate/enterbecomegoto(it is a cancellable push). - ⚪
owedRefreshre-runs every loader withfresh; keeping the cancelled run's dirty set until it applies would refetch less. - 🟠
owedRefreshclears only when fresh data applies (error/failed-fetch branch kept it);lastNavKey/lastSettledPathset only after the redirect check. - 🟡 A cancelled run's chunk rejection is swallowed, so
hydrate.tsdoesn't reload the page the user moved on to. - 🟡 Per-run state (
seenRoute,navPending,owedRefresh,suppressScroll, resolvers) lives in module flags that leak across runs; a per-run object committed on apply would replace the special cases. - ⚪
navigate()/popstate resetsuppressScroll, so a supersedednoScrollgoto no longer passes it to the newer nav. - 🟡 Back/forward onto a URL with no client route hard-loads it (server 404) instead of keeping the old page; a 404 hydration no longer books the next match as hydrated.
- 🟡
goto()to the current URL waits onrouter.loading(set by App's run), not the resolver queue — it missed link-click navs. - 🟢
hardLoad()useslocation.replace(no extra entry); a loader redirect keeps anoScrollgoto's flag; same-page hash clicks resetsuppressScroll. - 🟡 Hydration seeds the loader cache from
router.hydratedRoute, stages the snapshot only for the hydrated page, and settled marks wait for the error chunk;router.loadingclears at settle. - 🟡 A loader redirect back to the shown page runs as a real nav (forgets
lastNavKey); off-app redirectslocation.replace;settle()drains beforeafterNavigateso a listener'sgoto()waits. - ⚪ Relative
goto("#x")matches no route, leaves vialocation.href, and the null-state popstate resetshistoryIndexto 0 (pre-existing). - ⚪ A loader redirect back to the previous URL leaves two adjacent entries with the same URL; Back between them doesn't restore scroll/snapshot.
- 🟡 A failed
+loading.sveltechunk is swallowed: no skeleton, and nohydrate.tsreload of a page that loaded fine. - 🟢
navigate()setsrouter.loadingonstarted, so a same-tickgoto()to that URL waits; hydration's early return clears it. - ⚪ Back onto a 404 entry while hydration chunks load skips the hard load (first run forces
movedfalse). - ⚪ A redirect back to the shown page superseded before settling leaves
lastNavKey = "": next Back refetches and clearsform. - 🟠
export const cache = falsein+page.server.tswas silently ignored. Now honored:routeCacheFlagreads both modules' real exports; eitherfalseopts out. - 🟡 Navigating to a route with
+loading.svelteremounts its layouts (skeleton branch renders them separately): layout state like an open sidebar resets. - ⚪
await goto()resolves before scroll-to-top is applied, so ascrollTo()right after it gets overwritten. - 🟠 Route order:
sortRoutestie-broke same-tier routes bylocaleCompare([< letters), so/[a]/[b]beat/label/[id]. Now per segment: static > dynamic > catch-all. - 🟡
compilePatterncopied a catch-all's prefix literally:/[lang]/[...rest]never matched. LegacymatchPattern(same bug) removed;findMatchcompiles an unseen array once. - 🟡 Segment kinds were parsed four ways (matcher, sort, scanner, route types) and disagreed on mid-path/hyphenated
[...x]; onesegmentRank()inmatcher.tsnow feeds all. - 🟠 Mid-path catch-alls (
[...a]/[...b]/[...c]) compiled to nested(.+): 3 s per 4 KB URL (ReDoS). Build now rejects a non-last[...rest], nameless or duplicate params. - ⚪
isExactand prerender usedincludes("["); both now useisDynamic()(segmentRank), so/api/[id].jsonis static everywhere. - ⚪
segmentRankis strict (SEGMENTregex): malformed segments rank static,checkParamsruns first and names any bracket it can't read; unicode/$/.names still OK. Only a last[...x]compiles to(.+). - ⚪ Mid-path catch-alls (
docs/[...path]/edit): needs an anchored regex + span-aware ranking (SvelteKit sorts them last). Add both together if asked. - ⚪ Partial brackets (
[id].json,post-[slug]) matched literally;[[lang]]was a param named "[lang]". The build now rejects all three. Exact Map is first-wins like the scan. - ⚪ Catch-all prefix warning fires when another route already serves the prefix (
/[...all], or/[page]for/docs). Check withfindMatchon the sorted pages. - ⚪
[id=integer](SvelteKit matcher) built as a param named "id=integer"; the build now rejects it. Prefix warning now says "unless another route already serves it". - ⚪
[path...]built as a one-segment param (now "did you mean [...path]?");[...p=x]hint kept the...; prerenderreplaceread$&in values (function replacer now). - ⚪
substituteParamsgoes per segment (segmentRank): throws on a missingentries()key, keeps[draft]values literal; type imports use JSON-quoted paths so[it's]stays valid TS. - 🟠 An
entries()error skipped the whole route and the build passed (500 pages gone quietly); now exits 1. Values null/undefined/empty/.rejected;[id?]fails the build. - ⚪ Prerender: catch-all values may end in
/again;?/#rejected; errors throw (noprocess.exit); missingentries()export fails too.[path..]/[x…]typos caught. - ⚪ Prerender let
%2e%2e/%2Fthrough (home HTML saved asblog/%2e%2e.html); values are now checked decoded. Errors print one❌line (PrerenderEntriesError). - 🟡 Prerender: a slug with a bare
%(50%-off) failed as "invalid percent-encoding". Done in 1.2.4:%is rejected with a clear message. - ⚪ Prerender: decoded-only checks let
%3F/%2Fpass. Done in 1.2.4: the strict allowlist replaces decoding. - ⚪ Prerender: report every bad
entries()route at once (Promise.allSettled). Done in 1.2.4. - ⚪ Param names: one whitelist (
PARAM_NAME: letters, digits,_$-, inner dots) ends typo whack-a-mole ([ slug],[…rest]); specific hints kept for=,?, dots. - ⚪ One
parseSegment()returning{rank, name}or an error:SEGMENTruns 2–3× per segment, andcheckParamskeeps its own bracket regex. - ⚪ Skeleton → loader error: skeleton drops before the error chunk loads, so the old page remounts for that await (its onMount reruns; a flash on slow nets). Drop it after.
bosia 1.2.2 (2026-10-12) — 404.html for static hosts
- 🟡 Static hosts (Cloudflare assets) served
index.htmlwith a 200 for unknown paths. Prerender now saves the server's 404 asdist/static/404.html; docs' own copy inpost-build.tsis gone. Test:docs/test/not-found-page.test.ts. - 🟢 The prerender server no longer inherits
BOSIA_REUSE_PORT, so another server on the port can't answer for it. - ⚪ SSR-only apps get a
404.htmlonly when wrangler sets404-page(server boots just for it). - ⚪
/404.html(and/404on Cloudflare/Netlify/Pages) answers with a 200 serving the 404 page. Any file name does this; addnoindexif crawlers index it. - 🟠 Review: the ready loop ignored the child's exit, so a foreign server's
/_healthpassed. It stops once the child exits (the docs probe did this). - 🟡 Review: prerender server stderr shows now; a failed 404 render printed only "got 500".
- 🟡 Review: the docs test skipped when
404.htmlwas missing; it skips only before a build now. Deploy docs:404-pageis for assets-only deploys. - 🟡 A failed 404 probe fails the build when
wrangler.*setsnot_found_handling: "404-page"and there's nopublic/404.html(needsStatic404); else it warns. Test:needsStatic404.test.ts. - ⚪
needsStatic404reads wrangler text by regex: a key inside a/* */block comment still counts. Parse JSONC/TOML if it bites. - ⚪ One English probe:
/id/...gets the English 404. Add per-locale probe paths when needed (seedocs/ROADMAP.md). - 🟡 Error → error nav restores a still-mounted layout's title (
chainTitles), else the status title; keeps the error component's own only when nothing remounts. - 🟢 A
+loading.sveltewhose chunk hasn't loaded no longer counts as shown when deciding what remounts. - 🟡
--target=workersskips the 404 probe: the uploaded404.htmlonly answered/404.htmlwith a 200. - ⚪ 404 probe follows redirects, so a redirect-all hook reports "got 200"; use
redirect: "manual"and name the Location. - ⚪ The "404-page needs 404.html" error is built in two places in
prerender.ts; one helper would keep them in sync. - ⚪
needsStatic404also matches404-pagein anenvsection or a/* */block, failing builds whose main deploy doesn't use it. - ⚪ Ctrl-C during prerender: the 404 probe still runs against the dead server and can print a false "404.html could not be built". Skip it once
receivedSignalis set. - ⚪ A prerendered
/404route (trailingSlash: "never") overwrites the probe's404.htmlingenerateStaticSite, yet the build logs "✅ 404 page".
bosia 1.2.1 (2026-10-11) — layout guard warning
- 🟡 Dev warns once per route/depth/guard when a layout
redirect()/error(401|403)fires and a loader below runs user code (nestedload, pageloadunless masked,metadatawhen started). Read from module exports. Test:layout-guard-warning.test.ts. - ⚪ Also warns for read-only pages and non-auth redirects (locale, canonical): it flags the guard's place, not the code below. Accepted.
- 🔴 A plain-function
load()returning an object crashed (promise.finallyis not a function) whenLOAD_TIMEOUTwas set.withTimeoutwraps it inPromise.resolve. - 🟡 npm tarball lacked LICENSE (MIT requires the notice). Copied into
packages/bosia; npm packs it without afilesentry. Copyright: "Bosapi and contributors";AUTHORSlists people. - 🟢
packages/bosia/LICENSEis a hand copy of the root one; copy it in aprepackscript so the two can't drift. - 🟡 Client nav to a page with no title kept the last page's title (seen in Chrome e2e). It now gets the SSR default:
app.html's<title>, elseBosia App. Demoui.e2e.tstest, seen failing first. - 🟡 Svelte sets a
<svelte:head><title>only on mount or change. Review 4: a<TitleProbe>after each layout remembers what it wrote (titleMemory.svelte.ts); the build-timeheadTitle/layoutTitlesscan is gone. - ⚪ Not caught: a title written by a component after a layout's
{@render children()}, or one a layout changes between navigations. Documented in part. - 🟠 Review: invalidate, a form action or
?page=2reset a title a child set (<Seo>). The default now goes only on a page change, or when the title is still navigation's own. Demo test, seen failing first. - 🟠 Review: the scanner regex ran past
</svelte:head>, so a logo's<svg><title>in the root layout flagged every route. It now looks inside<svelte:head>blocks only. - 🟡 Review:
app.htmltitle entities (—) showed raw. Build ships the raw text; the browser decodes it (a<textarea>; raw text under Trusted Types). - 🟡 Review:
bosia syncignoredapp.html's title. It reads it too; the title argument is required now.headSplitmoved toappHtml.ts, shared by SSR and build. - ⚪ A plugin
head()<title>is replaced by the default on client nav. It runs per request on the server, so the client can't see it. Documented. - 🟠 Review 2: a layout
<svelte:head><title>flagged every route below it, so the old page's title stayed. A layout that stays mounted now gets its static title back. Demo/title-test. - 🟡 Review 2: back from an error page to the same page kept the error's title. The error and skeleton branches remount everything, so it counts as a new page. Demo
/title-error. - 🟡 Review 2: the first same-page nav after a hard load kept the
metadata()title. SSR hands it to the client (__BOSIA_TITLE__). Demo/title-meta; all three seen failing first. - 🟢 Review 2: the scanner reads each
+page.server.tsonce (readSource), not once for prerender and again fortrailingSlash. - 🟠 Review 3: an app with no root layout kept the old title when the layout count went to or from zero (
keptLayoutswas -1). Clamped at 0. Checked with a temp demo copy, seen failing first. - 🟡 Review 3: a dynamic layout title kept the last page's title, unlike the docs. It gets the default now. Demo
/title-live, seen failing first. - 🟡 Review 3: the scanner read a
<title>in an HTML comment, and took one under{#if}as always shown. It drops comments, usestitleText, and calls a head with{#…}dynamic. - 🟠 Review 4: a dynamic or
<Seo>layout title gave way to the default on every page change; it worked before this branch. Remembered at run time now. Demo/title-live,/title-seo. - 🟡 Review 4: after a redirect,
currentLayoutPathsnamed layouts not mounted. Set only once they render. Demo/title-redirect, seen failing alone. - 🟡 Review 4: a page whose
{#if}title didn't render kept the last title. A page change always writes the layouts' title first. Demo/title-test/cond. - 🟠 Review 5: a layout with
{#key}around its children lost its title (memory sat in the probe). Kept per layout component now; a write lives one microtask. Demo/title-test. - 🟡 Review 5: an
afterNavigatethat wrote the title passed for the root layout's. Writes are dropped as each flush starts ($effect.pre). Demo test, seen failing alone. - 🟢 Review 5: patch shared on
windowwith a dev warning;/title-livereally dynamic;clientDefaultTitle()andsysScriptBody()helpers. - ⚪ Not caught: an
{#if}page title on one dynamic route (/blog/a→/blog/b), and a layout title set in$effect/onMount. Documented; usemetadata().title. - ⚪ A layout that moves to or from the page's spot (layout count changes) remounts, since
renderLayoutuses another branch there. Title code allows for it; worth a look for state loss. - 🟠 Review 7:
Object.hasOwnin the title patch broke hydration on Safari 15.0–15.3; useshasOwnProperty.callnow. - 🟢 Review 6: the decoded default title collapses whitespace like
document.title;bosia syncwarns when it can't read the app.html title. - ⚪ Not caught: a page title from
$effect.pre/$:taken for the layout's; a layout rendering children after its title ({#if},{#await}); a changed layout title comes back stale. Documented. - 🟡 Next: replace the
document.titlepatch with an explicit layout title (layoutmetadata().titleor a title registry). The patch leans on Svelte effect order. - ⚪ A
+loading.sveltechunk that loads after the nav settles shows the skeleton until the next nav. Also on main. - ⚪ Client-nav error pages get the hard load's
404 — Not Foundtitle and drop the last page's metadata tags; an error/layout<title>still wins. - 🟡 Review: error → error with the same
+error.sveltekeeps its own title; status title format shared viaerrorTitle()(headScan.ts). - 🟡 Review: prerender watches
child.exited(signal kills);404-pagebuilds also fail when the server won't start, no routes prerender, or the 404 isn't HTML. - ⚪
needsStatic404misses single-quoted TOML ('404-page') and checks config files in a different order than wrangler. - ⚪ The Bun server serves
dist/static/404.htmlat/404.htmlwith a 200 (soft 404) for every prerendering app. - ⚪ Prerender's inherited stderr also prints normal server warnings (CSRF exempt paths) during
bosia build. - 🟡 Review 2: keepTitle compares the collapsed status title;
--target=workersnever requires 404.html; SSR-only404-pagebuilds log "Building 404.html". - ⚪ Prerender ready race: a server that grabs the port first can pass
/_health. Fix with a nonce in the child env echoed by/_health. - ⚪ Client nav never resets
<html lang>to app.html's when the next page (or an error page) has nometadata().lang. - ⚪ A conditional
<title>in+error.svelte({#if}) stays after 404 → 500 with the same boundary; a hard load shows the status title.
bosia 1.2.0 (2026-10-09) — parallel loaders
- 🟡 Layout + page loaders start at once;
parent()awaits the layers above. Settled root → leaf, so the root-most failure still decides the response. Demo test: 3×150ms loaders 455ms → under 400ms. - 🟡 Data endpoint runs
metadata()alongside the loaders and hands itsdatato pageload()(wasnullon client nav). Form re-render overlaps them too, like SSR. - ⚪ Hook-thrown
redirect()/error()now go throughfinalizeResponse(security headers, CSP, CORS, cookies). 4 new cases inhooks-redirect.test.ts. - ⚪ Registry auth drops
secure:on the session cookie. Shop/store templates get stub pages for every admin-sidebar link. - Docs (en/id): loaders guide "Loaders run in parallel", security note; routing skill R3a. Test:
parallel-loaders.test.ts. - ⚪ Cross-loader duplicate
setHeaders()still throws, but which loader throws now depends on timing, not depth. - 🟡 Fresh scaffolds fail
check(Prettier mismatch) — repo and all 4 templates pinprettier 3.9.9+prettier-plugin-svelte 3.5.2exactly; repo reformatted. Bump all five together. - 🟠
<svelte:head>SSR output goes in the raw<head>viabuildMetadataChunk. TheinsertAdjacentHTMLscript andbuildHtmlTail'sheadparam are gone, so JSON-LD and icons reach non-JS crawlers. Its scripts now run, without a nonce. - 🟡 Streaming title fallback ignored plugin
<title>s andheadCloseskipped%bosia.nonce%. Both builders now sharetitleFallback();buildMetadataChunktakesnonce. - 🟡
<svelte:head>now goes just before app.html</head>on both paths (closeHead), so page tags win again. Its scripts block paint and need a nonce — documented, kept. - 🟡
app.html<title>now suppresses the fallback;findHeadEndscans for the first real</head …>(else<body), skipping comments and script/style; split cached per segments in a WeakMap.errMetauses sharedhasTitle(). Docs: escape<in JSON-LD. - ⚪
buildHtmltakesheadExtras;buildMetadataChunktakes options.hasTitleis one linear scan (skips comments, script, style, svg). The fallback title goes last, so a missed real title still wins. - 🟢 Share one head scanner between
findHeadEnd(appHtml.ts) andhasTitle(html.ts); their rules differ (e.g.<script-loader>, svg/noscript/template, unclosed tags). - 🟢
buildHtmltakes an options object (17 positional params; a misplacedundefinedgoes unnoticed) and decides the error-page title slot itself, instead of the renderer (errMeta). - ⚪ Error pages never render plugin
headfragments, so a plugin<title>or tag is missing on 404/500 (renderErrorPage).
bosia 1.1.6 (2026-10-08) — HEAD = GET
- 🟡
+server.tsexporting onlyGETanswers HEAD (was JSON 405).Allowlists HEAD with GET. An explicitHEADexport still wins. - 🟡 HEAD reads the GET cache entry on pages and API routes (never coalesces or writes), so
curl -Ino longer reruns loaders. - ⚪ Cache-hit HEAD sends the real
Content-Length(BosiaAppnulls HEAD bodies, which made it 0). - 🟢 HEAD misses still report
Content-Length: 0:BosiaAppnulls the body. Fix if a crawler cares (needs Workers HEAD check).
bosia 1.1.5 (2026-10-07) — static files skip hooks
- 🟠 Static hits served in
handleRequestbeforehandle(same order asresolve(): framework prefixes, then API shadows files). Misses still go through hooks → 404. - Route matched once before hooks, parked per request, reused by
resolve()(re-matched if a hook rewrites the URL). Hooks get realparams. - ⚪ A static miss is remembered per request, so
resolve()404s without a second file lookup (ultrareview nit). - Demo, hook with 1ms fake session lookup: hydrate chunk 6.4k → 45.7k rps, favicon 40k → 66k. Pages unchanged (noise band 34–41k both).
- 🟠
route.id= folder path with groups (/(private)/x/[id]) on RequestEvent (null if no match), LoadEvent, MetadataEvent, API + actions. Scanner + codegen emit it. - Docs (en/id) + skills teach the hook gate
route.id?.startsWith("/(private)"); layout-guard section removed. Shop/store templates moved their gate toguardHandle. -
test/route-id.test.ts: hook guard stops SSR, client nav, form POST;metadata()counter proves it never ran. - 🟡
page.route.idon the clientpage(SvelteKit parity):clientRoutesemitid, derived frompage.urlviafindMatch.nullon 404. Tested inssr-page-url.test.ts. - ⚪ Rule: guard in hooks.
metadata()stays parallel with layout loaders (no wait); a layout redirect can't stop it. Pinned inroute-id.test.ts; security + metadata guides say so. - 🔴 Chrome e2e found:
bosia_sessionnot inCACHE_KEYS→ signed-in pages cacheable across users.authfeature + shop/store.env.exampleset it;bosia featignores# KEY=lines. - ⚪ Registry auth sets
secure: NODE_ENV === "production"(Bosia warns; skill says omit). Shop sidebar links to missing/dashboard/*pages.
bosia 1.1.4 (2026-10-04) — static files from memory
Load test (M1, oha, 50 conns): cached SSR ~46k rps, but
/about(prerendered) ~14k and hashed chunks ~13k.serveStaticreadBun.filefrom disk on every request.
- 🟠 New
core/assetCache.ts: byte-budget LRU, filled lazily on first hit (no boot I/O, cold start unchanged).serveStaticanswers hits from memory;.br/.gzcached per variant. - Env
ASSET_CACHE_MAX_BYTES(50MB,0= off) +ASSET_CACHE_MAX_FILE_BYTES(1MB), inFRAMEWORK_VARS. NotSTATIC_*: that prefix is the build-time env tier. - Range requests stay on
Bun.file(Bun answers 206). Workers untouched: Static Assets serve files before the worker runs. - Demo after:
/aboutbr 14.9k → 51.4k rps, hydrate chunk br 14.1k → 36.1k rps. Miss/hit headers + body identical; RSS unchanged (~55MB). - Tests: fill→hit parity (raw/br/gz), per-file limit, LRU eviction, Range bypass, disabled, env parsing. Docs: env vars + deployment (en/id),
bosia-envskill,.env.example. - 🟡
FRAMEWORK_VARSwas missingCACHE_*,IDLE_TIMEOUT,MAX_INFLIGHT,TRUST_PROXY,CSP_DIRECTIVES… Added,BOSIA_*reserved viaisFrameworkVar; a test scanssrc/for env reads. - 🟡 Brotli split: q3 per request, q5 for cache entries. Uncached
/blogbr 5.9k → 8.7k rps, 2.25KB/response. Cached hits unchanged (~48k). - 🟡 Cacheable miss compressed the client's variant twice. Now encoded once at q5, sent, and reused by
buildCompressedVariants(body, prebuilt). - ⚪
serveStaticsends a strongETag(size+mtime, per encoding) and answersIf-None-Matchwith 304. ETag only: noLast-Modified, so no heuristic freshness onpublic/. - ⚪ ETags are cached per process (one stat per path). A file swapped in place while the server runs keeps its old ETag until restart.
bosia 1.1.4 (2026-10-04) — security audit fixes (#5)
- 🔴 Layout server loaders always run; the
_invalidatedmask only omits their data. Actions run after layout guards;parent()is server-only. - 🔴 Never serve
dist/server|worker/, hooks/config bundles or build metadata. Dev matches request segments against realreaddirnames. - 🟠 CLI
add/block/page/feat/theme:containedPath+registrySource(URL-checked) run before deps; files read before any write.
bosia 1.1.4 (2026-10-04) — perf (#6)
- Route thunks import once; SSR overlaps metadata/imports/plugins;
/__bosia/datacached (URL +CACHE_KEYS+ mask,_fresh=1after invalidate), ETag 304 on hits. - zstd per request,
Bun.gzipSync; hover/touch/focus prefetch loads page chunks;Linkpreload header; dev Svelte compile cache, no prerender in dev. - 🟡 Server tests'
afterAllkills the child withoutawait child.exited, so WindowsrmSynchits EBUSY (flaky CI). 5 files: data-endpoint-cache, layout-guard, dev-static, hooks-redirect, preload-link. - ⚪ A redirect thrown from
hooks.server.tsships without security headers (X-Content-Type-Options etc.). Pre-existing on main.
docs 0.8.19 (2026-10-03) — "Brand" in block previews
- 🟢
docs/sync-docs.tscopiesregistry/blocks→docs/.bosia/blockswith__BRAND__→Brand;$blocks/*+$lib/blocks/*aliases point there. Registry keeps the sentinel.
Perf audit (2026-10-03) — open findings
Code read of the request path (server.ts, renderer.ts, html.ts, cache.ts, staticManifest.ts). Not benchmarked; only the microtask ordering (#4) was verified with a script.
- 🟠 SSR cache misses ship raw HTML. Fixed 1.1.3: body built whole (it never streamed) and sent via
compressBytes, br preferred. Demo/miss 9.5KB → 2.4KB br. - 🟠 Static assets uncompressed. Fixed 1.1.3: build writes
.br/.gzfordist/client+dist/prerendered;serveStaticnegotiates. Hydrate chunk 108KB → 31KB br. - ⚪
public/text files (svg/txt/json) still ship raw — the precompress pass skips the app's source dir. Precompress intodist/staticif it matters. - ⚪
compress()is untestable for encoding underbun test(isDevread at import). Integration coverage isapps/democurl +ui.e2e.ts; add a built-server encoding test. - 🟠 User
handlehook runs for every static asset: static branches live insideresolve(). A session-lookup hook = one DB query per chunk. Serve manifest hits inhandleRequestbefore hooks. - 🟡 Cache-write compression blocked the first byte (microtask). Fixed 1.1.3:
deferCacheWriteusessetImmediateon Bun, microtask on Workers (no compression there). Order test added. - 🟡 Layout loaders run serially root→leaf even when none call
parent(). Start all in parallel; makeparent()await the earlier layers (SvelteKit's model). - 🟡
metadata()adds a serial step: before loaders on SSR (needed only if it returnsdata), after loaders on the data endpoint (dataunused there). Also check: pageloadgetsmetadata: nullon client nav. - ⚪
public/files ship with no Cache-Control (heuristic caching). Prerendered HTMLmax-age=3600can outlive a deploy and point at deleted hashed chunks.
bosia 1.1.2 (2026-10-02) — preload route chunks
Clicks before hydration were lost: the HTML only hinted the entry, so page/layout chunks loaded in a serial waterfall after it ran.
- Client build emits a metafile;
preloadMap.tsmaps each route to its layout+page chunks plus static imports →manifest.preload. - Shell and
buildHtmlemit amodulepreloadper route chunk (skipped whencsr=false). Demo/blog/[slug]: 6 links, all 200; prerendered too. - 🟡 Measure the before/after hydration time on sso/pay prod (DevTools, throttled), then
bun update bosiain both. - 🟢 Send the preloads as an HTTP
Linkheader / 103 Early Hints so they start before the HTML arrives. - 🟢 Verify
wrangler dev(Workers) and aBASE_PATHbuild emit the links; covered by code path, not run.
bosia 1.1.0 (2026-09-28) — drop `page.params`
The 0.7.0 deprecation promised removal at 1.0. Params come from
$props()only. SvelteKit still keepspage.params; this is Bosia's choice.
- Removed the
page.paramsgetter + dead SSRappState.routeParamsseed. SSR error layouts now getparamsviassrPageData. Test probes the prop. - Moved the
use:enhancesuccess fix (84763a4, committed 09-28) from 1.0.0 to 1.1.0 in the changelog. - Demo
/all/[...catchall]showed no segments: it read the strippeddata.params. Now uses theparamsprop. -
+error.sveltegetsparamsas a prop (nested + root, SSR + client).ErrorProps.paramsaddsstring | undefinedfor child-route keys. Tested; demo/blog/boomshows the slug (Chrome: SSR + client nav). - 🟡 Dev: inspector
/__bosia/errorsSSE died every ~12s (ECONNRESET): server idle timeout 10s, ping 25s. Ping now 5s.
bosia 1.0.0 (2026-09-27) — Bun 1.4
Toolchain moves to Bun 1.4.2 (
.bun-version); apps declareengines.bun >=1.4. Safe floor because Elysia (hit by the 1.4.0–1.4.1 var/let bundler bug) is gone.
-
.bun-version1.4.2,@types/bun1.4.2 in every workspace.engines: { bun: ">=1.4" }in bosia, all 4 templates,apps/demo,apps/demo-worker. - 1.4 types type
BuildArtifact.kind; dropped theas { kind?: string }cast inbuild.ts. - Demo client 36 JS / 272KB → 33 / 268KB. Worker 265KB / 81KB gz → 244KB / 72KB gz. Manifest entry + component CSS unchanged.
- Bun 1.4.1 now injects a lazy-
import()modulepreload helper into the hydrate chunk (URLs fromimport.meta.url, copies CSP nonce). Kept;modulePreload: falseturns it off. - 🔴 Dev hydration died ("reading 'call'"): 1.4 chunks import the entry by bare URL, dev loaded it with
?v=→ two Svelte runtimes.html.tsnow emits the hashed entry verbatim; test pins it. - Verified in Chrome: prod, dev and
wrangler devhydrate + SPA-navigate with no console errors; form action, inspector badge. Plus 563 tests, dev-smoke, curl prod checks. - 🔴 Windows CI: Bun 1.4.2
rmSync("./dist", {recursive})threw ENOENT on an existing dir (forcehid it), so rebuilds kept staledist/.clearOutputresolves paths and finishes leftovers entry by entry. - 🟢 Report the Windows
rmSyncrelative-path ENOENT to Bun with a minimal repro; drop the fallback inclearOutputonce fixed upstream. - 🟡
new Bun.SQL("postgres://…")works on 1.4.2 (6 URL shapes + IPv4-only forward, live PG 15). Dropped 1.3.x gotcha from skills/registry. Object form stays (timeouts). - 🟡 Inspector
parseTopFrameskipped frames with(group)paths → pointed atrenderer.ts. OneFRAME_RE(sourcemap.ts) now serves server, overlay,resolveStack; Windows paths too. - 🟡
use:enhancesuccess never showedform: App's nav effect ran on the post-action invalidation and setform = null. Now only a real URL change clears it. Verified in Chrome (dev + prod). - 🟢 Inspector overlay dedups client-side: same source+message+file:line collapses into one row with
×N(50× loop on/errors-test→ 1 row). - 🟡
apps/demo/test/ui.e2e.ts(bun run test:ui):Bun.WebViewdrives demo in prod + dev. Hydration, SPA nav, title, back, form actions, guard, catch-all, no console errors. macOS only. - 🟢 Run
ui.e2e.tsin CI (backend: "chrome", ubuntu job) once it proves stable locally. - ⚪ Add the
parent()client-nav regression (0.6.17 section) toui.e2e.ts; needs a layout+page server pair in demo. - 🟢 Try
bun test --parallelin CI. - 🟢 Consider
minChunkSizefor the client build (fewer tiny chunks). - 🟢 Consider
Bun.serve{ dir }routes inBosiaAppfor static assets (sendfile, ETag, Range for free).
bosia 0.9.10 (2026-09-27) — Drop Elysia
Elysia only wrapped
handleRequest. Replaced by a small built-inBosiaApp, so plugins keep their shape and Workers bundles shrink.
- Dropped Elysia on Bun and Workers: own Elysia-shaped
BosiaApp(core/backend.ts) with routes, onRequest/onAfterHandle/onError, HEAD=GET w/o body. Test worker 527→151KB. -
Lean entry only when no plugin usesnot needed:backendBosiaAppis small, so server-timing and inspector run on it everywhere. - 🟡 Unrouted methods (TRACE etc.) answered 500 via Elysia's NOT_FOUND. Now 405 +
Allowon Bun and Workers. - 🟢
BosiaApproutes: add:parampaths when a plugin needs them (exact +"*"only today). - demo + demo-worker e2e in Chrome vs the 0.9.9 build: pages, nav, actions, API, HEAD, 405, guard data, inspector SSE all match. demo-worker 626→265KB.
- 🟡 Pre-existing:
/actions-testsuccess message never showed. Fixed in 1.1.0 (84763a4). - 🟡 Pre-existing (0.9.9 too): guard client-nav click stays put;
/all/[...catchall]shows no segments. Both work now,ui.e2e.tscovers them.
bosia 0.9.9 (2026-09-27) — Cloudflare Workers target
Deploy Bosia to Workers free tier via
target: "workers"— two runtime entries, no general adapter API. Spike: demo SSR renders in workerd, 172KB gzipped; syncnode:crypto/node:zlibwork there.
- 🟠 Phase 0 spike:
target: "browser"+external: ["node:*"]; ElysiaCloudflareAdapterhandles"*"routes +onError; a globalBunshim breaks Elysia's runtime detection. - 🔴 Phase 1:
config.tsrebuilt config as{ plugins }, droppingstrictImports(and any futuretarget). Now keeps every field; tests cover source + prebuilt paths. - 🟠 Phase 2: build artifacts read via
core/artifacts.tsreadArtifact(); build writes.bosia/artifacts.ts(inlined JSON), swapped in bymakeBosiaPlugin(_, "workers"). - 🔴
window.__BOSIA_ENV__was always empty: declaredPUBLIC_*names lived in the CLI process, not the spawned server. Build now stampspublicEnvinto manifest.json. - 🟡
.bosia/artifacts.ts+runtime.workers.tsare now generated only for the workers target. - 🟠 Phase 3:
Bun.CryptoHasher/Bun.gzipSync→node:crypto/node:zlibin cache.ts + html.ts; identity hash pinned by test. Nobosia:platform: lifecycle code lives in the Bun-onlylisten()tail. - 🔴 Phase 4:
server.ts→createApp(); entriesserver.bun.ts(hooks off disk, listen, signals) +server.workers.ts(static hooks/config viabosia:workers-runtime,fetch(req, env)→event.platform.env). - 🔴
Response.redirect(relative)throws in workerd → hook/metadata/load redirects 500'd. 4 sites (server.ts, renderer.ts ×3) now build the 3xx by hand. - 🟡
BODY_SIZE_LIMITalso checked fromContent-LengthinhandleRequest— Workers has nomaxRequestBodySize. Chunked uploads fall back to Cloudflare's cap. - 🟡 Workers plugin keeps bare node builtins external — a browser-target build silently swapped
fsfor{}.paths.tstoleratesimport.meta.dirbeing undefined. - 🟢 Dev-only plugins imported by
bosia.config.ts(inspector ≈234KB gz) land in the worker bundle. Demo is 410KB gz — fine under 3MB; revisit if apps get close. - 🟡 Phase 5:
targetconfig +--target=workers(BOSIA_TARGET) →dist/worker/index.js, manifesttarget,wrangler.jsonc(written once),bosia start→wrangler dev; client chunkschunk-[hash]. - 🟠
event.platform.envverified reachingload()— end-to-end build test imports the worker and callsfetch(req, env). - 🟢 Runtime vars on Workers come from
wrangler.jsoncvars/ secrets /.dev.vars, not.env— documented in deployment.md +bosia-env. - 🟡 Phase 6:
workersGuard.tsfails workers builds onBun.*,fs/node:fs,bun/bun:*in server files (routes, hooks,lib/server).BOSIA_WORKERS_GUARD=0warns instead. - 🟢 Guard scans server files by path — a Bun call in a plain
src/lib/*.tsa loader imports is missed. Upgrade: check in the workers plugin'sonLoad. - 🟡 Phase 7: deployment/cli/api docs (+ id),
bosia-cloudflareskill,bosia-bun-runtime+bosia-envWorkers notes; package description no longer says "no adapters". - 🔴 Live deploy: cache hits came back as brotli-inside-gzip — workerd re-encodes bodies with
Content-Encoding.encodeBody: "manual"on precompressed responses, kept throughhandleRequest's rebuild. - 🟡
apps/demo-worker: Workers mirror ofapps/demo(no uploads routes,/platformreads a wrangler var).apps/demostays Bun-only. - 🟡 Live
demo-worker: routes, actions, CSRF, guard, 413,platform.envOK. 60 alice/bob requests × gzip/br/none: no leak, all readable. CPU warm 0–5ms. - 🟡 Cold
/CPU (workerd, 8 isolates): median ~11ms → ~7ms. Workers skips compression (edge does it; miss 3.3 → 1.6ms), tailwind-merge warmed at startup. - 🟡 Live after fix: first request per isolate 8–15ms CPU (was 11–23), warm 1–5ms, no 1102 errors. Rest is V8 compiling on first run.
- 🟡 Startup warm-up: each isolate renders
/once (no hooks/loaders/cache). Live: 32 requests on a fresh deploy, max 3ms CPU, 0 over 10ms, 0 errors. Startup 98ms. - 🔴 Workers: client nav to a prerendered page POSTed its data file → asset server 405. Router now GETs prerendered data;
prerender+actionspages render live. - Workers docs + skill R9: prerender pages that never change (no worker CPU, no request).
- Workers bundle: stub
svelte/compiler(pulled in by dev-only inspector). Demo 1453→626KB, 408→184KB gz; live startup 148→49ms. Left: Elysia+typebox+file-type ~330KB (item 4). - 🟢 Windows CI:
findWorkersIncompatiblereturned\paths (report already printed/). Hits now use posix paths on every OS. -
Workers shared cache viaskipped: per-colo, no tag/prefix purge, no-op on workers.dev.caches.default - 🟢 Tests ran twice on publish pushes.
ci.yml→test.yml;publish.ymlruns only on apackage.jsonchange and waits for test.yml on the same commit. - 🟢 Test + publish merged into one
test-publish.yml: tests run once; publish jobs run after them, only whenpackage.jsonchanged. Docs-only pushes/PRs skip it (docs/**, root*.md).
bosia 0.9.8 (2026-09-27) — two pages, one URL: fail like SvelteKit
Found while writing the dev-smoke job: appending to
src/routes/+page.sveltein the default template created a second/next to(public)/+page.svelte. Nothing complained, and one page was silently unreachable.
- 🔴
scanner.tspreventConflictsthrowsRouteConflictErrorwhen pages or+server.tsAPIs match once groups and param names are ignored ([id]≡[slug]) — SvelteKit'sprevent_conflicts, Next.js's "two parallel pages". - 🟠
build.ts+bosia syncprint the message and exit 1 (no stack). Dev shows "Build failed", keeps the last good build (503 if none), recovers on fix. - 🟡 Breaking for apps that already had duplicates. Routing guide (+
id/) andbosia-routingskill R4 updated. - 🟢 CI: cleared the Node 20 deprecation —
checkout@v7,upload-artifact@v7,download-artifact@v8,wrangler-action@v4(Wrangler 4;pages deployflags unchanged).setup-bun@v2already on node24. - 🟢 Matcher: exact Map is last-wins, linear scan first-wins. Unreachable now that conflicts fail the scan; revisit if duplicates ever become legal.
bosia 0.9.7 (2026-09-24) — native Windows support
Asked whether Bosia runs on Windows. It had never been tried: CI was Linux-only, and the code assumed
/paths,:in NODE_PATH, extensionless.binshims andlsof.
- 🔴
build.tswrote\-separated client assets intomanifest.jsonand picked the server entry viasplit("/")→ broken asset URLs andbosia startpointing at a full path. NowtoPosix(relative())+basename(). - 🔴
safePath.tscheckedroot + "/", so on Windows every public/dist/static file resolved as "outside" and 404'd. Usessepnow. - 🟠
paths.ts:BOSIA_NODE_PATHjoins withpath.delimiter(C:split the old:list);resolveBosiaBintries.exe/.cmd/.bunxon win32 so Tailwind is found. New sharedtoPosix(). - 🟠
scanner.tsbuilds manifest paths with/by hand instead ofjoin; inspectordata-loc, sourcemap cwd-relative paths andcreate --localfile:paths also normalized. - 🟡
port.tsfalls back tonetstat -anoon Windows (listener = foreign:0, since the state column is localized). ParsersparseLsof/parseNetstatunit-tested. - 🟠 New
ci.yml: tests + scaffold/build/start/asset-probe smoke on ubuntu + windows.publish.ymlsplit into decide/test/build-templates/publish-npm/release/refresh-assets; publishing now waits on both OSes. - 🟡
.gitattributesforces LF so Windows checkouts don't failprettier --check. Getting Started lists Windows (native or WSL2). - 🔴 First CI run failed on
prettier --check:bun.lockwas gitignored, so CI installed prettier 3.9.9, not 3.8.3. Now committed (without untrackedapps/demo-worker); Bun pinned via.bun-version. - 🟠 Tests blocked the 0.9.7 publish, but
decideonly published when the push diff touchedpackage.json, so no retry was possible. It now publishes whenever the version isn't on npm. - 🟠 Windows CI run 2: 22/513 tests failed. Real bug:
resolveLocalRegistryusednew URL(import.meta.url).pathname(/D:/…on Windows) →import.meta.dir. Tests: scanner helper split on/; svelte-audit deleted its cwd (EBUSY). - 🟢
windows-latestCI green (513 tests + create/build/start/asset smoke); 0.9.7 published viaworkflow_dispatchafter the npm-baseddecidechange. - 🟠
dev-smokeCI job (ubuntu + windows,.github/scripts/dev-smoke.sh): dev serves, picks up a page edit + new route,pidsOnPortfinds the proxy, and a restart after a hard kill reaps the orphaned app server. - 🟡 Manual
bosia devon real Windows for live browser reload (SSE) and Ctrl+C shutdown — CI can't check either.
bosia 0.9.6 (2026-09-10) — every SSR'd page painted before its own CSS existed
Reported from Fisika with a screenshot of the OBE landing: correct type, no layout.
dist/manifest.jsonheld"css": []and.landing__titlelived in a+page-*.jschunk. Every scoped<style>in every app arrived with the JS bundle, so the streamed SSR markup painted first and hydration moved it.
- 🔴
svelteCompiler.tscompiled the browser target withcss: "injected", which is why. Not a first-paint tradeoff anyone took: 0.4.3 adopted it to fix a build failure, and 0.4.4's "Multiple files share the same output path" is what kept it. 0.8.6 already proved that collision gone on Bun 1.3.14 — the blocker had expired and nothing was watching. - 🟠
core/componentCss.tscollectsresult.css.codein theonLoadhook and writes ONEbosia-css-<hash>.css, rather than letting Bun emit per-chunk sidecars. Keepssplitting: trueat zero CSS outputs, so the collision class stays out of reach whether or not Bun regresses. MirrorstwHash.ts: rebaseurl(…)before hashing, or aBASE_PATHmount serves a component's background image off the origin. - 🔴 The inspector plugin does its own
.sveltecompile and registers first, so itsonLoadwins in dev. Changing onlysvelteCompiler.tswould have fixed production and leftbosia devflashing — the exact split that hides a bug until deploy. Both compileexternalnow; both hand off to the same collector. - 🟠 Collected from the browser instance only. The two compiler instances share module state and the client and server builds run concurrently, so a target-blind collector doubles every rule.
- 🟠
distManifest.csswas already rendered as<link>at four sites inhtml.ts— but ahead oftwCssLink(). Scoped rules used to be appended todocument.headat hydration, i.e. last, and the stylesheetsapp.css@imports land unlayered in Tailwind's output, so linking first would silently flip which side wins a tie. Moved after, on all four. - 🟡
scanner.tsthreads+loading.sveltedown the walk the waylayoutChainalready is. It was per-folder only, so Fisika's OBE needed 44 identical files to cover one section; one now covers 50 of its 51 routes.App.svelteneeded nothing —loadingDepthreads the shared-layout prefix, not the file's location. - 🟠
test/svelte-build.test.tskept itscssOutputs.length === 0assertion (still true, and it is the invariant that matters) and flipped the other way round: the scoped keyframe must be in the stylesheet and absent from every JS chunk.test/html.test.tsgained the ordering assertion that never existed,test/scanner.test.tsthe cascade with a nearer file overriding. - 🟠
apps/demogained/fouc-test, whose layout lives only in its own<style>block — no utility does any of it, so the page is a pass/fail readout rather than something to squint at. Verified A/B in Chrome against a real scripts-stripped page served from the origin: fixed renders the card, reverted reproduces the reported screenshot exactly. A fixture that is not first seen failing is not a fixture. - 🟡 Its
url(/favicon.svg)covers the rebase. Built underBASE_PATH=/demoit becomesurl(/demo/favicon.svg)and the seal loads — the failure mode this guards is a silent 404, not an error. - 🟡
/loading-test/nestedcarries no+loading.svelteand inherits its parent's. Watched live: skeleton up, header and footer still mounted, URL already changed. - 🟡 Verified against all four Fisika apps and
apps/demo, dev and prod. In Chrome: two linked stylesheets, zero injected<style>tags,.landingresolving tobosia-css-*.css. Dev SSR class names match the dev stylesheet — the inspector usesfnvon both targets, so they agree. Swept all 16 demo routes plus 404, 500 and the prerendered page: every one links both sheets in the right order.
bosia 0.9.5 (2026-09-05) — a guard that never ran on a click
Reported from pay as a fake "500 Internal Server Error" when a signed-out visitor clicked into
/admin. The server never errored. Underneath the 500 sat a leak: the guard the docs teach did not run on client navigations at all, so the route returned its loader data to a caller with no session.
- 🔴
event.urlis the page URL before the hooks run, not after. The/__bosia/data/<route>.json→/adminrewrite sat insideresolve()(server.ts:253), so a guard — which runs beforeawait resolve(event)— saw the transport path and itsstartsWith("/admin")was false.curl /__bosia/data/admin.jsonwas the whole exploit. - 🔴
throw redirect()/throw error()from a hook could never work:dist/hooks.server.jskeepsbosiaexternal (build.ts:24), so the hook'sRedirectcame from the app'snode_moduleswhile the server bundle carried its own class.instanceofcompared two different objects and was always false → real 500. Branded both classes withSymbol.forand swapped all 23instanceofsites forisRedirect()/isHttpError(). - 🟠 A hook's raw 3xx on a data request becomes
{redirect, status}JSON.fetchfollows a 303 silently, so the router used to receive the login page's HTML at 200.Locationships verbatim —redirect()already rebased it throughwithBase(), so rebasing again would double aBASE_PATHprefix. - 🟠 The router stopped reading "not JSON" as "the loader crashed".
readDataResponse()readsredirected, the status and the content-type that were sitting on the response all along and were thrown away by.catch(() => null). A hook'stext/plain404 now renders a 404, not a 500. - 🟡
event.isDataRequest(SvelteKit parity) tells a hook which kind of request it has, for the cases that genuinely differ. Authorization is not one of them —urlis the same either way on purpose. - 🟡 The parse is keyed on the incoming
Requestin aWeakMap, sinceevent.urlno longer carries the marker and hooks callresolve(event)themselves. Handingresolve()a fabricatedRequestdetaches it — documented onHandle, pinned by a test rather than papered over with a path sniff that could not fire anyway. - 🟠
test/hooks-redirect.test.tsboots a built server whose fixture guard is verbatim the one the docs teach — a normalising guard would have dodged the leak and passed against the broken build. 11 of its 13 assertions verified red beforehand, the security one included. - 🟡
apps/demogained/guard-test. Verified in Chrome against both builds: unfixed, the click renders the panel and itsrahasiasentinel to a signed-out visitor; fixed, both the click and the hard load land on login with a clean console.
bosia 0.9.4 (2026-09-05) — `page.url` was invented during SSR
Every server-rendered page reported pathname
/.bosia-page-shellandbosia-sidebarboth teach active-nav state frompage.url.pathname, so each page shipped with the homepage highlighted until hydration corrected it.
- 🟠 Two hardcoded fallbacks stood in for data the renderer already had:
page.svelte.tsreturnedhttp://localhost/with nowindow, androuter.currentRouteinitialised to"/".renderer.tshas the realurland already passes it to everyload()andmetadata(). - 🟠 One
renderWithPageContext()wrapper seedsrouter.currentRoute/.originandappState.routeParams, then renders — at all fourrender()sites, the two error paths included. Partial seeding would leave an error render holding the previous request's URL: real cross-request leakage, worse than a wrong-but-fixed default. - 🔴 The seed is browser-space, not app-space.
server.tsstripsBASE_PATHonce at the top ofhandleRequest, buthydrate.tsassigns rawwindow.location.pathnameandclientRoutescarry the prefix. Seeding the stripped path would render/profiland let hydration flip it to/sso/profil— the same jump, inverted.withBase(currentBase(), …)puts it back. - 🟡 A deliberate exception to the rule in
App.svelteandappState.svelte.ts(server singletons hold no per-request state):pageis imported directly by user components, so props cannot reach it. Safe only because bosia destructuresrender()synchronously — svelte'sRenderOutputisPromiseLike, so this is bosia's commitment, not svelte's. The wrapper makes it structural. - 🟡
page.paramswas{}on the server for the same reason —appState.routeParamsis never written during SSR. Seeded frompageDataFull.params, the sourceApp.sveltealready reads. Dropped the write-onlyrouter.paramscell (routeris not public API). - 🟠
test/ssr-page-url.test.tsboots a built server: pathname, full href, active-nav state, params, sequential and concurrent requests, and both error paths. Verified 8 of 9 failing before the fix withReceived: "/". A client-side test would agree with both the broken and fixed code — the fallback never fires in a browser. - 🟠
test/basePath-server.test.tsgained the mounted case, asserting the prefixed path. Verified failing with an app-space seed (data-path="/profil").BASE_PATHmust be set at build time too, so it extends the existing fixture rather than standing up a second build. - 🟡
apps/demonav now sources active state frompage.url.pathnameper bosia-page-shell R6, plus a/page-url-testroute printing the server-rendered values. Verified in Chrome against the built server: SSR HTML and post-hydration DOM agree, no console warnings, no flash of "Home".
bosia 0.9.3 (2026-09-02) — client nav only synced half the head
Reported from Fisika: after an in-app link,
canonicalandog:titlestill held the homepage's values. Two defects — the data endpoint dropped the fields, and the router applied only the two that survived.
- 🟠
/__bosia/data/*.jsonsendsmeta,linkandlangalongsidetitle/description. Explicit whitelist, not a spread:metadata.datafeedsload()server-side and may hold secrets, so it stays off the wire. - 🟠 The router rebuilds the head from that payload instead of poking
document.titleand one description tag.metadataTags()stampsdata-bosia-metaon what it owns, soheadExtrasand<svelte:head>output are never touched. - 🟡 A route with no
metadata()clears the previous page's tags rather than inheriting them — matches what a hard load of that URL renders. A failed fetch still leaves the head alone. No title kept the old one; 1.2.1 uses the default. - 🟠
test/formAction-metadata.test.tsasserts the data endpoint carries every head field and omitsmetadata.data. Verified failing on the old server before the fix — the DOM half is straight-linecreateElement, the wire was the root cause.
bosia 0.9.2 (2026-08-22) — `metadata()` was skipped on form-action re-renders
Reported from Fisika: after a plain (non-
enhance) form POST the tab title flipped toBosia Appon two unrelated routes.renderPageWithFormDatanever calledloadMetadata()and rendered through abuildHtmlthat had no metadata parameter at all — so the title was only the visible part of the loss.
- 🔴 Form-action re-renders resolve
metadata()like GET does.metadataTags()extracted frombuildMetadataChunkand shared withbuildHtml, so the streaming and non-streaming renderers can't drift on this again. Metadata sits ahead of<svelte:head>on both paths — first<title>wins. - 🟠
metadata.datareachesload()on POST. It was hardcodednull, so a loader consuming it silently returned different content after a submit — the one part of this that was never cosmetic.metadata.langand pluginhead/bodyEndfragments restored on the same path. - 🟠
redirect()/error()thrown insidemetadata()work.loadMetadatacaught everything, so every caller'sRedirect/HttpErrorbranch was unreachable dead code; both are now re-thrown ahead of the catch-all log. - 🟡 Error pages title themselves
404 — Not Foundinstead ofBosia App. Synthesized only when the error component set no<title>, so a custom+error.sveltestill wins. The route's ownmetadata()is deliberately not run — the route may be what failed. - 🟠
test/formAction-metadata.test.tsboots a real built server and submits a real form: title, description, og tag,lang,metadata.data, GET parity,redirect()frommetadata(), and the 404 title. A unit test onbuildHtmlwould have passed the whole time the bug shipped — the missing piece was the call site.
bosia 0.9.1 (2026-08-18) — closing out `BASE_PATH`
The six items 0.9.0 left open. One was a live bug: with
BASE_PATHin.env.production,build.tscallsloadEnv()after its imports, so the import-timepaths.BASE_PATHwas""while the lazycurrentBase()was/sso— CSS silently kept origin-rooturl()s.
- 🟡 One base source.
paths.BASE_PATHdeleted;twHash.ts,html.ts,server.ts,prerender.tsandclient/base.tsall readcurrentBase(). Fixes the.env.productiondivergence:twHash.tsnow reads post-loadEnv. - 🟡
basePathstamped intomanifest.jsonat build; the server warns on boot when it disagrees.!== undefinedso olderdist/artifacts stay silent. - 🟠
navigate()warns in dev when a path matched no route but would have with the prefix — the one choke point every goto/link/form falls through. Warn only; the router still converts nothing. - 🟡 Dev SSE opens
${base}/__bosia/sse(hydrate.ts) — the last hardcoded root among the injected copies. - 🟡
srcset/imagesrcsetrebased per candidate, descriptors and spacing intact. A value containingdata:is skipped whole — its commas are not separators. - 🟠
test/basePath-server.test.tsboots a real built server under/sso: off-mount 404,__BOSIA_BASE__, trailing-slash 308,redirect()Location, prerender output. The gap both 🔴s shipped through.
bosia 0.9.0 (2026-08-17) — `BASE_PATH`: mount an app under a sub-path
Found in SSO Fisika: three apps share one origin (
fi.uinsgd.ac.id,/obe,/sso) because no subdomain was available. Serving from a prefix was impossible — route paths are compiled root-absolute into the client bundle, everyredirect()emits a root-absoluteLocation, and nothing in the framework rewrote a leading segment. An nginx-only attempt (strip +proxy_redirect+sub_filter) was verified in a real browser and fails two ways: app-authoredhref="/daftar"walks off to the neighbouring app, andhydrate.tsmatcheslocation.pathnameagainst a route table that has no prefix, so the app mounts into a permanent "Loading…".
- 🔴
BASE_PATH=/sso— normalized incore/basePath.ts(pure, env-free, shared by both bundles so the two halves of the router cannot disagree).server.tsstrips the prefix once, before anything reads a pathname; off-mount requests 404. - 🔴
clientRoutesare generated with the prefix (routeFile.ts), so the route table, an anchor's href and the address bar are all the same strings. The client router does no conversion at all: a click pushes exactly the URL that was in the link.serverRoutesstay app-space, since the server strips at the edge and app code should never learn it is mounted. - 🔴 Root-absolute literals in
.sveltemarkup are rewritten at compile time (svelteCompiler.ts),<script>left alone. This is what makes the href in the DOM correct for a crawler, a middle-click and "copy link address" — an earlier version fixed only the server-rendered HTML and let the client re-render write the origin-root path back. - 🟠
Redirectrebases in its constructor — one choke point, so everyredirect()in every app needs no change. Validation runs first: a base must never launder a target the validator rejects. - 🟠 Rendered markup rebased (
href/src/action/formaction+ CSSurl()), never the JSON data islands — those are loader output and a path rewrite would corrupt them. Coverssrc/app.htmlviainterpolateSegment. - 🟠 Compiled Tailwind CSS rebased at build time (
twHash.ts, before hashing). A@font-facesrc inside a.cssfile is out of reach of any markup rewrite, and the miss is silent — fallback font, blank icon, nothing in the console. - 🟠 Cookie default path is the mount, resolved per-jar rather than at import. On a shared origin this is what stops two bosia apps from overwriting each other's
session. - 🟡
baseexported frombosia, for URLs built in<script>— the compile-time rewrite only sees markup. Verified against the Fisika design system:Icon.sveltebuildsurl("/icons/…")in a$derivedand needs it. - 🔴 Trailing-slash 308 was emitting an app-space
Location(server.ts), so/sso/about/bounced the visitor to/about. AtrailingSlash: "always"route did it on every request. Rebased — it was the oneLocationnot coming fromRedirect. - 🔴 Prerendering never ran under a base:
prerender.tsfetched the spawned server at the origin root, which now 404s, so/_healthnever answered and the step bailed with "server failed to start" — without failing the build. Prefixed the fetch base; the files written stay app-space, which is what the server looks them up by. - ⚪
svelteCompiler.tsmasked<script>blocks with literal NUL sentinels, which made git treat the whole module as binary — no diffs, no blame. Swapped for an HTML comment.
The six items left open here were closed in 0.9.1 above.
bosia 1.1.0 (2026-09-28) — success-only action data is wiped by the default `enhance`
Found in Ujiku (2026-08-07), building
/akun/profil: the save worked and the row was written, but the "Perubahan tersimpan." message never rendered. A successful action that returns data for theformprop silently loses it under the defaultuse:enhance.
client/enhance.ts:53setsappState.form = result.data, then (withinvalidateAllon, the default) bumpsappState.invalidationTickat:60. That tick fires the nav effect inclient/App.svelte, which doesappState.form = nullat:144before refetching the loader. The data is wiped between the assignment and the render — no error, no console warning, the UI just never shows it.Why it stays hidden: the
failurebranch (enhance.ts:48-51) setsappState.formand returns early without invalidating, sofail()messages always render fine. Only success-with-payload is affected — and most actions return{ ok: true }and redirect, so nobody looks.
- 🟠 Don't null
appState.formon the form's own invalidation. Fixed in 1.1.0 (84763a4):App.svelteclears it only when pathname+search changes. -
🟡 Dev warning whennot needed, the data is no longer discarded.invalidateAlldiscards successdata -
🟡 Documentnot needed: defaultinvalidateAll: falsevs latching in the forms guideupdate()keepsformand refreshes loaders. - ⚪ Ujiku (
akun/profil,akun/keamanan) latches success into local$statebeforeupdate(). Can drop the latch on bosia ≥1.1.0.
bosia 0.8.16 (2026-08-07) — `$env` export list shouldn't depend on the mode
Found in Ujiku CI (2026-08-07):
bun run checkfailed withModule '"$env"' has no exported member 'PUBLIC_GTM_ID'. Runtime values already come fromprocess.envas designed — butenvCodegen.tsderives the export names from whateverloadEnv(mode)found, so a runtime var vanishes from$envwhen its file isn't loaded. CI has only.env.production, andbosia syncruns dev mode unlessNODE_ENV=production.
- 🟠
loadEnv()harvests names from every root file matching/^\.env(\..+)?$/(incl..env.example, excl..envrc); values still only from the mode's four files. Name-only keys stay out ofprocess.envso?? defaultfallbacks survive. - 🟡 Test:
.env.productionalone still yields thePUBLIC_*export (empty string), no.envneeded. Plus mode-scoped values,.env.examplenames,.envrcignored, shell env fills a name-only key, malformed off-mode file throws. - 🟢
guides/environment-variables(EN+ID) "Names vs values" section +bosia-envR4/R5 —.env*is the name manifest; a host-supplied var still must be declared orbun run checkfails.
docs 0.8.8 (2026-07-30) — Purge stale `ui/icon` refs
The
<Icon>wrapper died in2bdf95d(2026-06-06) but its references outlived it.bosia-dashboardandbosia-mobile-screenstill ranbosia add … ui/icon— a registry id that no longer exists, so the CLI fails mid-workflow.
- 🟠
ui/icondropped frombosia-dashboard+bosia-mobile-screen(frontmatterrequires.componentsand thebosia addline). No replacement dep —@lucide/svelteis npm, and registry components declare it in their ownnpmDeps. - 🟡
bosia-design-reviewP1 item +references/checklist.md§11 rephrased around@lucide/svelte; noted it's a convention to check, not something a wrapper enforces. - 🟡 Sidebar docs (EN + ID) taught the dead
<Icon name="home">API for a snippet that wants a lucide component — now<House size={16} />with a real import. - 🟢 "Migrating from the old
<Icon>wrapper" section deleted fromcomponents/ui/icon.md(EN + ID); thenpmDepsnote folded into Install. Docs now have zero mentions of the old API. - 🟢
bosia-iconadded to the skills index table — it was on disk and served by the API, but absent from the human-facing list. - 🟢 Drifted counts fixed against
registry/index.json: 61 components, 162 blocks, 23 pages, 19 themes (both READMEs +LandingPage.svelte).
bosia 0.8.15 (2026-07-30) — One server per port
Found in Ujiku (2026-07-29): two
bun run startprocesses sat on :9000 at once, no error. Elysia's Bun adapter defaults toreusePort: true(elysia/dist/adapter/bun/index.js:163-190), soSO_REUSEPORTlet the second bind succeed and the kernel load-balanced between two differentdist/builds — a save's side effect silently didn't happen, guarded routes 500'd on the old build only. Cost hours to spot.
- 🟠 A second server on a busy port fails loudly with the offender's pid and exits 1. Delivered as a catch on the failed bind, not a pre-probe: no TOCTOU race, smaller diff, and it covers every boot path (Docker, PM2, systemd), not just the CLI.
- 🟡
reusePortoff inserver.ts's served config — the kernel is the source of truth. Applies to prod, the dev inner app server and the prerender build server.bosia startnow propagates the child's exit code. - 🟢
BOSIA_REUSE_PORT=1opts back in, for the N-workers-one-port case Elysia's default was meant for. Docs + skill + CLI reference updated. - 🟢
pidsOnPort()extracted tocore/port.tsfromdev.ts's inlinelsof; shared by the reaper and the refusal message. One test.
bosia 0.8.14 (2026-07-25) — Static-host hash-nav fix
- 🟢 Hash-only nav guard in
App.svelte: same-page TOC anchors scroll only, no loader refetch/POST — fixes error page on static hosts (Cloudflare Assets).
bosia 0.8.14 (2026-07-25) — Perf Phase 3: prerender
Phase 3 of 4 (plan: snazzy-dream) — parallelize
prerenderStaticRoutes. Items 1–3 only.
- 🟢
detectPrerenderRoutes: read files + run dynamicentries()imports concurrently viaPromise.all(was two serial loops). All warnings/skip semantics preserved. - 🟢 Bounded worker pool (
PRERENDER_CONCURRENCY, default 6) over prerender fetches; per-target try/catch keeps one failure from aborting the batch. - 🟢 Health poll checks first, sleeps 50ms only on failure — drops the guaranteed 200ms floor.
- ⚪ Item 4 skipped: mtime-skip of the
cpSyncstatic mirror — marginal gain on a once-per-build copy, real staleness risk.
bosia 0.8.13 (2026-07-23) — CSRF webhook exemption
- 🟢
CSRF_EXEMPT_PATHSenv var: exempt server-to-server webhook paths (no Origin/Referer) from the CSRF origin check. Exact or path-boundary match; exempt routes self-authenticate. Docs + tests.
bosia 0.8.12 (2026-07-19) — Perf Phase 1: runtime hot path
Phase 1 of 4 (plan: quizzical-storm) — per-request wins on the production hot path.
- 🟢 Map-based exact-route lookup in
matcher.ts— O(1) before the dynamic/catch-all linear scan. - 🟢
/dist/+/__bosia/static requests short-circuit before the API scan. - 🟢 Sync fast path in
resolveApiMatch— non-.jsonpaths skip the microtask hop. - 🟢 Lazy cookie parsing; skip page-module import on cache hits via build-time
cacheflag (flag removed in 1.2.8, seerouteCacheFlag); module-levelTextEncoder; alloc-free CSRF origin check. - 🟢 Deferred from item 6: Set-based
LoaderDeps(ripples into serialization) + CORS origin Set (opt-in, tiny list) — not worth it on 1-5 element lists.
docs 0.8.6 (2026-07-17) — Registry gap-fill Phase 4: newsletter + consent + SEO
Phase 4 of 4 (plan: mellow-jingling-dragon) — the registry gap-fill is complete.
- 🟢
registry/blocks/{newsletter/{centered,split},consent/banner}— signup forms POST to/api/newsletter; consent bar persists accept/decline in localStorage withonDecisionhook. - 🟢
registry/features/newsletter/— dialect-split subscribers table (email unique, lowercased), duplicate-safe subscribe,POST/GET /api/newsletter; drizzle append-line. - 🟢
registry/features/seo/— no DB:/sitemap.xml,/robots.txt(staging noindex),/rss.xmlfed bysrc/features/seo/config.ts;PUBLIC_BASE_URLenvVar, request-origin fallback. - 🟢 Docs:
blocks/{newsletter,consent}.md,guides/{newsletter,seo}.md+id/mirrors, 2 demos, nav;bosia-sections→ 27 blocks / 13 families;bosia-seopoints atfeat seo.
docs 0.8.6 (2026-07-17) — Registry gap-fill Phase 3: gallery + process + errors
Phase 3 of 4 (plan: mellow-jingling-dragon): purely frontend — no feature. Gallery/process section blocks + standalone error pages.
- 🟢
registry/blocks/{gallery/{grid,masonry},process/{steps,timeline}}— dialog-lightbox grid, CSS-columns masonry (no JS), numbered steps row, milestone timeline. - 🟢
registry/pages/errors/{not-found,server-error,maintenance}— standalone single-file pages, zero deps. - 🟢 Docs:
blocks/{gallery,process}.md,pages/errors.md(one doc for all three) +id/mirrors, 3 demos, nav, overviews (Sections 21→25);bosia-sectionsskill → 24 blocks / 11 families.
docs 0.8.6 (2026-07-17) — Registry gap-fill Phase 2: blog
Phase 2 of 4 (plan: mellow-jingling-dragon): blog blocks + pages + a drizzle-backed blog feature, so the
bosia-blogskill composes real registry blocks instead of scaffolding from scratch.
- 🟢
registry/blocks/blog/{post-list,post-header,post-body,related}— editorial post cards, header with byline + cover, 70ch prose wrapper (html/ snippet / sample viaui/typography), related row. - 🟢
registry/pages/blog/{index,post}— navbar/footer compositions; optionalposts/post/bodyHtml/relatedprops match the feature loaders' shape. - 🟢
registry/features/blog/— dialect-split posts table (slug unique, title, excerpt, body, cover, tag, published_at), repository, service, loaders for/blog+/blog/[slug](404 unknown slugs), seed003_blog; drizzle append-line. - 🟢 Docs:
blocks/blog.md,pages/blog/{index,post}.md,guides/blog.md+id/mirrors, 3 demos, nav, overviews (+Blog category, nine→ten);bosia-blogskill now requires the blocks and points atfeat blog.
docs 0.8.6 (2026-07-17) — Registry gap-fill Phase 1: contact + about + team
General-purpose sites hit registry gaps at once: no contact/about/team. Phase 1 of 4 (plan: mellow-jingling-dragon); Phases 2–4 (blog, gallery/process/errors, newsletter/consent/SEO) await approval.
- 🟢
registry/blocks/{contact/{split,simple},team/{grid,spotlight}}— form sections POST JSON to/api/contact(actionprop overrides); team cards + founder spotlight. - 🟢
registry/pages/company/{contact,about}— composed from navbar/hero/stats/team/cta/footer blocks. - 🟢
registry/features/contact-form/— dialect-split submissions table, repository, service (plain validation),POST/GET /api/contact; drizzle append-line. - 🟢 Docs:
blocks/{contact,team}.md,pages/company/{about,contact}.md,guides/contact-form.md+id/mirrors, 4 demos, nav;bosia-sectionsskill → 20 blocks / 9 families. - 🟢 Overviews backfilled (rule: every registry item appears in its overview): blocks overview +Footers/+Sections/+counts, pages overview +Landing/+Company;
id/mirrors (id blocks overview gained the whole catalog). - ⚪
cards/pick-listandstats/sentiment-barnamed in overview but have no section on their category doc pages (cards/utility.md,stats.md). - 🟢 Phase 2 — blog blocks/pages/feature (shipped, see Phase 2 section).
- 🟢 Phase 3 — gallery/process/error pages (shipped, see Phase 3 section).
- 🟢 Phase 4 — newsletter/consent/SEO (shipped, see Phase 4 section).
docs 0.8.6 (2026-07-17) — Docks: mobile bottom-nav blocks
Bosia had 19 top navbars but no mobile bottom navigation. Ported the Dockline design into a new
docksblock category, adapted to semantic tokens +@lucide/svelte.
- 🟢
registry/blocks/docks/{edge,floating,pill,fab}/— self-contained<nav>tab bars; active tabbg-primary/10 text-primary, FABbg-primary, count/dot badges; registered inregistry/index.json. - 🟢 Docs —
blocks/docks.md(+DocksDemowired into slug route), overview section,bosia-docksskill.
0.8.10 (2026-07-16) — Dev DX trio (rebuild filter, cached route scan, health-poll reload)
The three v0.2.1 DX items: dev rebuilt on
.md/test/editor-temp edits, re-walkedsrc/routesevery build, and slept a flat 200ms before broadcasting reload.
- 🟡
core/devWatch.ts(new) —shouldIgnoreForRebuild(md/test/spec/swap files) +affectsRouteManifest(only+files & dirs undersrc/routesmatter);test/devWatch.test.ts. - 🟡
core/dev.ts— watcher + mtime poll skip ignored files;routesDirtyflag →BOSIA_SKIP_ROUTE_SCANenv on the build spawn (always scans in managed mode);Bun.sleep(200)→waitForAppHealthy()polling/_health(50ms, 10s cap). - 🟡
core/build.ts— when flagged, reuses previousroute-manifest.json(read before cleanup) instead ofscanRoutes(); falls back to a real scan if missing. - ⚪ Known ceiling: apps importing
.mdfromsrc/lose live-reload for those files; drop.mdfrom the ignore list if it bites.
0.8.9 (2026-07-14) — `create` into current/existing folder
createrequired a name and refused any existing dir. Users wantcreate ./ no-name to scaffold in place.
- 🟠
packages/bosia/src/cli/create.ts—./omitted → cwd; project name = cwd basename; existence guard → emptiness guard (ignores.git/.DS_Store); threadprojectName(not path) through copy/prebuilt/finish; drop misleadingcdwhen in place. - 🟢 Docs
reference/cli.md(+id) updated to[name]optional syntax.
0.8.8 (2026-07-11) — Inspector: AI popover clamped to viewport
openForm()placed the popover at a fixedr.bottom+6— off-screen for elements near the bottom of the page/iframe (rukoku editor embed).
- 🟠
plugins/inspector/overlay.ts— mount at 0,0, measureoffsetWidth/Height, flip above when no room below, clamp both axes to the (iframe) viewport with 8px margin. - ⚪ Known ceiling: position computed once at click; no scroll/resize reposition (popover is short-lived).
0.8.8 (2026-07-11) — `__BRAND__` guard reaches all templates
The 0.8.3 brand guard lives in
bosia sync, but only thedefaulttemplate'scheckscript ran sync — demo/shop/store (rukoku apps) passedbun run checkwith__BRAND__still in the footer.
- 🟠
templates/{demo,shop,store}/package.json—checknow runsbosia sync &&first (also fixes tsc checking stale.bosia/codegen). - ⚪ Existing scaffolded apps keep their baked
checkscript; fix is the same one-line edit per app.
0.8.7 (2026-07-10) — `export const snapshot` (page state on back/forward)
SvelteKit-parity
snapshot = { capture, restore }in+page.svelte. Mirrors scroll restoration: captured at the same save points keyed byhistory.state.bosiaIndex, restored post-tick()after the destination mounts; persisted tosessionStorageon unload (JSON-serializable values only).
- 🟠
core/client/router.svelte.ts—pageSnapshotsmap +savePageSnapshot()at the foursaveScroll()sites;pendingSnapshothandoff (popstate + reload-restore ininit()); separate sessionStorage flush so a non-serializable snapshot can't kill scroll persistence. - 🟠
core/client/App.svelte—bind:this={pageInstance}on both<PageComponent>spots;router.getPageSnapshotclosure;settleSnapshot()insettleScroll()+ first-hydration branch. - ⚪
Snapshot<T>type inbosia/client+ generated$types.d.ts(routeTypes.ts);routeTypes.test.tsassertion; docsguides/navigation(en+id) "Snapshots" section;bosia-navigationskill R2b;CHANGELOG.mdappended. - ⚪ Deliberately skipped: layout snapshots (need per-depth instance refs in the recursive layout snippets); non-JSON values (no devalue); inherits the
beforeunloadmobile page-discard ceiling from scroll.
0.8.7 (2026-07-10) — `setHeaders()` in load functions
SvelteKit-parity
setHeaders(headers)on the server-loader event. One accumulator shared across layout+page loaders (cross-loader dup detection); keys lowercased;set-cookieand duplicates throw → existing 500 path. Headers land on SSR HTML, the response-cache write, form-action re-renders, and/__bosia/data/*.json. Data endpoint: loadercache-controlwins over the computed default unless cookies were accessed — thenprivate, no-cachewins (privacy beats intent).
- 🟡
hooks.ts—LoadEvent.setHeaderstype +makeSetHeaders()(lives here so tests avoidbosia:routes);renderer.tswires it into both loader events, returnsloaderHeaders, spreads onto response sites + cache write. - ⚪
compress()+serveCached()base keys lowercased so extraHeaders override instead of comma-joining. - ⚪
server.tsdata endpoint destructuresloaderHeadersout of the JSON body, merges with cookie-privacy override. - ⚪
test/setHeaders.test.ts; docsguides/server-loaders(en+id) incl. missingdependsrow;bosia-routingskill R3b; roadmap check-off;CHANGELOG.md; version0.8.7. - ⚪ Deliberately skipped: headers on error/redirect responses; prerender = silent no-op; masked data requests don't re-set headers.
0.8.6 (2026-07-05) — Scroll restoration on back/forward
Browser auto-restoration fired at popstate time, before the SPA rendered the destination — it clamped against the wrong document height (worse with
+loading.svelteskeletons). Router now owns it:scrollRestoration="manual", position saved per history entry, restored after the nav settles.
- 🟠
core/client/router.svelte.ts— manual mode; per-entry index inhistory.state.bosiaIndex; save on push/hash-push/popstate/unload;pendingScrollhandoff; sessionStorage persistence + reload restore. - 🟠
core/client/App.svelte— sharedsettleScroll()replaces the two duplicated top-scroll blocks; popstate branch restoresrouter.pendingScrollaftertick(). - ⚪ Docs
guides/navigation(en+id) "Scroll behavior" section;bosia-navigationskill notes it's built-in (never hand-roll inafterNavigate).CHANGELOG.mdappended. - ⚪ Split off:
export const snapshot(form/UI state restore) — shipped 0.8.7. - ⚪ Known ceiling: scroll save relies on
beforeunload, which mobile page-discard can skip (router.svelte.ts:192) — addvisibilitychange/pagehidefallback if reports come in. - ⚪ Known ceiling: restore is a single post-
tick()jump; images/async content loading later can shift the position (App.svelte:78) — re-clamp on load events if it bites.
0.8.6 (2026-07-05) — Unify dev/prod component CSS (drop the collision workaround)
Investigated the open 0.8.5 Bun-collision item. It does NOT reproduce on Bun 1.3.14: CSS-chunk hashes now include source-module identity, and shared CSS hoists to one chunk (proved via isolated
Bun.buildharness). Prod was never at risk — its client compiler usescss:"injected"(styles in JS, no chunking). Only the dev inspector usedcss:"external"+ a hand-rolled runtime<style>injection to dodge the (now-fixed) collision — functionally identical toinjected, just more code.
- 🟠
plugins/inspector/bun-plugin.ts— inspector compile nowcss: generate==="client" ? "injected" : "external", mirroringsvelteCompiler.ts. Deleted thevirtualCssmap, theVIRTUAL_NSvirtual-module resolve/load handlers, and the runtime injection block (~30 lines); dropped unusedbasenameimport. - ⚪
test/svelte-build.test.ts— new dev-inspector case: many+pageroutes sharing one styled component → build succeeds, zero CSS chunks, scoped keyframe present in JS. - ⚪ Version bump
0.8.6+CHANGELOG.md.
0.8.5 (2026-07-04) — Content-hashed Tailwind CSS delivery
Prod served
/bosia-tw.csswith no Cache-Control (browser heuristic caching → stale styles after deploys); dev used?v=Date.now().
- 🟠 Tailwind output →
dist/client/bosia-tw-<sha256:10>.css(temp file + rename viatwHash.ts);twfield in dist/manifest.json; all 4 html link sites viatwCssLink(), no cache buster; fallback/bosia-tw.cssfor old artifacts. Immutable caching free viaHASHED_BASENAME. - 🟡 CSS output collision under
splitting: true— RESOLVED in 0.8.6. Doesn't reproduce on Bun 1.3.14 (hash now includes module identity + shared CSS hoists); won't file upstream. Dev switched tocss:"injected", workaround removed.
docs 0.8.2 (2026-07-03) — Footers + marketing sections
Registry had 19 navbars and 17 heros but zero footers and nothing for the middle of a landing page. Net-new design across four phases.
- Phase A —
footers/*(12 blocks): 6 standard + 6 themes, docs, demos, nav group. - Phase B — 7 section families (16 blocks): pricing, testimonials, faq, cta, features, stats, logos.
- Phase C —
pages/landing/*(2 pages): saas + simple. - Phase D — skills (
bosia-footers,bosia-sections) + landing/saas/pricing skill repoints + indexes.
0.8.4 (2026-07-03) — Cache security fixes (framework review S-findings)
Review of the response-cache + static-serving layers found identity-hash collision + memory-DoS risks and 3 perf issues. Discovered during planning:
Bun.brotliCompressSyncdoesn't exist — brotli variants were never built (fixed by P1).
- 🔴 S1 — identity hash FNV-1a-32 → SHA-256 (64-bit hex); collision could serve user A's page to user B.
- [~] S2 — anonymous-identity write gate — CANCELED during planning. Replaced by: [x] loud runtime warning (dev+prod, once per name) when a cached response's loader read a cookie not in
CACHE_KEYS, + docs callout in response-cache guide. - 🟠 S3 —
CACHE_MAX_BODY_BYTESper-entry cap (default 2MB,0unlimited); guard incacheSet+ early-skip before compression. - 🟡 P1 — brotli via
node:zlibquality 5 (also un-breaks brotli entirely). - 🟡 P2 — prerendered pages: boot-time manifest (
buildPrerenderManifest) instead of per-requestexists()probes. - 🟡 P3 — percent-encoded static paths 404 fixed (
decodeURIComponentinlookupStatic+ dev fallthrough). - 🟠 Identity-aware dedup — data-endpoint dedup key now includes the
CACHE_KEYSidentity hash (computeCacheKey), so per-user routes dedup safely. BREAKING:(private)no longer skips dedup; custom session cookies must be inCACHE_KEYS. - 🟡 P4 — LRU eviction is O(evicted.tags), not O(all tags):
LRU.setreturns the evicted{key,value}socacheDeleteIndexOnlyusesentry.tagsdirectly. - 🟠 Header-auth cache-isolation caveat — startup banner + response-cache docs now say the uncovered-cookie warning cannot see request headers; custom auth headers must be in
CACHE_KEYS. - 🟡 Auth
nextround-trip —registry/features/authlogin/register now honor?next=viasafeNext(same-site paths only, else/dashboard); was always forcing/dashboard. - 🟠 Route
scopedeleted — zero runtime consumers after identity-aware dedup;(private)is an ordinary route group (scanner/types/ambient/codegen field removed). - 🟠 Miss coalescing —
coalesceMissgate in cache.ts wired into SSR + API paths; N concurrent misses on one key build once, waiters re-check after the leader's microtask write. - 🟡
CookieJar.peek— identity hashing no longer flipsaccessed/readNames, so it can't forceCache-Control: private(latent bug on all cached paths).
docs 0.8.1 (2026-07-02) — Storefront e-commerce expansion (reviews, cart, wishlist, search, account)
The storefront covered browse/buy but nothing around it. Five phases add the real-world pieces: PDP reviews, cart page + wishlist (first favs-API UI), search, account area, quick-view + mega-menu.
- 🟠 Phase A —
blocks/storefront/reviews(summary bars, list, working form) wired intopages/storefront/product; docs product family + demo. - 🟠 Phase B —
empty-state,cart-lines,wishlist-gridblocks;pages/storefront/{cart,wishlist};order-summarygains actalabel prop; docs family + 2 page docs + 3 demos. - 🟠 Phase C —
search-overlayblock +pages/storefront/search(header already hadonSearch); layout family docs + search page docs + demo wiring. - 🟠 Phase D —
store/orders.tssamples;account-nav,order-list,order-detail,address-book,account-settings;pages/storefront/account(client-side section switching); account family docs + page docs + 2 demos. - 🟡 Phase E —
quick-viewmodal (+product-cardonQuickVieweye button),mega-menustandalone panel; product/layout/catalog docs + demo wiring.
docs 0.8.1 (2026-07-02) — Extract Mode Switcher component
The dark/light/system toggle lived inline in the navbar and couldn't be reused. Extract it as a standalone
mode-switchercomponent. Also fixes a bug: a stored theme was read into state on load but never applied, so the theme visually reset on refresh.
- ⚪
registry/components/ui/mode-switcher/— new component (cycle button), apply theme on init. - ⚪ Navbar consumes
<ModeSwitcher />; theme logic + Button import removed; dep →ui/mode-switcher. - ⚪ Register in
registry/index.json; docs page + demo + overview + nav entries. - ⚪
bosia-theme-tokensskill — "Runtime light/dark switch" note so agents reach forui/mode-switcher.
0.8.3 / docs 0.8.1 (2026-07-02) — `__BRAND__` placeholder + guard
AI agents install navbar/storefront blocks but forget to rename the hardcoded brand ("Mercato", "Brand"), shipping someone else's name in the nav/header/footer. A real-looking word doesn't trip the "replace me" reflex. Fix: rename the literal to an unmistakable
__BRAND__sentinel, failbosia sync(→bun run check) while any survive, and teach the block-compose skill to rename it.
- 🟠
registry/blocks/navbars/*/block.svelte(19) +storefront/{header,footer}— brand text →__BRAND__. - 🟠
packages/bosia/src/core/brandGuard.ts+cli/sync.ts— scansrc/, fail on leftover__BRAND__. - ⚪
bosia-block-composeskill — R8 + checklist gate for the placeholder. - ⚪ Version bump
bosia 0.8.3+docs 0.8.1+ bothCHANGELOG.md. - ⚪ Docs landing redesign — shared
LandingPage.svelte(EN/ID), code-forward hero, live registry showcase. - ⚪ Chart tooltip — draw with native SVG rect/text, not
<foreignObject>; kills Chrome's ghost-box repaint bug.
0.8.2 (2026-07-01) — Host-managed dev mode
A multi-tenant host (rukoku) edits app source then watches the dev server self-trigger a rebuild on every file event — and the watcher can compile a half-written file mid-edit, wedging the preview with a stale-manifest 500. Add an opt-in mode where the host is the single clock: it drives one build per turn via
/__bosia/rebuildand reads the real{ ok }result. Default unset → normalbosia devbyte-for-byte unchanged.
- 🟠
packages/bosia/src/core/dev.ts—BOSIA_DEV_MANAGED=1gate; skip thefs.watch+ mtime-poll starts. - 🟠
packages/bosia/src/core/dev.ts—buildAndRestart()returnsPromise<boolean>(real build result). - 🟠
packages/bosia/src/core/dev.ts—POST /__bosia/rebuildsibling of hold/resume, returns{ ok }. - ⚪ Version bump
0.8.2+CHANGELOG.md.
0.8.0 (2026-06-30) — `+loading.svelte` route skeletons
Navigating between pages leaves stale content on screen until the router atomically swaps in the new page+layouts (
App.svelte). Move the per-app skeleton trick into the framework as a Next.js-style convention: a+loading.sveltein a route folder renders automatically during nav, nested inside the layouts shared with the current page. No app-side resolver, nobeforeNavigatewiring.
- 🟠
packages/bosia/src/core/types.ts—PageRoute.loading: string | null. - 🟠
packages/bosia/src/core/scanner.ts— detect sibling+loading.svelte; update conventions header. - 🟠
packages/bosia/src/core/routeFile.ts— emitloadingimport +layoutPathsidentity keys in bothclientRoutesgenerators. - 🟠
packages/bosia/src/core/client/App.svelte— load + render the skeleton on real path changes under the shared-layout prefix; reset on settle. - 🟠
packages/bosia/src/core/server.ts— fallback manifest synthesis includesloading: null. - ⚪
test/scanner.test.ts— detect-vs-null+loading.sveltecase. - ⚪ Docs (en+id):
guides/routing(Loading Skeletons),guides/navigation(cross-ref),project-structuretable,reference/sveltekit-differencesrow. - ⚪ Skills:
bosia-routing(R5b + checklist + source),bosia-navigation(R9 + source). - ⚪ Version bump
0.8.0(framework + docs) + bothCHANGELOG.md. - ⚪ Follow-up: nearest-ancestor
+loading.svelteinheritance (full Next.js segment semantics) — currently per-folder only. - ⚪ Follow-up: warm the
+loading.sveltechunk on hover/viewport inprefetch.tsso the skeleton paints instantly on click. - ⚪ Follow-up: cold-import lag for non-hover nav (touch/keyboard/programmatic) on first visit — skeleton shows after the chunk downloads; self-heals on 2nd visit. Fix only if noticeable: touchstart/focusin warm triggers, or idle warm-all.
0.7.8 (2026-06-27) — Inspector: drop the breadcrumb header
The alt+click comment form showed the framework-stripped component chain (
Button.svelte → +page.svelte) as a header above the textarea. Meaningless noise for non-technical end users (e.g. rukoku's). The AI still gets full file/tree context viabuildContext(el)in the payloadcomment, so the visible header is pure clutter — remove it.
- 🟠
packages/bosia/src/core/plugins/inspector/overlay.ts—openForm()no longer builds thechain/header;form.innerHTMLstarts at the<textarea>.buildContext/submitunchanged, AI payload unaffected. - ⚪ Version bump
0.7.8(framework) +CHANGELOG.md.
0.7.7 (2026-06-23) — Per-response frame-guard opt-out
The framework re-applies
X-Frame-Options: SAMEORIGINto every response after the userhandleruns, so a proxy hub can't serve an embeddable preview even after stripping the upstream header. Add a per-response opt-out so the guard stays on the hub's own pages.
- 🟠
packages/bosia/src/core/hooks.ts— exportNO_FRAME_GUARD_HEADER(internal marker header). - 🟠
packages/bosia/src/core/server.ts— when a response carries the marker, strip it and skip onlyX-Frame-Options(other security headers stay). - ⚪
packages/bosia/src/lib/index.ts— re-exportNO_FRAME_GUARD_HEADERfrom the public API. - ⚪ Version bump
0.7.7(framework) +CHANGELOG.md.
0.7.5 (2026-06-21) — `store` template (Postgres + MinIO/S3)
shopbakes SQLite + empty S3 vars. Add a siblingstoretemplate that defaults to Postgres (nativeBun.SQLviadrizzle-orm/bun-sql, no driver dep) and MinIO/S3 uploads — the production-shaped stack — socreate … --template storeneeds no post-scaffold rewiring.
- 🟠
packages/bosia/templates/store/— copy ofshop;template.jsonall dialectspostgres;.env.examplePostgres URL + MinIO defaults;instructions.txt+README.mdreworded;data/dropped (S3 uploads, no SQLite file). - 🟠
packages/bosia/src/cli/create.ts— registerstoreinTEMPLATE_DESCRIPTIONS. - ⚪ Docs —
getting-startedtemplate table +reference/cli--templateline & bullets (en + id). - ⚪
bosia-shop-templateskill — cover thestoresibling (postgres default, no driver/@aws-sdkdeps). - ⚪ Version bump
0.7.5(framework) /0.7.8(docs).
0.7.4 (2026-06-16) — Navbar block family (18 `navbars/*` blocks + `bosia-navbars` skill)
The registry shipped hero and auth families but no navigation bars. This ports the standalone Navbar Stock design system (18 specimens across standard, themed and app/interactive layouts) into a new
navbars/block category, mirroring the heros port end-to-end. The hardcoded acid-lime accent collapses toprimary; dark bars invert tobg-foreground text-background, glass usesbackdrop-blur-xl. Theme-agnostic semantic tokens only, so each navbar restyles across all 19 themes — no new theme added.
- 🟠 18 navbar blocks under
registry/blocks/navbars/*— standard ×6, themes ×6, app ×6; self-contained<header>sections, inline Tailwind on semantic tokens, inlined primitives. Registered inindex.json. - ⚪ Docs — 3 grouped family pages (
blocks/navbars/{standard,themes,app}) with stacked live previews (3 demos registered); nav gains a Blocks → Navbars group. - ⚪ New
bosia-navbarsdesign skill (catalog of all 18, golden rule, token map, icon list,[[bosia-navigation]]/[[bosia-heros]]cross-links) + skills-index row (count 51 → 52). - 🟠 Stripped the embedded site navbar from all 17
heros/*blocks — page-level blocks must not carry navbar chrome (clashed with layoutnavbars/*). Rule documented inbosia-heros/bosia-block-composeR7/bosia-page-shellR1. - 🟠 Added
navbars/overlay(family now 19) — transparent absolute navbar with light text + outline CTA floating over a full-bleed hero photo. Registered; added to Themes docs +NavbarsThemesDemo; exception documented. - ⚪
CHANGELOG.md— appended under 0.7.4.
0.7.3 (2026-06-15) — Login/auth page family (new `auth/*` pages + 9 blocks + `bosia-login` skill)
The registry shipped a storefront page family but no auth/login pages — only a single
cards/loginblock. This ports the standalone Login Design System (12 themes, centered + split layouts, the full sign-in/up/forgot/magic-link/OTP/SSO screen set) into Bosia as a visual-only page family: 9 reusableauth/*blocks (semantic tokens, brand →primary, status → emerald/amber/destructive, social brand logos keep their official colours) and 6 pages composed from them. One card, two layouts (centered ↔ split) via a one-linevariantswap, mirroring the storefrontpurposepattern. Backend stays inbosia-auth-flow— pages note the pairing.
- 🟠 9 blocks under
registry/blocks/auth/*—auth-shell(centered ↔ split),auth-card,brand,social-row,divider,auth-field,password-strength,otp-input,form-message. Registered inindex.json. - 🟠 6 pages under
registry/pages/auth/{login,register,forgot,magic-link,otp,sso}/— composition only; eachmeta.jsonlists itsauth/*block deps so the installer recurses; defaultvariant="centered", swappable tosplit. - ⚪ Docs — new
blocks/authpage + 6pages/auth/*pages (7 demos registered), Pages overview gains an auth section, nav gains Blocks → Auth and Pages → Auth groups. - ⚪ New
bosia-logindesign skill (catalog of the 6 pages + 9 blocks, token map, voice, centered↔split swap,[[bosia-auth-flow]]pairing) + skills-index row (count 50 → 51). - ⚪
CHANGELOG.md— appended under 0.7.3.
0.7.3 (2026-06-15) — Prebuilt template artifacts for fast `bosia create`
bosia create --template shopwas slow:installFeaturefetched 150+ files serially from GitHub raw. Fix: CI bakes the finished scaffold into a version-locked Release tarball;createdownloads + extracts it. Registry path stays the fallback.
- 🟠
packages/bosia/templates/shop/template.json— opt in with"prebuilt": true(only heavy templates need it;default/demohave notemplate.jsonand are already instant). - 🟠
cli/create.ts— whenprebuilt && !--local && !BOSIA_BUILDING_PREBUILT, downloadv<version>/<template>.tar.gz, extract, substitute{{PROJECT_NAME}}, restore.env; falls back to registry on 404/offline. - 🟠
scripts/build-prebuilt-templates.ts(new) — iteratetemplates/*, skip unlessprebuilt; scaffold with literal{{PROJECT_NAME}}+--no-installvia local registry;tar -czf dist/prebuilt/<t>.tar.gz. - ⚪
packages/bosia/package.json—build:templatesscript; version0.7.3. - ⚪
.github/workflows/publish.yml— Bun setup +bun run build:templates; attachdist/prebuilt/*.tar.gzto the release. - ⚪
.github/workflows/refresh-prebuilt.yml(new) — registry/template changes without a version bump rebuild artifacts andgh release upload --clobberonto the existing release. Keeps the create fast path in sync. - ⚪ Docs —
reference/cli.mddocuments the fast prebuilt download + registry fallback +--no-install.
0.7.2 (2026-06-14) — Brief intake: defer ALL installs to after approval
Bug: during Quick-start intake the AI ran
bosia_add_theme/bosia_add_blockbeforebrief_request_approval, so the Setuju button never appeared. Fix: intake is now capture-only; all installs move to a new step 9 after BRIEF.md is complete.
- 🟠
bosia-brief-intake/SKILL.md— steps 3.3/3.4 now record theme/first-screen choices (no install); new step 9 "Install NOW" runs after status complete; step-5 approval gate hardened; R5 bans installs before complete. - 🟠
bosia-brief-visual/SKILL.md— reworded to "records, doesn't install"; "Workflow side effects" → "Deferred install — NOT during intake" (run by intake step 9); checklist splits recording from the add-theme checks. - 🟠
bosia-brief-platform/SKILL.md— same treatment: description reworded; "Workflow side effects" → "Deferred install — NOT during intake"; format-helper + block scaffolds run in intake step 9; checklist gate annotated. - 🟠
bosia-brief-intake/SKILL.md(+example-brief.md) —## Todoseed now carries TWO items: "Redesign login & register" + new "Replace mock data with real DB integration". Step 6, BRIEF.md template, and example brief updated.
0.7.1 — Inspector: structured AI payload (labeled fields)
The alt+click → "Send to AI" comment now carries labeled fields —
url,pageFile(nearest+page/+layout),componentleaf, cappedtext, framework-strippedtree— so the AI traces props back to the page, not the leaf.
- 🟠
inspector/overlay.ts—isFrameworkFrame/fileOf/userFrames/findPageFile/ownText/buildContexthelpers;submit()sendsbuildContext(el) + "---" + comment; form header shows the framework-stripped chain. - ⚪ Docs —
guides/inspector.mdaiEndpointsection rewritten with the labeled format + whypageFilematters. - ⚪ Skill —
bosia-inspector-edit/SKILL.mdrewritten to the new[Inspector]contract (split on---, default topageFileover thecomponentleaf, useurl/textto disambiguate).
0.7.0 — Deprecated `page.params` fallback
Re-adds a deprecated, working
paramsgetter on thepageobject (bosia/client) so legacy code reaching forpage.paramskeeps working. Returns reactiveappState.routeParams, dev-only warn-once, removed in 1.0.0.$props()stays taught.
- 🟠
page.svelte.ts— addget params()returningappState.routeParams;@deprecatedJSDoc + dev-onlyconsole.warn(warn-once, guarded byNODE_ENV !== "production"so it tree-shakes out of prod). - ⚪ Docs —
guides/routing.mddeprecated callout;reference/roadmap.mdline updated;reference/changelog.mdentry. Skills left as the correct$props()pattern.
Shop template defaults to sqlite-file
Flips the
shoptemplate from PostgreSQL to built-in SQLite (sqlite://./data/app.db) — zero-config, no DB server. Per-feature dialects already supported sqlite; this only changes template defaults plus an ENOENT fix fordata/.
- 🟠
packages/bosia/templates/shop/template.json— all fivefeatureOptionsdialectspostgres→sqlite. - 🟠
packages/bosia/templates/shop/.env.example—DATABASE_URL→sqlite://./data/app.db;instructions.txtwording → SQLite. - 🟠
registry/features/drizzle/drizzle-index.sqlite.ts—mkdirSync(dirname(path))before opening the file (skip:memory:) sodb:migrateagainst./data/app.dbdoesn't ENOENT. - ⚪
_gitignoreignoresdata/*.db*; shipdata/.gitkeep. Docs/skills (bosia-shop-template) updated postgres-default → sqlite-file.
0.6.25 — Port the Mercato storefront (new `page` tier + 24 blocks + clay theme)
Ports the Mercato React storefront. Adds a new
pageregistry tier (page = group of blocks, no backend) viabosia add page. 24storefront/*blocks compose 4 pages sharing one runes cart, mapped to existing themes + a newclaytheme.
- 🟠 New
pageregistry tier + CLI —cli/page.ts(runAddPage), routed inaddRouter.ts(+pages/...alias);pagesadded toManifestandRegistryIndex;bosia add listshows pages; dispatch tests. - 🟠 Harvested 24 sections under
registry/blocks/storefront/*—store(runes cart/favs/drawer + catalogue), header/footer, home sections, product card/grid/drawer, listing + PDP + checkout blocks. Registered inindex.json. - 🟠 Added the
claytheme (registry/themes/clay/) — warm paper neutrals, terracotta primary, soft warm shadows, Newsreader/Hanken Grotesk/Geist Mono fonts; registered inregistry/index.json(19 themes). - 🟠 Four pages under
registry/pages/storefront/{home,listing,product,checkout}/— composition only, one sharedcreateCart()wired through header/grids/drawer; eachmeta.jsonlists its block/ui deps so the installer recurses. - ⚪ Docs — new Pages section + 4 page pages, 6 grouped
blocks/storefront/*pages,themes/clay; 10 demos; nav groups; newbosia-storefrontskill;$lib/blocks/$pagesaliases +registry/pagesTailwind@source.
0.6.24 — Port the Hero Stock hero system (17 blocks)
Ports the Hero Stock React design system (17 full-bleed hero sections across 6 verticals) into a new
heros/block category. Hardcoded accents collapse toprimary; dark photo heroes invert. Semantic tokens only — no new theme.
- 🟠 Ported 17 hero blocks under
registry/blocks/heros/*(commerce ×7, education ×2, food ×2, fashion ×2, services ×2, saas ×2); inline Tailwind on semantic tokens, pickers as local$state, kept Unsplash images. Registered. - ⚪ Docs — 17 per-hero pages (each backed by its
meta.jsonso/api/blockslists each with its own install line); single-hero previews; newbosia-herosskill; nav grouped by vertical; skills-index row.
0.6.24 — Port the Cardstock card system (29 blocks + 6 themes)
Replaces the single
cards/feature-editorialblock (now removed — it painted its numeral withaccent, so it never followed the brand). The new cards map the brand colour toprimaryand use semantic tokens only, so they restyle across themes.
- 🟠 Extended the theme contract on all 12 existing themes — added
--font-monoand theme-scoped elevation (--shadow-xs/sm/md/lgwired through--elevation-*per:root/.dark; midnight gets a faint primary glow). - 🟠 Added 6 Cardstock themes —
paper,carbon(brutalist hard-offset shadows),bloom(diffuse tint),terminal+grape(dark glow),sage; each with light + dark, registered inregistry/index.json(18 themes) + docs pages + nav. - 🟠 Ported 29 card blocks under
registry/blocks/cards/*(data, people, commerce, media, utility, auth); inline Tailwind on semantic tokens, status colours shadcn-style, sparkline/ring/mini-bars as inline SVG. Registered. - 🟠 Removed
registry/blocks/cards/feature-editorial/; re-pointed tests, CLI help, docs demo/page, and thebosia-landing/bosia-saas-landing/bosia-block-composeskills tocards/feature(now takestitle/body/icon/ctaprops). - ⚪ Docs — 6 per-category card pages with live demo galleries + per-card install lines; new
bosia-cardsskill cataloguing all 29.
0.6.24 — Remove the Todo template + feature ahead of rebuilding card blocks
The
todostarter template (and itstodofeature/components) was the original CRUD demo. We're rebuilding the card blocks from scratch, so it's being pulled to clear the way. Theeditorialcard block stays until replacements exist.
- 🟠 Deleted
packages/bosia/templates/todo/,registry/features/todo/,registry/components/todo/, anddocs/content/docs/components/todo/. - 🟠
registry/index.json— droppedtodofromfeaturesand the fourtodo*componentsentries. - 🟠
packages/bosia/src/cli/{create,index,add,feat}.ts— removed thetodotemplate description/example and re-pointed the staletododoc-comment examples at still-present features. - ⚪ Docs — removed the Todo template rows and component pages from
getting-started.md,components/overview.md,reference/cli.md,guides/inspector.md, thenav.tsTodo group, and thebosia-block-composeexample.
0.6.24 — Three-state theme switcher (Light / Dark / System)
Theme switching was binary (light/dark) and the logic was duplicated across four slightly-different places.
- 🟠
core/html.ts— extracted the inline FOUC bootstrap (duplicated 4×) into oneTHEME_INIT_JSconstant; now resolves an explicitsystemvalue live againstprefers-color-scheme, compatible with storeddark/light. - 🟠
navbar.svelte— replaced the buggyisDarkboolean (never persisted, out of sync with FOUC) with a 3-statelight/dark/systemcycle that persists tolocalStorage, syncs the DOM, follows live OS changes. Sun/Moon/Monitor. - 🟠
docs/src/lib/components/ThemeToggle.svelte+docs/src/routes/+layout.svelte— mirror the same 3-state logic and system-aware FOUC script (Monitor inline SVG added). - ⚪
docs/content/docs/guides/styling.md+docs/content/docs/components/ui/navbar.md— document the three modes, storage values, and cycle pattern. - ⚪ Bumped
svelteto^5.56.3in the 5 stalepackage.jsonfiles (docs, 4 templates); root,packages/bosia, andapps/demowere already current.
0.6.24 — `add theme` strips the template's default `:root`/`.dark` block
A theme's
tokens.cssdefines its own:root/.dark, but the template'sapp.cssships a default block earlier in the file. Afteradd themeboth coexisted at the same specificity and the template's won.
- 🟠
templates/{default,shop,demo}/src/app.css— wrapped the default:root/.darktoken block in/* bosia-theme-vars */sentinels so removal targets only the template defaults, never user:rootrules.@theme {}stays above, untouched. - 🟠
cli/theme.ts—runAddThemenow callsstripTemplateThemeVars(appCssPath)after wiring the@import, removing the sentinel-bounded block (idempotent). The installed theme's:rootbecomes the only one, so its tokens win.
0.6.23 (2026-06-11) — Surface yesterday's shop-template bugs in skills + ban the `postgres` npm pkg
0.6.22 shipped the
shoptemplate and patched 5 install-blocking bugs. Three reflect cross-cutting gotchas future authors will hit again, and the shoppackage.jsonstill carried a stalepostgresdep.
- 🟠
core/dev.ts— the proxy's "App server is starting…" 503 is now an HTML page carrying the/__bosia/ssereload client (was bare text/plain), so a live-reload racing into a rebuild auto-recovers instead of sticking. - 🟠
core/dev.ts— reload-hold control:POST /__bosia/holdsuppresses the SSE reload broadcast,POST /__bosia/resumeflushes one reload if any rebuild fired.holddoubles as a heartbeat (90s safety timer) - 🟠
packages/bosia/templates/shop/package.json— drop unused"postgres": "^3.4.0"; scaffold uses Bun-nativedrizzle-orm/bun-sqlagainstBun.SQL, no userland driver needed. - 🟠
bosia-bun-runtime/SKILL.md— new## Postgres — Bun.SQLsection with the object-form snippet + Bun 1.3.xFailedToOpenSocketgotcha; banned-packages table gainspostgres/pgrows pointing atBun.SQL. - 🟠
bosia-database-setup/SKILL.md— reversed the misleading "bun add postgres" line into a "no install" directive cross-linking[[bosia-bun-runtime]]; R5 gains a one-liner about the URL-string gotcha. - 🟠
bosia-drizzle-feature/SKILL.md— new R11 ("Registry files use import paths from their target location") coveringmeta.json#files[].targetdepth, with theauthfeature's 3-up import as the example; P0 gate added. - 🟠
sidebar-menu-item.svelte—hasChildren = $derived(!!children && !href)so leaf items with a{#snippet icon()}body don't render as collapsible parents (Svelte 5 fillschildrenfrom whitespace around named snippets). - 🟠
features/shop/admin-sidebar.svelte— Orders is now a parent groupingAll orders/Pending/Refundsunder oneSidebarMenuItem. Logo<img>tags gainh-5 w-5so the SVG renders at 20px instead of overflowing. - ⚪
bosia-shop-template/SKILL.md— new skill bundling eight rules for extending a shop scaffold: don't re-install features, edit sidebars in place, nopostgres/pg/@aws-sdk, routes under(private)/dashboard/, services notdb.select - 🟠
core/server.ts— API-route handlers now treatthrow redirect(...)/throw error(...)like page actions (303 / typed JSON) instead of a generic 500; also supportsreturn redirect(...). Found via shopPOST /logout. - 🟠
templates/{default,shop,todo,demo}/src/app.css— add@custom-variant dark (&:where(.dark, .dark *));. Tailwind v4 defaultsdark:toprefers-color-scheme, so the navbar'sclassList.toggle("dark")was a no-op. - 🟠
features/shop/admin-sidebar.svelte— header rebuilt perSidebarDemo: theme-aware logo button doubling as a collapse toggle, brand row, working<SidebarTrigger> - 🟠
registry/features/shop/meta.json— add"ui/breadcrumb"tocomponentssobosia create … --template shopactually scaffolds the breadcrumb files. - 🟠
templates/shop/src/routes/(private)/+layout.svelte— derivesegmentsfrompage.url.pathname, render<Breadcrumb>above{@render children()}; last segment becomes<BreadcrumbPage>, earlier ones<BreadcrumbLink>. - 🟠
packages/bosia/templates/shop/src/routes/(public)/+page.svelte— add a<svelte:head>with<title>Welcome to your shop</title>+<meta name="description">so the home page has basic SEO instead of an empty<head>. - 🟠
features/auth/login-page.svelte+register-page.svelte— sibling<meta name="description">inside the existing<svelte:head>(login: "Sign in to your account."; register: "Create your account. First account becomes admin."). - ⚪ CLI-internal bugs (block-deps 404 routing,
--localflag drop, dialect default underskipPrompts:true) deliberately omitted — code-only fixes, no public API to document. - 🟠
cli/block.ts— session-levelinstalledSet mirrorsadd.ts:40;runAddBlockearly-returns when a block is already installed this session. Without itfiles/upload-areawas re-written during shop scaffold (two pullers). - ⚪
registry/features/shop/meta.json— dropui/typography(unused) andui/form/ui/input/ui/button(already declared byauth; component installer dedupes viaadd.ts:116so removal is safe).
0.6.22 (2026-06-10) — `shop` template + `auth` / `rbac` / `shop` registry features
Templates bottomed out at
todo(one CRUD feature), but the most common "build me an app" prompt is a storefront.
- 🟠
registry/features/auth/— multi-dialectuser/sessiontables,Bun.password.hashargon2id. - 🟠
registry/features/rbac/—permissiontable with composite PK,can(userId, resource, action, scope?)with*wildcards,resources.tsregistry,auth-handle.tsrewritten to attachlocals.can(...), seed grants*to the first user. - 🟠
registry/features/shop/— multi-dialectproduct/order/order_item/cart_itemtables,valibotvalidators, repositories + services,PublicNavbar+AdminSidebar,resources.append.tsadds eightshop.*permissions. - 🟠
templates/shop/— thin glue:template.jsondeclaresfeatures: ["auth","rbac","file-upload","shop"] - ⚪
packages/bosia/src/cli/create.ts—TEMPLATE_DESCRIPTIONSmap gainsshop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart". - ⚪
docs/content/docs/getting-started.md— template table gains ashoprow.
0.6.22 (2026-06-10) — Sidebar docs + skill (fix three AI mis-uses) + `DropdownMenu` floating mode
AI agents hit three sidebar failures: (1) wrapping leaf
SidebarMenuItems inDropdownMenuswallowed thehref; (2) skipping the user footer.
- ⚪
components/ui/sidebar.md— new "Choosing the right item shape" table with a "never wrap inDropdownMenu" callout; new "User Footer" section. - ⚪
bosia-sidebar/SKILL.md— new skill. STOP block names the three failures. R1–R7 cover leaf/parent shape, no-foreign-trigger,iconsnippet,bind:collapsed, user-footer, floating/side/align,trigger="hover". P0 checklist gates. - 🟠
dropdown-menu.svelte— context exposestriggerEl/setTriggerEl. Triggerbind:thissyncs viauntrack - ⚪
SidebarDemo.svelte— leading "Dashboard" leaf withLayoutDashboardicon (R1); user footer rebuilt withDropdownMenu+Avatar,justify-starton the trigger (fixes chevron),floating side="top" align="start"on the content.
0.6.21 (2026-06-09) — Fix three AI-agent app-building failures (block install, EACCES, `page` export)
AI agents hit three failures: (1)
bosia add blocks/cards/feature-editorial404'd (CLI only routedblocksingular); (2)bosia add ui/typographyaborted on EACCES overwriting a foreign-uid file.
- 🟠
packages/bosia/src/cli/addRouter.ts— new dispatcher; routesblocks/<cat>/<name>tokens torunAddBlock, splits mixed batches (components + blocks) into onerunAddcall plus per-block calls. - 🟠
packages/bosia/src/cli/index.ts—addcase callsrouteAddwith injected runners; help text adds the alias. - 🟠
packages/bosia/src/cli/registry.ts— newwriteRegistryFile(dest, content)helper does unlink + retry on EACCES/EPERM, then surfaces a chown hint if still failing. - 🟠
packages/bosia/src/cli/add.tsandblock.ts— component/block file write loops route through the new helper. - 🟠
core/client/page.svelte.ts(new) — reactivepageobject backed by$derivedoverrouter.currentRoute; exposespage.url/page.pathname. Noparamsgetter — Bosia already plumbsparamsinto+page/+layoutvia$props(). - 🟠
packages/bosia/src/lib/client.ts— re-exportpage. - 🟠
docs/content/skills/bosia-block-compose/SKILL.md— canonicalbosia add block cards/feature-editorialexample. - 🟠
docs/content/skills/bosia-saas-landing/SKILL.mdandbosia-landing/SKILL.md— split single install line into per-category calls (theme / components / block). - 🟠
docs/content/skills/bosia-svelte-runes/SKILL.mdR6.5 — illustrative "removed import" example switched frompageto a deletedcnutility so the lesson no longer contradicts the now-realpageexport. - ⚪
packages/bosia/test/registry.test.ts— coverage forrouteAdddispatch (block, alias, mixed batch, multi-block, plain components),readRegistryJSONblocks-category path,writeRegistryFilehappy-path/overwrite. - ⚪
packages/bosia/test/page-store.test.ts(new) — compile-and-wiring checks for thepagemodule (compiles viasvelte/compiler, output referencesderived,bosia/clientre-exportspage).
0.6.21 (2026-06-09) — `bosia-image-dialog` skill + block (multi-image picker)
AI-generated apps seed
images.unsplash.complaceholders but there was no UI primitive to swap them without clobbering. Newfiles/image-dialogblock composesUploadArea+ an External URL tab + an existing-images gallery.
- 🟠
registry/blocks/files/image-dialog/—block.svelte(Dialog + Tabs + selection state, embedsUploadArea, fetches/api/filesonce on mount) andmeta.json(deps: button, dialog, input, label, sonner, tabs, upload-area). - 🟠
registry/index.json— addfiles/image-dialogtoblocks. - ⚪
docs/content/skills/bosia-image-dialog/SKILL.md— workflow steering, P0/P1 checklist, anti-patterns (single-image flows, id-vs-url storage, merge-with-stale-state). - ⚪
docs/content/skills/bosia-file-upload/SKILL.md— cross-reference the new dialog for replace-existing / gallery flows. - ⚪
docs/content/docs/blocks/files/image-dialog.md+docs/src/lib/components/demos/FilesImageDialogDemo.svelteregistered indocs/src/routes/(docs)/[...slug]/+page.svelte. - ⚪
docs/src/lib/docs/nav.ts— insert Image Dialog under Blocks → Files.
0.6.21 (2026-06-09) — Drawer component (mobile bottom-sheet)
Drawer was the last unbuilt Priority-2 overlay. Mobile action sheets had no first-class component.
- 🟠
registry/components/ui/drawer/— 8 svelte sub-components (drawer, content, trigger, close, header, title, description, footer) +index.ts+meta.json - 🟠
registry/index.json— addui/drawer. - 🟠
docs/content/docs/components/ui/drawer.md— usage, props, sub-components, accessibility, Drawer-vs-Dialog guidance. - 🟠
docs/src/lib/components/demos/DrawerDemo.svelte+ register indocs/src/routes/(docs)/[...slug]/+page.svelte. - 🟠
docs/src/lib/docs/nav.ts— insert Drawer entry under UI children. - ⚪
docs/content/skills/bosia-drawer/SKILL.md— workflow steering for AI agents (mobile-first contract, P0/P1 checklist). - ⚪
backup/COMPONENT_PLAN.md— flip Drawer to[x].
0.6.19 (2026-06-08) — `.webmanifest` 404 + stale-build recovery
Two unrelated prod bugs on komba (pure-SSR) after the 0.6.18 deploy. (1)
/site.webmanifest404'd.
- 🟠
packages/bosia/src/core/server/staticServer.ts— add.webmanifestto theisStaticPathwhitelist. - 🟠
packages/bosia/src/core/build.ts:156-170— hash the client entry filename (naming.entry: "[name]-[hash].[ext]") sostaticManifestserves it asimmutableand per-build URL changes invalidate the browser cache automatically. - 🟠
core/client/hydrate.ts— add a production-onlyunhandledrejectionhandler that detects failed dynamicimport()and triggerslocation.replace(?_v=…). Loop-guard viasessionStorage["bosia:reload-attempt"](10s window). - ⚪ Follow-up: surface a stale-build event on the router (
onStaleChunkhook?) so apps can show a soft toast instead of a hard reload — defer until the safety net proves insufficient.
0.6.18 (2026-06-07) — pure-SSR apps still lost `public/` in production containers
Bug: komba (pure SSR, zero prerendered routes) on 0.6.17 —
/bosia-tw.css404'd becausegenerateStaticSite()early-returned whendist/prerendered/didn't exist, sopublic/never mirrored todist/static/. Fix: always mirrorpublic/
- 🟠
packages/bosia/src/core/prerender.ts—generateStaticSite()now copiespublic/unconditionally; SSG-specific copies (prerendered + client mirror) gated ondist/prerendered/existing. - 🟠 Test: build emits
dist/static/withpublic/contents even when no prerendered routes exist.
Same-day addition (2026-06-06) — replace custom `` wrapper with `@lucide/svelte`
The hand-curated
registry/components/ui/icon(95 inline SVG paths) plus 28 components with hardcoded<svg>duplicated work per glyph. Decision: drop the wrapper, import each icon from@lucide/svelte
- 🟠
apps/demo/package.json,docs/package.json— add@lucide/sveltedep. - 🟠
registry/components/ui/icon/— delete; removeui/iconfromregistry/index.json. - 🟠 Migrate 17 registry components (accordion, select, checkbox, pagination, calendar, carousel, sidebar, breadcrumb, command, date-picker, input-otp, radio-group, resizable, nav-menu, combobox, navbar.
- 🟠 Update 3 skill examples (
bosia-dashboard,bosia-mobile-screen,bosia-empty-states) and the docsSidebarDemo. - 🟠
components/ui/icon.mdrewritten as a@lucide/svelteguide with a deprecation callout;overview.md,navbar.md,upload-area.md,crop-image.mdcallouts updated;IconGrid.sveltedeleted. - ⚪ New
docs/content/skills/bosia-icon/skill steering AI agents toward@lucide/svelte(neverlucide-svelte).
Same-day addition (2026-06-06) — production runtime needed `src/app.html`
Bug from komba Dockerfile: runner copies only
dist/. Container boots →renderer.tsreadssrc/app.htmlfrom cwd → missing → throws.
Decision: emit parsed segments to dist/app-html.json during build; renderer reads dist first, falls back to src/app.html for dev/HMR. Zero app changes — dist/ is already in every Docker COPY.
- 🟠
packages/bosia/src/core/appHtml.ts— addwriteAppHtmlSegments(segments, outDir)(serializes to${outDir}/app-html.json);getAppHtmlSegments(cwd)now tries persisted artifact first, falls back toloadAppHtmlTemplate(cwd). - 🟠
packages/bosia/src/core/build.ts— after writing route-manifest, callwriteAppHtmlSegments(appHtml)so production runtime has the segments insidedist/.
0.6.17 (2026-06-07) — production runtime also needed `src/hooks.server.ts`, `bosia.config.ts`, and `public/`
Same komba Dockerfile incident. Once dist had
app-html.json, the app booted but every auth request 303'd to/login:server.tsreadsrc/hooks.server.tsfrom cwd with no fallback →locals.userundefined. Same forbosia.config.ts
Decision: extend the dist-first / src-fallback pattern (used for app-html.json) to hooks, config, and static assets. Build emits self-contained ESM artifacts under dist/
- 🟠
core/build.ts— newbundleRuntimeUserFiles(cwd)step after the server bundle: externalize npm + bosia/elysia/bun/svelte, Bun.buildsrc/hooks.server.ts→dist/hooks.server.jsandbosia.config.*→dist/bosia.config.js - 🟠
packages/bosia/src/core/server.ts— hook loader checks${OUT_DIR}/hooks.server.jsfirst, falls back tosrc/hooks.server.ts. Log line reports which path won. - 🟠
packages/bosia/src/core/config.ts—loadBosiaConfigchecks${OUT_DIR}/bosia.config.jsfirst, dynamic-imports it directly (skips Bun.build at server start); falls back to the existing compile-from-source path for dev. - 🟠
packages/bosia/src/core/staticManifest.ts— walk${outDir}/static/(mirror ofpublic/written by build) so prod images can ship onlydist/.addOncekeepspublic/canonical when both exist (dev double-walk).
Same-day addition (2026-06-04) — `parent()` returns `{}` on client-side JSON nav
Bug from komba:
(await parent()).farmIdworked on SSR but returnedundefinedon client-side nav (14 routes)
Decision: make parent() see the real parent chain even when layer loaders are skipped. Two viable shapes — pick one:
- A (server-side): when handling
/__bosia/data/*.json, accept aparentSnapshotspayload in the POST body (the client already has cached data per skipped layer); merge intoparentDatabefore the page loader. Lowest churn. - B (client-side): when a page loader is selected to re-run and any ancestor layout is skipped, re-run those layouts server-side too. Simpler invariant ("parent() always sees fresh data") but defeats the whole point of selective re-runs.
A is preferred. Plus a P0 doc/skill update so the workaround (locals-based farm/user scope) is the documented pattern even after the framework fix lands, since locals is also better for typed access.
- 🟠 Client: emit
parentSnapshots: Record<depth, data>for skipped layers. Done inApp.svelte(nav) +prefetch.tsvia a sharedbuildParentSnapshotshelper — POST with{ parentSnapshots }only when non-empty, else keep the cacheable GET. - 🟠
packages/bosia/src/core/server.ts—/__bosia/databranch parsesparentSnapshotsfrom the POST body (guarded try/catch → undefined on GET/malformed) and forwards intoloadRouteDatavia a new arg. - 🟠
core/renderer.ts—loadRouteDataacceptsparentSnapshots - ⚪
bosia-routing/SKILL.md— added a rule under R3: don't useparent()for scope identifiers (farmId/orgId/userId), read fromevent.locals;parent()is fine for view-layer data,localsis the source of truth for cross-loader scope. - ⚪ Regression test under
apps/demo: a layout server returning{ orgId }, a child+page.server.tsasserting(await parent()).orgId. Verify SSR AND a client-nav JSON fetch. (Not yet added — needs a client-nav harness.)
Same-day addition (2026-05-30) — file-upload private-by-default + skill rule
0.6.11 made
/uploads/<key>.webpreach+server.ts, but the default handler had no auth/ownership check andfilehad nouserId
- 🟠
registry/features/file-upload/file.{sqlite,pg,mysql}.table.ts— adduser_idNOT NULL (text for sqlite/pg, varchar(36) for mysql). - 🟠
registry/features/file-upload/file.repository.ts—getAllByUser(userId),getByKey(key),getOwned(id, userId),remove(id, userId)— ownership is part of every query. - 🟠
registry/features/file-upload/file.service.ts—upload(file, userId)stores userId;getAll(userId)filters;getByKey(key)exposes the row for the route's ownership check;remove(id, userId)rejects on ownership mismatch. - 🟠
registry/features/file-upload/api-files-server.ts,api-files-id-server.ts,uploads-static-server.ts— all gated onlocals.user; uploads-static responds withContent-Type: record.mime+Cache-Control: private, no-store. - 🟠
bosia-file-upload/SKILL.md— new R5.5 "Files are private by default", anti-patterns (remove auth check, repoint topublic/uploads, dropuser_id), P0 gates on 401 + cross-user 404 curl,[[bosia-auth-flow]]declared a hard prereq.
Same-day addition (2026-05-30) — API routes shadow static fallthrough
Bug from fotoku:
/uploads/<uuid>.webp404'd even with a+server.tsat/uploads/[...path]. Root cause: the static-files block (matches by extension) ran beforeresolveApiMatch, so any static-extension URL short-circuited into static lookup.
- 🟠
packages/bosia/src/core/server.ts— move the API-match block (+server.tsresolution + cache + error handling) above the static-files block and prerender block. No logic change inside the moved block. - ⚪
apps/demo/src/routes/uploads/[...path]/+server.ts,apps/demo/uploads/sample.webp,apps/demo/src/routes/(public)/uploads-test/+page.svelte— regression demo so/uploads-testrenders an image served via the +server.ts handler.
Same-day addition (2026-05-30) — fix `props_id_invalid_placement` in UI components
Svelte 5 requires
$props.id()to be the entire RHS of a top-levelconst. Nine UI components called it inside a template literal (const baseId = \tabs-${$props.id()}`;`), which throws at init.
- 🟠
components/ui/{accordion,collapsible,hover-card,menubar-menu,nav-menu-item,popover,sidebar-menu-item,tabs,tooltip}.svelte
Same-day addition (2026-05-30) — `bosia feat drizzle` defaults to sqlite-file, not in-memory
Bug: AI ran
bosia feat drizzleand gotsqlite://:memory:, losing data on restart. Three drifts:meta.jsonDATABASE_URLwas a comment string with:memory:last; the runtime fallback was:memory:
- 🟠
registry/features/drizzle/meta.json— single concreteDATABASE_URL=sqlite://./data/app.db, no inline comment options. - 🟠
registry/features/drizzle/drizzle-index.ts+drizzle.config.ts— runtime fallback nowsqlite://./data/app.db, notsqlite://:memory:. - 🟠
docs/content/skills/bosia-database-setup/SKILL.md— default scheme updated tosqlite://./data/app.db; references tosrc/features/drizzle/db.tscorrected toindex.ts(actual file shipped by the feature).
Same-day addition (2026-05-30) — UI ids use `$props.id()` instead of `crypto.randomUUID` / `Math.random`
Generated apps crashed with
crypto.randomUUID is not a functionover plain http (LAN IP, preview subdomains)
- 🟠
field,tooltip,popover,hover-card,nav-menu-item,menubar-menu— replacecrypto.randomUUID().slice(0, 8)with$props.id().menubar-menunow prefixes its id withmenubar-for consistency with siblings. - 🟠
registry/components/ui/tabs/tabs.svelte,sidebar/sidebar-menu-item.svelte,accordion/accordion.svelte,collapsible/collapsible.svelte— replaceMath.random().toString(36).slice(2, 10)base ids with$props.id(). - 🟠
registry/components/ui/alert-dialog/alert-dialog.svelte,dialog/dialog.svelte— collapse the two separateMath.random()calls into one$props.id()shared bytitleIdanddescriptionId. - ⚪ Server-side
crypto.randomUUIDinfeatures/file-upload/file.service.tsandfile.{mysql,sqlite}.table.tsleft as-is — runs in Node wherecryptois always available, and those ids persist to the DB rather than per-render.
Same-day addition (2026-05-30) — brief intake: drop DB question + approval-button tool + `bosia-database-setup` skill
Two pain points: (a) AI kept asking follow-up questions after the Quick Start batch (heading said "six", script listed 7, no rule against follow-ups)
- 🟠
bosia-brief-intake/SKILL.md— Quick Start now five questions (palette + aesthetic merged into Q5). Step 5 is the approval gate viabrief_request_approval - 🟠
bosia-brief-intake/references/quick-start-script.md— rewritten as a five-question script with the merged Q5 and an updated inference table. "After answers locked" now references thebrief_request_approvaltool call. - 🟠
bosia-database-setup/SKILL.md— new skill replacingbosia-brief-database. Triggers on user intent ("pakai postgres", "buat tabel"). Workflow A = engine swap; Workflow B = new table/column. R1 keeps sqlite-file the silent default. - 🟠
docs/content/skills/bosia-brief-database/— deleted; superseded bybosia-database-setup. - ⚪
docs/content/skills/SKILL.mdcatalog — count bumped 44 → 45; added abosia-database-setuprow to the "Conventions — framework ·" section. - ⚪
docs/content/skills/bosia-drizzle-usage/SKILL.md+references/troubleshooting.md— swapped everybosia-brief-databasereference tobosia-database-setup.
Same-day addition (2026-05-29) — fix in-page anchor link scroll
Bosia's SPA router intercepted every
<a>click, re-ran the full page load, thenscrollTo(0,0). Hash-only links like<a href="#features">never scrolled becausee.preventDefault()killed the browser default.
- 🟠
core/client/router.svelte.ts— short-circuit same-page hash nav in the click handler: whenanchor.pathname + anchor.searchmatches the current location and a hash is present, skipnavigate() - 🟠
core/client/router.svelte.ts— exportscrollToHash(hash)helper: decodes the fragment, resolvesgetElementById, callsscrollIntoView(), returns whether it found a target. Used by both the click handler andApp.svelte. - 🟠
core/client/App.svelte— replace unconditionalwindow.scrollTo(0,0)after nav settle withtick().then(() => scrollToHash(hash) || scrollTo(0,0))in both paths.
Same-day addition (2026-05-28) — new skills `bosia-page-shell` + `bosia-query-defaults`
AI agents kept (a) re-rendering navbar/footer in every
+page.svelteinstead of+layout.svelte, (b) hand-rolling<table>instead ofui/data-table, (c) shipping repositorylistwith nolimit/offset/orderBy. No skill said otherwise.
- 🟠
bosia-page-shell/SKILL.md— new skill. R1: chrome lives in+layout.svelte, never+page.svelte. R2 layout-depth table. R3 requires(private)/+layout.server.tsto producedata.user. R5 forbids hand-rolled<table> - 🟠
bosia-query-defaults/SKILL.md— new skill. R1 fixes the repo signature tolist(db, { limit, offset, orderBy?, where? })returning{ rows, total }. R2 defaults limit/offset/orderBy. R3 clamps limit ≤100. - ⚪
docs/content/skills/SKILL.md— catalog updated from 42 → 44 skills; new rows added to "Conventions — design ✦" (bosia-page-shell) and "Conventions — framework ·" (bosia-query-defaults).
Same-day addition (2026-05-28) — fix `feat` block install + non-interactive `add block`
AI agent ran
file_upload_installand got a silent 404 (the block path lives underblocks/, notcomponents/). The retry hung on@clack/promptsbecause the MCP runner never closes stdin. Framework fixes only — no per-app patching.
- 🟠
cli/feat.ts—FeatureMetagainsblocks?: string[]. After component install, iteratemeta.blocksand callrunAddBlockso block deps route to the block installer (was 404'ing throughaddComponent - 🟠
cli/block.ts—runAddBlockacceptsInstallOptions, gates the "Replace existing block?" prompt behind!skipPrompts, threadsoptionsinto recursiveaddComponentcalls, honors-y - ⚪
packages/bosia/src/cli/index.ts—add blockdispatch now passes through-y(was filtering to--long flags only). - ⚪
registry/features/file-upload/meta.json— movedfiles/upload-areaandfiles/crop-imagefromcomponentsto the newblocksfield. - 🟡
bosia-file-upload/SKILL.md— R5 now shows the explicitimport UploadArea from "$lib/blocks/files/upload-area/block.svelte"and full props; workflow step 5 gainsdb:generate && db:migrate
Same-day addition (2026-05-28) — fix `file-upload` end-to-end (Bun.Image, image host, route placement)
AI agent scaffolded
bosia feat file-uploadand hit three regressions: (1)Bun.Image.open/decodedoesn't exist, uploads crash; (2)PUBLIC_BASE_URLbakes the wrong host into stored URLs.
- 🔴
features/file-upload/file.service.ts— rewrite the Bun.Image pipeline. Drop thedecodeImageshim. Usenew Bun.Image(bytes), read dims frommetadata(), positionalresize(w, h, opts),.webp({ quality: 85 }).bytes() - 🟠
features/file-upload/storage-local.ts—save()returns relative/uploads/${key}(noPUBLIC_BASE_URLprefix). The browser resolves against the page origin → works forlvh.mepreview, prod domains, and localhost without env tuning. - ⚪
registry/features/file-upload/meta.json— drop misleadingPUBLIC_BASE_URL=http://localhost:3000default (now empty string). (F2) - 🟠
bosia-routing/SKILL.md— new R6 hard rule: authenticated UI MUST live under(private). Anti-pattern block(public)/admin/...❌ vs(private)/admin/...✅. Decision rule + P0 checklist entry. (F3a) - 🟠
docs/content/skills/bosia-dashboard/SKILL.md— STOP rule at top: files under(private)/; create(private)/+layout.server.tsif absent. (F3b) - 🟠
docs/content/skills/bosia-crud-flow/SKILL.md— STOP rule at top: resource routes under(private)/<resource>/...; admin CRUD never(public). (F3c) - 🟡
bosia-bun-runtime/SKILL.md— newBun.Imagesection: constructor,metadata(), positionalresize, per-format encoders (.webp({ quality: 0–100 })),.bytes(). Anti-pattern callout forBun.Image.open/decode. (F4) - ⚪
docs/content/skills/bosia-file-upload/SKILL.md— R2 cross-references the newBun.Imagesection inbosia-bun-runtimefor the API surface. (F5)
Severity: 🔴 Critical · 🟠 Major · 🟡 Minor · ⚪ Trivial
Completed (v0.0.1 – v0.1.26)
Click to expand completed items
Core Framework
- 🔴 SSR with Svelte 5 Runes (
$props,$state) - 🔴 File-based routing (
+page.svelte,+layout.svelte,+server.ts) - 🟠 Dynamic routes (
[param]) and catch-all routes ([...rest]) - 🟡 Route groups (
(group)) for layout grouping - 🟠 API routes —
+server.tswith HTTP verb exports - 🟠 Error pages —
+error.svelte
Data Loading
- 🔴 Plain
export async function load()pattern (no wrapper) - 🟠
$typescodegen — auto-generatedPageData,PageProps,LayoutData,LayoutProps - 🟠
parent()data threading in layouts - 🟠 Streaming SSR for metadata (non-blocking
load()) - 🟠 Form actions (SvelteKit-style)
Server
- 🔴 ElysiaJS HTTP server
- 🟡 Gzip compression
- 🟡 Static file caching (Cache-Control headers)
- 🟡
/_healthendpoint - 🟠 Cookie support (
cookies.get,cookies.set,cookies.delete) - 🟡 Cookie
sameSiteaccepts both casings (lax/Lax) — normalized to canonical header - 🟠 Protocol-aware
Securecookies — auto-downgrade over HTTP with warn;TRUST_PROXY=truehonorsx-forwarded-proto - 🟠 Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
- 🟡
DISABLE_X_FRAME_OPTIONS=trueenv var to omitX-Frame-Optionsfor intentional cross-origin iframe embedding - 🟠 Graceful shutdown handler (SIGTERM/SIGINT)
- 🟠
.envfile support with$envvirtual module - 🟡 CORS configuration (framework-level)
- 🟠 Session-aware fetch (cookies forwarded in internal API calls)
- 🟡 Request timeouts on
load()andmetadata()functions - 🟠 Route PUT/PATCH/DELETE through
handleRequest()— consistent CSRF, CORS, security headers, and cookie handling - 🟠 Graceful shutdown drain — drain in-flight requests before stopping; return 503 from health check during shutdown
- 🟡 Concurrent build guard in dev — prevent overlapping builds when rapid file changes trigger
buildAndRestart()while a build is already running - 🟡 Clean dev server shutdown — release
Bun.serve, file watchers, and timers on SIGINT so the event loop drains naturally; outerbun runreports exit 0 instead of 130 - 🟡 Single-^C stop — CLI wrappers survive SIGINT and wait for the child (no more orphaned server); dev skips the drain for instant exit; second ^C force-quits (130)
- 🟠 Dev watcher safety net — 5s mtime poll of
src/complementsfs.watchso atomic-write edits (temp + rename) that macOS drops still trigger rebuilds - 🟠 Dev crash backoff — replace the "stop after 3 crashes" silent-stop with exponential backoff (500ms → 5s) that never gives up, so a transient error or fixed source change brings the app back without manual restart
Security
- 🔴 XSS escaping in HTML templates — sanitize
JSON.stringify()output in<script>tags - 🔴 SSRF validation on
/__bosia/data/— validate route path segment - 🔴 CSRF protection — Origin/Referer header validation for state-changing requests
- 🟠 Strip stack traces from error responses in production
- 🟠 Request body size limits
- 🔴 Path traversal protection — validate static/prerendered file paths stay within allowed directories
- 🟡 Cookie parsing error recovery — wrap
decodeURIComponent()in try-catch - 🟡 Cookie option validation — whitelist/validate
domain,path,sameSitevalues - 🟠
PUBLIC_env scoping — only expose vars declared in.envfiles - 🟠 Streaming error safety — validate route match before creating stream
- 🟡
safeJsonStringifycrash guard — try-catch for circular reference protection - 🟠 Open redirect validation on
redirect() - 🟡 Cookie RFC 6265 validation — validate names against HTTP token spec; use
encodeURIComponentonly for values
Client
- 🔴 Client-side hydration
- 🔴 SPA router (client-side navigation)
- 🟡 Navigation progress bar
- 🟠 HMR via SSE in dev mode
- 🟡 Per-page CSR opt-out (
export const csr = false) - 🟡 Link prefetching —
data-bosia-preloadattribute for hover/viewport prefetch - 🟠 Fix client-side navigation with query strings/hashes
- 🟡 Use
insertAdjacentHTMLfor head injection — prevents re-parsing<head>, avoiding duplicate stylesheets and script re-execution
Build & Tooling
- 🔴 Bun build pipeline (client + server bundles)
- 🟠 Manifest generation (
dist/manifest.json) - 🟠 Static route prerendering (
export const prerender = true) - 🟠 Tailwind CSS v4 integration
- 🟠
$libalias →src/lib/* - 🟡
bosia:routesvirtual module - 🟡 Validate Tailwind CSS binary exists before build
- 🟡 Prerender fetch timeout
- 🟡 Fix
withTimeouttimer leak - ⚪ Remove duplicate static file serving
- 🟠 Static site output — merge prerendered HTML + client assets + public into
dist/static/for static hosting - 🟡 Validate
.envvariable names — reject invalid identifiers that break codegen - 🟡
.envparser escape sequence support — handle\n,\", etc. in quoted values
Routing
- 🟠 Dynamic route prerendering with
entries()export — enumerate dynamic route params for static prerendering
CLI
- 🔴
bosia dev— dev server with file watching - 🔴
bosia build— production build - 🔴
bosia start— production server - 🟠
bosia create— scaffold new project (with--templateflag and interactive picker) - 🟠
bosia add— registry-based UI component installation - 🟠
bosia feat— registry-based feature scaffolding - 🟡
bosia addindex-based path resolution — resolves component names fromindex.jsoninstead of blindly prefixingui/ - 🟡
bosia featnested feature dependencies —featuresfield in meta.json for recursive installation - 🟡
bosia featoverwrite prompt — asks before replacing existing files - 🟡
bosia addmulti-component install —bosia add button card inputinstalls all in one call - 🟡
bosia add -y/--yesflag — auto-confirm overwrite prompts for CI / scripts
Templates & Features
- 🟠
todotemplate (formerlydrizzle) — PostgreSQL + Drizzle ORM with full CRUD todo demo - 🟠
drizzlefeature —bosia feat drizzlescaffolds DB connection, schema aggregator, migrations dir, seed runner - 🟠 Multi-engine
drizzlefeature — adapter,drizzle.config.ts, and seed-runner branch onDATABASE_URLscheme (postgres, mysql, sqlite file, sqlite in-memory) over Bun's built-in drivers (no per-engine npm dep) - 🟠 Bun-native drizzle migrate runner —
src/features/drizzle/migrate.tsreplacesdrizzle-kit migratefor sqlite/postgres/mysql apps (drizzle-kit's sqlite migrate needsbetter-sqlite3 - 🟠
bosia-brief-databaseskill + hook intobosia-brief-intake— captures DB engine + connection during brief intake, writes## Databaseblock to BRIEF.md - 🟠
todofeature —bosia feat todoscaffolds todo schema, repository, service, routes, components, and seed data - 🟡
todocomponent —bun x bosia@latest add todoinstalls todo-form, todo-item, todo-list components - 🟡 Registry as single source of truth —
bosia create --template todoinstalls features from registry viatemplate.jsoninstead of duplicating files
Hooks & Middleware
- 🟠
hooks.server.tswithHandleinterface - 🟡
sequence()helper for composing middleware - 🟠
RequestEvent—request,params,url,cookies,locals
Docs & Ecosystem
Docs site + component registry roadmap moved to
docs/ROADMAP.md. Only framework-level (packages/bosia) items remain below.
- 🟠 GitHub Actions for auto-publishing to npm and deploying docs
- 🟡 Dev server auto-restart on crash
- 🟠 SEO infrastructure —
Metadatatype supportslangandlinkfields; dynamic<html lang>;<link>tag rendering in streaming SSR
v0.1.0
- 🟡 Rename framework from
bosbuntobosia - ⚪ Dead code cleanup (
renderSSR,buildHtmlShell, unexported internals) - 🟡
splitCsvEnvhelper for CSRF/CORS origin parsing
v0.2.0 — Production Hardening
Stability, security, and performance improvements for production workloads.
Security
Findings #1–#7 below come from the v0.4.5 security audit — see
backup/SECURITY_ISSUE_1.mdfor full context, attack scenarios, and proposed diffs.
- Cookie secure defaults — default
HttpOnly; Secure; SameSite=Laxoncookies.set()with opt-out - Auto-detect
Cache-Controlon/__bosia/data/—private, no-cachewhen cookies accessed;public, max-age=0, must-revalidateotherwise - 🔴
load()fetchcookie scoping —makeFetchnow forwards theCookieheader only to same-origin requests or origins in theINTERNAL_HOSTSallowlist; third-party hosts get no cookie. User-suppliedinit.headers.cookieis preserved - 🔴 Audit #1 —
allowExternalredirect validation — still validate againstjavascript:,data:,vbscript:schemes even whenallowExternal: true(moveDANGEROUS_SCHEMEScheck above the early return inerrors.ts:32) - 🟠 Audit #4 — Trusted proxy configuration —
TRUST_PROXYenv to control whenX-Forwarded-*headers are trusted in CSRF checks (csrf.ts:37-40) - 🟠 Audit #6 — CSP nonce infrastructure — per-request nonce generation, inject into all framework
<script>tags, expose nonce in hooks for user scripts, opt-inCSP_DIRECTIVESenv emits matchingContent-Security-Policyheader - 🟠 Audit #2 — CORS preflight validation — validate
Access-Control-Request-Method/Access-Control-Request-Headersagainst allowed config inhandlePreflight(cors.ts:53-69) - 🟠 Audit #3 — CORS
Vary: Originon all responses when CORS is configured — prevent CDN caching bugs on non-matching origins (set atserver.tsrequest level, not only ingetCorsHeaders) - 🟡 Audit #5 — Validate prerender
entries()return values — reject/,\,..in dynamic segment values before URL substitution (prerender.ts:44-50) - 🟡 Escape
langattribute in HTML shell —<html lang="${lang}">injectslangraw; if ametadata()deriveslangfrom URL/user input it can break out of the attribute - ⚪ Validate
CORS_MAX_AGEenv — reject non-numeric values instead of producingNaNheader
Security test coverage (from audit)
- 🟡 Test:
allowExternal: truestill rejectsjavascript:/data:/vbscript:URLs - 🟡 Test:
handlePreflightrejects whenAccess-Control-Request-Methodis not inallowedMethods - 🟡 Test:
Vary: Originis present on CORS-configured responses even when requesting origin doesn't match - 🟡 Test: dedicated
safePath()unit test file (currently only covered indirectly via static file serving) - 🟡 Test:
substituteParams()rejects malicious entry values containing path-traversal characters - 🟡 Test:
TRUST_PROXYenv gatesX-Forwarded-*header trust in CSRF checks
Performance
- 🟠 Parallelize client + server builds — run both
Bun.build()calls withPromise.all()instead of sequentially (~500-1000ms savings) - 🟠 Parallelize Tailwind CSS with builds — run Tailwind CLI concurrently with client+server builds (~500-800ms savings); ensure output exists before manifest step
- 🟡 Convert
sequence()middleware recursion to loop —apply(i+1, e)pattern risks stack overflow with many handlers; use iterative approach
Server Reliability
- 🟠 Stream backpressure handling — check
controller.desiredSizeto prevent memory buildup on slow/disconnected clients - 🟠 Streaming SSR error recovery — render proper error page instead of bare
<p>Internal Server Error</p>whenrender()throws mid-stream - 🟠
renderPageWithFormDataloader error handling — catchHttpError/Redirectthrown fromloadRouteData()after a successful form action; let them surface as proper redirect/error responses instead of crashing the request. - 🟡 Prerender process cleanup — proper signal handling, verified termination (
await child.exitedafterkill()), use random port instead of hardcoded 13572 - 🟡 Fix
buildAndRestartrecursive tail call — replace recursion withwhileloop to prevent stack growth under rapid file changes
Client
- 🟡 Bound prefetch cache size —
prefetchCachegrows unbounded between navigations; add LRU eviction (max ~50 entries) - 🟡 Prefetch cache TTL — stale prefetch data served after long idle; discard entries older than 30s on
consumePrefetch() - 🟠 Router click handler must respect modifier/middle clicks.
Build
- 🟡 Fail build on tsconfig.json corruption — don't silently continue with degraded config
- 🟡
compress()threshold uses character count not byte count —body.lengthon a UTF-8 string under-counts multi-byte content; switch toBuffer.byteLengthorTextEncoder().encode(...).lengthbefore threshold check - 🟡
.envparser inline-comment stripping —KEY="value" # notecurrently keeps# noteas part of the value; strip trailing comment after the closing quote - ⚪ Tune gzip compression threshold — raised to 2KB (
GZIP_MIN_BYTES = 2048); small responses fit in single TCP packet, gzip overhead outweighs savings below this size
DX
- 🟠 Audit #7 — Dev proxy must forward
X-Forwarded-Host/X-Forwarded-Prototo the inner app — without them the CSRF check derives the wrongexpectedOrigin, 403'ing same-origin POST/form actions in dev. DX-only, prod unaffected. - 🟡 Stale env cleanup in dev — reset removed
.envvars on hot-reload
v0.2.1 — Features & DX
New capabilities and developer experience improvements.
Data Loading
- 🟠
depends()andinvalidate()— selective data reloading - 🟡 Prefetch sends the loader cache mask — hover/viewport
data-bosia-preloadwas warming the data endpoint with no mask, re-running every loader server-side; now it sends the same_invalidatedbits as a real nav - 🔴 Stale prefetch after invalidation — a hover/viewport prefetch of the current page was served instead of refetching after
use:enhance/invalidate*()/goto({ invalidateAll }); now skipped — 1.1.1 - 🟡
setHeaders()in load functions — set response headers from loaders — shipped 0.8.7
Navigation
- 🟠
beforeNavigate/afterNavigatelifecycle hooks — exported frombosia/client; fired by SPA router around pushState/popstate navs and on full-page unload (willUnload=true); cancel support viacancel()on programmatic navs - 🟠 Scroll restoration and snapshot support (
export const snapshot) — scroll restoration shipped 0.8.6;snapshotshipped 0.8.7
Routing
- 🟠 Layout reset (
[email protected]or[email protected]) - 🟠 Route-level
+error.svelte— per-layout error boundaries instead of global-only - 🟡 Page option:
ssrtoggle (export const ssr = false) - 🟡 Page option:
trailingSlashconfiguration
Forms
- 🟠
use:enhanceprogressive enhancement — client-side fetch submission with automatic form state management (like SvelteKit)
Types
- 🟠 Typed route params — generate
{ slug: string }from[slug]instead ofRecord<string, string> - 🟡 Error page types in generated
$types.d.ts
Server
- 🟡 Structured logging with request correlation IDs
DX
- 🟡 Cache route scanning in dev mode — skip
fs.readdirSync()re-scan when changed file is not a route file (+page/+layout/+server/+error) — shipped 0.8.10 - 🟡 Remove hardcoded 200ms SSE delay — poll
/_healthinstead ofBun.sleep(200)before broadcasting reload — shipped 0.8.10 - 🟡 Smarter dev rebuild triggers — filter watcher by extension; skip rebuilds for
.md, test files, and non-source changes — shipped 0.8.10
v0.2.2 — Ecosystem, Observability & Scale
Nice-to-haves for a growing framework and performance at scale.
- 🟡 Production sourcemaps — external source maps for debuggable production errors
Performance (at scale)
- 🟠 Request deduplication — share the in-flight loader promise for concurrent identical GET requests instead of running twice. Reworked in 0.3.1 around a folder convention: dedup ON by default keyed on URL only.
- 🔴 Dedup key cross-user data leak — replaced cookie-fingerprint identity with a folder convention. Routes under
(private)skip dedup and run per-request. - 🟡 Trie-based route matcher — replace linear O(n) route scan with radix trie for O(k) matching (k = URL segments). Matters when route count exceeds ~100
- 🟡 Compiled route regex — pre-compile route patterns to
RegExpat startup instead of parsing on every match - 🟠 Concurrency / backpressure ceiling — Bun accepted unlimited connections, the likely OOM vector under slow-loris. Add an env-gated soft cap (
MAX_INFLIGHT) reusing the in-flight counter, return 503 when exceeded. Shipped v0.5.13. - 🟡 Response cache + brotli —
Bun.gzipSync()ran on every HTML >2KB with no precompressed cache; no brotli. Add an LRU cache + brotli. Shipped v0.6.0 — skip-render cache keyed on URL + identity hash, per-route opt-out, brotli+gzip per entry. - 🟡 Static-asset fallthrough cost — every static hit called
Bun.file().exists()up to 4×. Build a boot manifest so prod lookups are a Map check. Shipped v0.6.9 —staticManifest.tswalks the dirs once at boot. - 🟡 Collapse SSR
render()calls — rootApp.svelte+ error pages render in separate Svelterender()invocations. Profile under representative load first.
Server Reliability
- 🟠 Process-level error handlers in prod — install
process.on("uncaughtException"/"unhandledRejection")outside the dev inspector path. - 🟡 Structured logging — replace emoji-prefixed
console.log/errorinserver.tswith a level-based logger that emits JSON in prod (pretty in dev) with a request ID. - ⚪ Tunable shutdown timers —
server.ts:906hardcodes the 2 s force-exit window and 10 s drain. Expose viaSHUTDOWN_DRAIN_MS/SHUTDOWN_FORCE_MSfor deploys with long-running streaming responses. Source: 2026-05-23 pre-prod audit - ⚪ Startup banner shows resolved hostname —
server.ts:880-882logshttp://localhost:${PORT}even though Bun binds0.0.0.0by default. Cosmetic only (container is reachable). Source: 2026-05-23 pre-prod audit
v0.2.3 — CLI & Feature Installer
Per-file install strategies so features can safely contribute to shared files.
CLI / Feat
- 🟠
bosia featper-file strategies —meta.jsonfiles: FileEntry[]with astrategyfield:write(default),skip-if-exists,append-line,append-block,merge-json. Replaces the all-or-nothing replace prompt for shared files. - 🟡 Document
meta.jsonschema and strategies indocs/(CLI /bosia featpage) - 🟡
bosia feat <name> --dry-run— preview file actions (write/skip/append/merge) without touching disk - 🟡 Validation: error early when two installed features write to the same target with
writestrategy (force one to declare append-line/append-block) - 🟠
authfeature scaffold — usesappend-blockto register hooks insrc/hooks.server.tsand routes barrel - 🟡
s3/storagefeature → shipped asfile-uploadin v0.6.4:bun x bosia feat [-y] file-upload [-d sqlite|postgres|mysql]scaffolds Drizzle metadata + local/S3 adapter +/api/filesPOST (WebP @0.85, fit 1920×1080) - 🟡 Track installed features per project (
bosia.jsonat root, committed) — enablesbosia feat list/add list. Schema{ version, features, components, blocks }keyed by name.
v0.3.0 — Test Integration (Phase 1 + 2)
Built-in testing powered by
bun test. See TEST_PLAN.md for full details.
DX
- 🟡 Prettier formatting — root config + scripts (
format,format:check); all 3 templates ship matching.prettierrc.jsonso scaffolded projects format-on-create. Pre-commit hook auto-formats staged files. No lint.
CLI
- 🟠
bosia testcommand — wrapsbun testwith framework-aware defaults - 🟡 Auto-load
.env.test(fallback.env) before running tests - 🟡 Set
BOSIA_ENV=testautomatically - 🟡 Pass through flags (
--watch,--coverage,--bail,--timeout, etc.) - 🟡 Unit tests for core pure utilities (
matcher,cookies,csrf,cors,errors,html,dedup,env) - 🟡 Unit tests for build/codegen helpers (
scanner,routeTypes,envCodegen,hooks.sequence,paths.resolveBosiaBin,lib/utils.cn,cli/registry.mergePkgJson,prerenderpath/URL helpers)
Test Utilities (`bosia/testing`)
- 🟠
createRequestEvent()— mock factory for testing+server.tshandlers and hooks - 🟠
createLoadEvent()— mock factory for testingload()functions - 🟡
createMetadataEvent()— mock factory for testingmetadata()functions - 🟠
mockCookies()— in-memory cookie jar implementingCookiesinterface - 🟡
mockFetch()— fetch interceptor for isolating loaders - 🟡
createFormData()— helper for building form action payloads
v0.3.1 — Route & API Integration Testing (Phase 3)
Test routes end-to-end without starting a real server.
- 🟠
createTestApp()— build an in-processBosiaAppfrom the route manifest - 🟠
testRequest()— send HTTP requests to the test app, get standardResponseback - 🟠 Support API routes, page routes (SSR HTML), and form actions
- 🟡 Response assertion helpers:
expectJson(),expectRedirect(),expectHtml()
v0.3.2 — Component Testing (Phase 4)
Render and assert on Svelte 5 components in tests.
- 🟠
renderComponent(Component, { props })— SSR render a component, return HTML - 🟠
renderPage(route, options?)— full SSR pipeline (loader → layout → page) - 🟡 Snapshot testing support (built into
bun test) - 🟡 Investigate
@testing-library/sveltecompatibility with Bun
v0.4.0 — Plugin Core
First-party plugin system. Standardize OpenAPI / OpenTelemetry / server-timing as plugins; let third parties drop in any Elysia plugin. Full design in
plans/plugin-feature.md.
Config & Types
- 🔴
bosia.config.tsloader —packages/bosia/src/core/config.ts; resolve fromprocess.cwd(), compile viaBun.build({ target: "bun" }), cache, default to{ plugins: [] } - 🔴 Public types in
packages/bosia/src/lib/index.ts—BosiaPlugin,BosiaConfig,BuildContext,DevContext,RenderContext,defineConfighelper
Elysia Hooks
- 🔴
backend.before/backend.aftermount points inserver.ts—beforeruns raw routes (e.g./openapi.json) bypassing framework middleware;afterreceivesRouteManifestfor introspection
Build Hooks
- 🟠
build.preBuild/build.postScan/build.postBuildinbuild.ts— callpreBuildbeforeloadEnv,postScanafterscanRoutes(),postBuildaftergenerateStaticSite() - 🟠
build.bunPlugins(target)merged into client + serverBun.build()plugin arrays
Render Hooks
- 🟠
render.headfragments injected before</head>inbuildMetadataChunk - 🟠
render.bodyEndfragments injected before</body>inbuildHtmlTail - 🟠
RenderContext(request, route, metadata) threaded fromrenderer.tsintohtml.tsbuilders
First-Party Plugin
- 🟠
bosia/plugins/server-timing— exercisesbackend.before; addsServer-Timing: handler;dur=...header
Docs & Demo
- 🟡
docs/content/docs/guides/plugins.md— usage guide - 🟡
apps/demo/bosia.config.ts— server-timing wired
v0.4.1 — OpenAPI Plugin
Auto-bridge file routes to OpenAPI spec.
- 🟠
bosia/plugins/openapifirst-party plugin - 🟠
build.postScanreadsRouteManifest, emitsdist/openapi.json - 🟠 Runtime mount via
backend.before—GET /openapi.json,GET /docs(Scalar/Swagger UI) - 🟡 Optional
schemaexport on+server.ts(TypeBox or Zod, decide later) - 🟡 Docs: OpenAPI usage page
v0.4.2 — OpenTelemetry Plugin
Tracing + metrics for production apps.
- 🟠
bosia/plugins/opentelemetryfirst-party plugin - 🟠 OTLP exporter config via env vars (
OTEL_EXPORTER_OTLP_ENDPOINT, etc.) - 🟠 Trace
backend.beforerequest → response,load()calls, render time - 🟡 Verify
devparity — telemetry must work inbosia dev
v0.4.1 — Inspector Plugin ✅ (shipped 2026-05-06)
Click element in browser → open exact source file:line in editor / hand off to AI agent. No Vite, no React-style fiber tree — does it via compile-time attribute injection.
Compile-Time
- 🟠
bosia/plugins/inspectorfirst-party plugin (dev-only) - 🟠 Contributes Bun plugin via
build.bunPlugins()— runs beforeSveltePlugin()and replaces its.svelteonLoadwith an injecting variant - 🟠 Parses
.sveltesource withsvelte/compilerparse(), walksRegularElementnodes, injectsdata-bosia-loc="<relpath>:<line>:<col>"viamagic-string(preserves source maps) - 🟡 Skips
<svelte:*>and component (capitalized) tags - 🟡 Strips attribute from production builds (no-op when not dev)
Runtime Overlay
- 🟠 Dev-only client overlay injected via
render.bodyEnd— alt+hover highlights element, alt+click capturesdata-bosia-loc - 🟠
POST /__bosia/locateendpoint (mounted viabackend.before) — receives{ file, line, col }, opens editor (or POSTs toaiEndpointwith comment) - 🟡 Editor integration —
code -g file:line(configurable viainspector({ editor: "code" | "cursor" | "zed" })) - 🟡 Toast feedback — overlay shows "opened
: " on click
Docs
- 🟡
docs/content/docs/guides/inspector.md— usage + AI-agent workflow
v0.4.2 — Template fixes ✅ (shipped 2026-05-07)
Make a freshly scaffolded project pass
bun run checkout of the box.
- 🟠 Ship
.gitignorewithbun x bosia create— npm pack strips.gitignore, so templates store it as_gitignoreandcopyDirrestores the dotfile name on copy - 🟡 Ignore generated Tailwind output
public/bosia-tw.cssin template.prettierignoreand.gitignore(default, demo, todo) sobun run checksucceeds on a clean scaffold - 🟡
bun run check:templates— packs viabun pm pack, extracts the tarball, and asserts eachtemplates/*still has the expected files (no install, no scaffold) so this class of regression fails locally before publishing
v0.5.1 — Inspector default in all templates ✅ (shipped 2026-05-15)
Ship every scaffolding template with a minimal
bosia.config.tsso freshly scaffolded projects get Alt+click-to-source out of the box.
- 🟡 Add
bosia.config.tstotemplates/{default,demo,todo}/enablinginspector({ editor: "code" }).copyDircopies it as-is; no substitutions needed. Production-safe (plugin self-disables underNODE_ENV=production). - ⚪ Note preconfigured state in
docs/content/docs/guides/inspector.mdso existing-project users still find the manual setup steps
v0.5.5 — Dev/Build dist collision ✅ (shipped 2026-05-18)
Dev and build no longer share
./dist. Dev writes to.bosia/dev/; standalonebun run buildkeeps writing to./dist/.
- 🟠 Decouple URL namespace (
/dist/client/...) from on-disk location viaOUT_DIRinpaths.ts(readsBOSIA_OUT_DIR, default./dist) - 🟠
dev.tshardcodes.bosia/devand passesBOSIA_OUT_DIRto spawned build + app-server children; never reads the env itself - 🟠
build.ts,prerender.ts,html.ts,server.ts,cli/start.tsall read fromOUT_DIRinstead of hardcoded./distliterals - 🟡 Verification path:
BOSIA_OUT_DIR=.bosia/verify bun run buildproduces full artifacts without touching./dist. Catches whattsc --noEmit+svelte-checkmiss (route scan, prerender child, server-entry compile). Verified atapps/demo.
v0.5.6 — Build/dev `.bosia/` cleanup collision ✅ (shipped 2026-05-18)
Follow-up to v0.5.5.
OUT_DIRwas split, butbuild.tsstill blanket-wiped./.bosiaat startup — clobbering a concurrently-runningbosia devwhose compiled server lives at.bosia/dev/. Cleanup is now scoped.
- 🔴
build.tscleanup is scoped toOUT_DIRplus only the codegen files this build owns (routes.ts,routes.client.ts,env.server.ts,env.client.ts,types). No more blanket.bosia/rmSync.
v0.5.7 — `params` as a top-level page/layout prop ✅ (shipped 2026-05-19)
Match SvelteKit:
+page.svelte/+layout.sveltereceiveparamsas a sibling prop ofdata, not nested underdata.params. Network protocol (data endpoint payload, SSR injection) is unchanged —paramsis stripped at the component boundary.
- 🟠
App.sveltepassesparamsas a separate prop on pages and layouts; SSR branch strips mergedparamsoffpageDatavia local helper - 🟠
hydrate.tsseedsappState.pageDatawithout the mergedparamskey (still seedsappState.routeParamsfrom same payload) - 🟠
routeTypes.tscodegen:PageData/LayoutDatano longer intersect{ params: Params };PageProps/LayoutPropsdeclareparams: Paramsas a sibling ofdata - 🟡 Update demo + template
blog/[slug]/+page.svelteand docs (README.md,docs/content/docs/guides/routing.md) to consumeparamsas a top-level prop - 🟡 Standardize
defaultandtodostarter templates on the(public)/route group convention used bydemo, so scaffolded projects are ready to add authenticated areas (e.g.(app)/,(admin)/) without restructuring later
Same-day addition (2026-05-19) — Inspector runtime error capture
Inspector now captures live client + server runtime errors in a passive badge inside the running app. "Send to AI" per row reuses the alt-click →
aiEndpointhandoff. Live-only (no buffer/replay), dev-only (prod unaffected).
- 🟠 Server capture: Elysia
.onError()+uncaughtException/unhandledRejectionlisteners installed lazily insidebackend.before().uncaughtExceptionrethrows so crash-recovery still triggers. 500ms dedup prevents render-loop floods. - 🟠 SSE broadcaster at
/__bosia/errors— module-scoped controller Set,event: bosia-errordata frames, 25s:pingkeepalive, abort-driven cleanup. No replay buffer (live-only contract) - 🟠 Reorder the Elysia onError chain in
server.ts: the base 500 responder now registers AFTER theplugin.backend.beforeloop so plugin handlers fire first. - 🟠 Client capture in
overlay.ts:window.error+unhandledrejectionlisteners + EventSource subscription to/__bosia/errors. Unified list, stable ids, UI dedup - 🟠 Floating badge UI bottom-right (
● N errors) → click → expandable panel with per-row stack details, Dismiss, and AI-only "Send to AI" button. Badge hidden when list empty - 🟠 Sourcemap resolution dev-only —
build.tsemitssourcemap: "linked"in dev ("none"in prod). Newinspector/sourcemap.tslazy-resolves compiled frames → source via@jridgewell/trace-mapping, only for the clicked error. - 🟡 Last-interaction context: track the most recent
data-bosia-locthe user clicked/keyed and appendLast user interaction: <file>:<line>:<col>to the payload. - 🟡
errorsEnabled?: boolean(defaulttrue) config flag onInspectorOptions— opt out of the whole feature without removing the plugin - 🟡 AI-only action button — overlay still surfaces the badge for visibility without
aiEndpoint, but the "Send to AI" button only renders when configured. Standalone bosia apps in editor-mode see display-only errors
v0.5.8 — `bind:*` shadow crash fix ✅ (shipped 2026-05-19)
Dev pages using
<input bind:value={state}>crashed withRangeError: Maximum call stack size exceededon first render. Svelte's dev output wraps the binding in a namedfunction get(); Bun rewrites$.getto a named importget
- 🔴 Post-process Svelte compile output in
inspector/bun-plugin.tsandsvelteCompiler.tsto rename the innerget/setto$$g/$$s(length-preserving so source-map columns stay accurate). Dev-only — prod uses anonymous arrows. - 🔴 Inject Inspector-extracted component CSS via a runtime
<style>element instead of aloader: "css"virtual module.
v0.5.9 — `src/app.html` template ✅ (shipped 2026-05-20)
SvelteKit-style document shell. Users create
src/app.htmlwith%bosia.head%/%bosia.body%placeholders to control HTML chrome (lang, data attributes, favicon, analytics)
- 🟠
packages/bosia/src/core/appHtml.ts— parse, validate, cache template with invalidation for HMR - 🟠 Placeholders:
%bosia.head%,%bosia.body%(required);%bosia.lang%,%bosia.nonce%,%bosia.assets%,%bosia.env.PUBLIC_*%(optional) - 🟠 Update
html.tsbuilders (buildHtml,buildHtmlShellOpen,buildMetadataChunk,buildHtmlTail) to accept optional segments and slot user chrome - 🟠 Update
renderer.tsto load template once per process and thread through 6 call sites - 🟠 Validation at build time in
build.ts— fail fast if required placeholders missing - 🟡 Scaffold
src/app.htmlin templates (default,todo) and demo with%bosia.lang%anddata-themeattributes - 🟡 Favicon detection: if user's
headOpencontainsrel="icon", skip framework default favicon injection - 🟡 Unit tests: template loading, validation, parsing, caching, interpolation, segment structure
- 🟡 New skill
bosia-app-cssdocumentingsrc/app.cssorder + the Tailwind v4 / LightningCSS rule: font@import url(...)must come before@import "tailwindcss"or it's silently dropped. Catalog 33 → 34. - 🟡 New CLI command
bosia add font "<Family>" "<url>"(cli/font.ts, reusesmergeFontImports()). Prepends@import url(...)tosrc/app.csswith a/* bosia-font: <Family> */marker so it survives LightningCSS ordering. Idempotent.
v0.5.10 — SvelteKit navigation parity ✅ (shipped 2026-05-20)
Closes the gap between Bosia's client nav API and SvelteKit's
$app/navigation. Apps reached forwindow.location.hrefbecausegoto()wasn't exported (full reload, lost state)
- 🟠
goto(url, opts?)exported frombosia/client. Returns a Promise resolving after the nav settles. HonorsreplaceState,invalidateAll,noScroll - 🟠
beforeNavigate(fn)/afterNavigate(fn)lifecycle hooks.nav.cancel()blocks SPA navigations; popstate cancellation is a no-op since history already advanced. Auto-unregister on destroy viaonDestroy. - 🟠 Router exposes navigation
type("link"|"goto"|"popstate"|"form"|"enter") and theNavigationobject threading into both lifecycle phases. - 🟠
router.navigate(path, { replace, source })supportshistory.replaceState(used bygoto({ replaceState: true })) and threads the source through to the Navigation object - 🟡
beforeunloadfiresbeforeNavigatewithwillUnload: trueso listeners can observe (cancellation requires nativebeforeunloadevent — out of scope) - 🟡 Hydration safety net — wrapped
main()incore/client/hydrate.tsin a.catch()so any future hydrator failure logs to console instead of silently leaving "Loading…" on screen - 🟠 404/error pages no longer ship a stuck
#__bs__spinner blocking the "Go home" link.buildHtml()now gates spinner injection on emptybody— non-streaming SSR skips it; streaming SSR andssr=falsestill get it for the TTFB gap. - 🟡 Demo route
apps/demo/src/routes/(public)/nav-test/+page.svelteexercises all four patterns plus the cancel/event-log flow - 🟡 New docs page
docs/content/docs/guides/navigation.mdcovers the four patterns and the lifecycle hooks; added to the Guides sidebar indocs/src/lib/docs/nav.ts - 🟡 New
bosia-navigationskill so AI agents pick the right navigation pattern and use the lifecycle hooks correctly. Catalog index bumped 34 → 35; cross-references added inbosia-routingandbosia-auth-flow.
Same-day addition (2026-05-20) — Surface dev-server errors to the inspector overlay
Inspector captured runtime errors only. Dev-infra errors — build failures, app crashes,
.envreload failures, port conflicts.
- 🟠
core/dev.tscaptures build/app-crash/dev-uncaught errors into a bounded ring (50 entries, 30s TTL) with a 500ms dedup. Build + app-server stderr piped + tee'd so terminal output is unchanged. - 🟠 New
event: bosia-errorover/__bosia/sse(same wire shape as inspector'sServerError). The SSE handler flushes recent buffered errors to new clients so pre-connect errors stay visible. - 🟠 New
core/dev-error-page.tsrenders the fallback HTML the dev proxy returns whenfetch(app)throws on an HTML nav. Embeds the overlay, pre-seeds buffered errors, subscribes to/__bosia/ssereloadto swap itself out. - 🟡
.envreload failures inside the dev watcher no longer crash the dev parent — caught, logged, and routed through the same buffer so the user sees the validation error in the badge instead of a dead process
Deferred (logged for follow-up)
- 🟡
pushState(url, state)/replaceState(url, state)for shallow routing - 🟡
onNavigate(fn)(runs betweenbeforeNavigateand the actual nav) - 🟡
preloadCode(...routes)(preloads route module without data) - 🟡
applyAction(result)/deserialize(result)from$app/forms - 🟡
disableScrollHandling()for fine-grained scroll control - 🟠 Diagnose & fix
window.location.hrefstall on static builds — needs a confirmed repro; safety-net try/catch is in place so the next occurrence surfaces a console error instead of staying on "Loading…"
v0.6.0 — Server response cache (skip-render) ✅ (shipped 2026-05-24)
Before v0.6, every HTML response re-ran
metadata(), layoutload(), pageload(),render(), andBun.gzipSync()even when byte-identical. The new in-memory cache short-circuits all of it.
- 🟠 New
core/cache.ts— tiny LRU +tagIndex+pathIndex,computeCacheKey(url, req, cookies),serveCached(entry, req)withAccept-Encoding: br|gzip|identitynegotiation,buildCompressedVariants()(brotli + gzip), tag/path eviction. - 🟠 Renderer integration (
renderer.ts) — cache read before metadata/load/render, cache write after chunks are built, streaming preserved on miss. CSP-enabled deploys skip the cache (per-request nonce is incompatible with cached bytes). - 🟠 API endpoint integration (
server.ts) —+server.tsGET handlers cached with the same key rules. v0.6 invalidates API entries by URL/prefix only (nodepends()for API yet). - 🟠 Public API —
invalidate(key)/invalidateAll(prefix)frombosiamirror the existing browser-sideinvalidate()semantics. Form actions call them after a write. - 🟡 Per-route opt-out —
export const cache = false;in+page.server.ts,+page.svelte<script module>, or+server.ts. Generated$types.d.tsexports aCacheOptiontype alias for IDE support. - 🟡 Env vars —
CACHE_KEYS(defaultsession,sid,auth,token,jwt,Authorization) controls identity-hash inputs;CACHE_MAX_ENTRIES(default 500, 0 disables). Documented inguides/environment-variablesand the response-cache guide (EN+ID). - 🟡 Author guidance — new
bosia-response-cacheskill walks agents through when to callinvalidate()from server code, how to tag loaders withdepends(), and when to opt a route out. - 🟠 Dev proxy now forces the inner app to
Accept-Encoding: identity. Previously it forwardedgzip,br, the inner returned compressed bytes, Bun'sfetch()auto-decoded but leftContent-Encoding: gzip - 🟠
core/cache.tsguardsprocess.envreads — re-exported through the publicbosiabarrel, it evaluated in the browser bundle and threwReferenceError: processon hydration in Safari. - 🟠 Server-only response-cache exports moved to
bosia/server—core/cache.tsstill evaluated client-side via the shared barrel. Added./servertoexports, createdlib/server.ts, removed them from the shared barrel. - 🟡 Inspector dev-error reporter type alignment —
devErrorReport.tsdeclaredsource?: "server"|...butpushServerErroraccepted"elysia"|..., failingbun run check(TS2322)
Deferred to v0.7+
- 🟡 Key-based invalidation for
+server.tsendpoints — give API handlers adepends()argument or supportexport const tags = [...]soinvalidate("app:user")evicts API responses too. - 🟡 TTL-based expiry — author wants pure-invalidate today, but TTL is useful for "refresh every N seconds" pages.
- 🟡 Layout-level
cache = falsecascade — done in 1.2.9 (routeCacheFlagreads layout modules). - 🟡 Multi-replica cache (pub/sub invalidation) — single-replica only in v0.6.
- 🟡 Soft-purge / stale-while-revalidate.
- 🟡 Custom key function —
export const cache = { key: (req) => string }.
v0.6.5 — Compile-time component-import audit ✅ (shipped 2026-05-27)
A scaffolded app crashed on first SSR render with
undefined is not a function:import * as Card+<Card.Root>, butindex.tsexportsCard/CardContent, notRoot.bosia buildsucceeded silently.
- 🟠
core/svelteAudit.ts— walks the modern Svelte 5 AST, extracts top-level bindings from<script>, tracks shadowing from{#each}/{#snippet}/{@const}. For namespace imports,Bun.Transpiler.scan()introspects the source's exports. - 🟠
core/svelteCompiler.ts— switchedcompile()tomodernAst: true, wired the audit intoonLoad, added module-scoped per-file dedupe (Map<absPath, Promise>) so it runs once across the parallelbrowser+buntargets. - 🟠 Promotes select
svelte/compilerwarnings to errors:component_name_lowercase,bind_invalid_value,invalid_html_attribute— silently-broken cases the user almost never wants to ship. - 🟡
resolveImport.ts+sourceLoc.ts— extracted fromplugin.tsandinspector/bun-plugin.tsso the audit and the resolver share one alias/tsconfig-paths/relative-path implementation and onelineColFromOffsethelper. - 🟡
BosiaConfig.strictImports(boolean |{ unbound, namespaceMember, warnings }) — per-component opt-out.BOSIA_STRICT_IMPORTS=0env var downgrades to aconsole.warnat runtime without failing the build. - 🟡
test/svelte-audit.test.ts— 8 fixtures cover the repro (missing namespace export), positives (correct member, named import, each-block shadowing, bare-package skip), and edges (unbound identifier, dotted on default import, env override). - 🟡 ConstTag siblings —
{@const Foo = ...}now scopes its binding across the whole surrounding fragment, not just its own children. Previously a sibling-bound<DemoComponent />false-flagged.
v0.6.4 — Combined files demo, CORS-safe ✅ (shipped 2026-05-26)
The crop block's docs demo loaded a remote Unsplash URL with
crossorigin="anonymous"; the browser blocked it as CORS and the cropper rendered blank. Replaced the two demos with one: pick a file viaUploadArea
- 🟡
demos/FilesUploadCropDemo.svelte— single combined demo.UploadArea(enableCrop) → on crop, captures the(file, done)pair, opensCropImageagainstURL.createObjectURL(file), wraps the Blob as aFile, callsdone(file) - 🟡
docs/src/routes/api/demo-upload/+server.ts— tinyPOSTreturning{ url, ok }so the demo Upload button doesn't 500. - ⚪ Both
blocks/files/{crop-image,upload-area}.mdfrontmatterdemo:now points atFilesUploadCropDemo.[...slug]/+page.svelteimports only the new demo; deletedFilesCropImageDemo.svelteandFilesUploadAreaDemo.svelte. - 🟡
blocks/files/crop-image/block.svelte— switched the 400px viewport fromh-[400px]tostyle="height: 400px;". The class works for end-users. - 🟡
docs/src/app.css— added@source "../../registry/blocks/**/*.{svelte,ts,js}"so utility classes declared inside registry blocks are emitted intobosia-tw.cssfrom the docs build alongsideregistry/components/ui. - 🟡
docs/src/lib/docs/content.ts—contentDir/demoFileno longer resolve relative toimport.meta.dir(dev bundle 3 levels deep, prod 2), which missed the content dir in dev → every catch-all docs page 404'd.
Same-day addition (2026-05-26) — `file-upload` feature + CLI dialect flags
The
files/upload-areablock shipped since v0.6.3 but bosia had no server-side counterpart.
- 🟠
registry/features/file-upload/— full backend.file.service.tsvalidates MIME, decodes viaBun.Image, fit-resizes to 1920×1080, re-encodes WebP @0.85, persists via Drizzle. Three dialect table files target one install path. - 🟠
cli/feat.ts— per-feature options system. Top-level handles only-y/--local; everything after the feature name flows toresolveFeatureOptions(), parsed against the feature's ownmeta.jsonoptionsschema. Unknown flags abort. - ⚪
cli/index.ts— feat subcommand argv handler simplified: first non-flag token is the name, everything else (including pre-name-y) flows torunFeat. Help text updated so feature-specific flags follow the feature name. - ⚪
packages/bosia/src/cli/registry.ts—InstallOptionsgainedfeatureOptions(resolved values) andfeatureArgs(raw tokens for the root feature). No CLI-level dialect type — dialect is nowfile-upload-specific. - ⚪
registry/index.json—featuresarray gainsfile-upload. - 🟡
docs/content/docs/guides/file-upload.md— install / env / wiring / S3 swap docs; cross-link added fromblocks/files/upload-area.md. Nav entry under Guides. - ⚪
bosia-file-upload/SKILL.md— new skill teaching when to install file-upload (avatar/profile/media-library triggers), R1–R5, workflow, anti-patterns.
v0.6.3 — Skills API exposes references ✅ (shipped 2026-05-25)
AI agents fetching
/api/skills/<name>.jsonsaw theSKILL.mdbody but not the companion reference files that carry the actionable detail, so they guessed paths or scraped the site.
- 🟡
listSkillReferences(name)indocs/src/lib/skills/list.ts— reads<SKILLS_ROOT>/<name>/references/, filters to.md, validates slugs against^[a-z0-9-]+$, returns{ file, path }[]sorted by file. Silent[]on missing dir. - 🟡
GET /api/skills/[name]response gainedreferences: SkillReference[]so agents discover the available reference files in one round-trip. - 🟡 New route
api/skills/[name]/references/[file]/+server.ts— prerendered,entries()enumerates(name, file)pairs,realpathtraversal guard mirrors the[name]route. Returns{ name, file, path, content }withmax-age=60
Same-day addition (2026-05-25) — Files blocks (crop + upload)
Registry had no file-handling blocks. Ported two from a working CMS: an image cropper and a drag-and-drop upload area. Both installable standalone.
- 🟡
registry/blocks/files/crop-image/— Svelte 5 cropper wrappingsvelte-easy-crop - 🟡
blocks/files/upload-area/— drag-drop + click-to-pick with preview, size validation,XMLHttpRequestprogress,Progressbar. Props:uploadUrl(required),accept,maxSizeMB,fieldName,extraFields,headers,enableCrop - ⚪
registry/components/ui/icon/icons.ts— addedcropandzoom-inpaths (lucide-static). - ⚪
registry/index.json—blocksarray gainsfiles/crop-imageandfiles/upload-area. - ⚪ Docs pages
docs/content/docs/blocks/files/crop-image.mdandupload-area.md; Files group added todocs/src/lib/docs/nav.ts;FilesCropImageDemoandFilesUploadAreaDemoregistered in[...slug]/+page.svelte. - 🟡
core/build.ts— addedconditions: ["svelte"]to bothBun.buildcalls so Svelte libs (likesvelte-easy-crop) resolve to theirsvelteexport. An earlier genericonResolvehandler broke shiki's chunked CJS interop.
Same-day addition (2026-05-25) — Clean-architecture skill for generated apps
Bosapi-generated apps put
db.select(...)directly in+page.server.tsloaders and skipped a service/repository layer.
- 🟡 New
bosia-clean-architectureskill — eight rules (R1–R8): nodbin routes, repository ownership, service-owned validation, derived valibot validators, one entity per feature, cross-feature via service namespace, table home. - 🟡 Three companion references —
feature-template.md(copy-adapt for all six files + callers),refactor-recipe.md(grep → extract → swap-import usingwarung-nasi),shared-folder.md(what belongs infeatures/shared/and what doesn't). - 🟡
bosia-drizzle-featureupdated — folder diagram gained*.repository.ts/*.validator.ts/*.dto.ts; R2 split into repository + service with examples; workflow now 9 steps; new anti-patterns and P1 items for the split. - 🟡
bosia-drizzle-usageupdated — Quick Start rewritten so loaders callCatalogService.summary()notdb.select(...); workflow writes the repository first, then service; new red flags fordbin routes. - 🟡 Catalog
docs/content/skills/SKILL.mdbumped 38 → 39 skills;bosia-clean-architectureadded under framework conventions and into the discovery-order step 2.
v0.5.13 — Inspector component call-site chain ✅ (shipped 2026-05-23)
Alt-clicking a
<button>rendered by a sharedButton.svelteshowed onlyButton.svelte:5:1
- 🟠 Compile-time injection of
<!--bosia:o=...-->/<!--bosia:c-->markers aroundComponent/SvelteComponent/SvelteSelfnodes ininjectLocs - 🟠 Runtime
collectStack(el)walks DOM ancestors + previous siblings with a depth counter matching eachbosia:cto itsbosia:o, so siblings don't bleed. Returns outermost-first. - 🟡 Tooltip widened with
max-width:90vw+ ellipsis so long chains don't overflow the viewport. - ⚪
docs/content/docs/guides/inspector.mdupdated to describe the chain feature and extend the prod-output grep to check for both markers. - 🟡
bosia-inspector-editskill updated for the new payload — parses theComponent tree (outer → leaf): …prefix, defaults the target to the outermost call-site, requires a one-sentence justification when the agent picks the leaf instead.
Same-day addition (2026-05-23) — Env + CORS skills for AI agents
Bosapi preview apps (
a-<uuid>.lvh.me) surfaced403 Cross-origin request blockedand the AI kept reaching for CORS env vars.
- 🟡 New
bosia-envskill — four-tier prefix (PUBLIC_STATIC_/PUBLIC_/STATIC_/none),$envvirtual module for user vars,process.envfor framework-reserved vars. - 🟡 New
bosia-corsskill — CORS env recipe (CORS_ALLOWED_ORIGINS+ methods/headers/credentials/max-age), theVary: Origininvariant, and a triage table distinguishing a real CORS failure from Bosia's CSRF rejection. - 🟡 Catalog
SKILL.mdupdated 35 → 37 skills; both entries added under framework conventions and into discovery-order step 2; cross-references wired both ways and tobosia-security-review/bosia-elysia-routes.
v0.5.11 — `$types` resolution inside `.svelte` files
tsc --noEmitresolves./$typesfrom.sveltevia therootDirstrick, socheck/buildtype-check correctly. Butsvelte-language-serverdoesn't honorrootDirsin its virtual TS document.Acceptance: in a freshly scaffolded app, hovering
PagePropsin+page.svelteshows the generated type, autocomplete onparams.lists only the route's dynamic segments, and no "module not found" appears for./$types. Same in Zed and VS Code.
- 🟠 Investigate options: (a) a TS Language Service plugin hooking
moduleResolutionfor$typesfrom.sveltefiles; (b) fork/extendsvelte-language-serverconfig. - 🟠 Ship the plugin/shim from
packages/bosiaand wire it into the scaffolding templates'tsconfig.json(compilerOptions.pluginsorsvelte.config.js) so new apps work out of the box. - 🟡 Verify in Zed and VS Code on
apps/demo/src/routes/(public)/blog/[slug]/+page.svelte: hover showsParams = { slug: string }, autocomplete onparams.listsslug, typingparams.foored-squiggles. - 🟡 Document the editor setup step in
docs/content/docs/guides/routing.md(or a new "Editor setup" guide) — what extension to install, whattsconfig.jsonlooks like. - ⚪ Note the limitation + workaround in the meantime under
docs/content/docs/reference/sveltekit-differences.md. (Updated 2026-05-24 to reflect shipped features: navigation API, plugin system, response caching)
v0.5.4 — Brief intake skills ✅ (shipped 2026-05-17)
Six new design-track skills that gather product brief (identity / voice / visual / platform) into
BRIEF.mdat app root before any UI emit. Closes the "agent invents palette + tone every turn" drift bug.
- 🟠
bosia-brief-intake— orchestrator. Walks the four group skills in order, writesBRIEF.md, chainsbosia-brief-review. Auto-trigger surface: empty BRIEF.md. - 🟡
bosia-brief-identity— name, tagline, audience, language, formality, self-reference. Locks sapaan + UI string language for the rest of the session. - 🟡
bosia-brief-voice— tone adjectives, emoji/exclamation policy, microcopy spine table (5 rows: empty / error / confirm-destructive / success / primary action), domain glossary, copy no-go. - 🟡
bosia-brief-visual— palette intent → theme pick decision matrix, shape, density, type, icons, custom marks. Runsbosia_add_theme+--primary/--accentoverride. - 🟡
bosia-brief-platform— form factors, primary surface, ID format regex, number/dateIntlformatters, imagery aspect ratios, first-screen scaffold queue, MVP feature list (cap 7). - 🟡
bosia-brief-review— quality gate. P0/P1 checks: sections complete, theme installed matches brief, formatter modules scaffolded, sapaan consistent, no emoji leak in product strings, first-screen names resolve to real catalog entries. - 🟡 Catalog
SKILL.mdindex updated — 25 → 31, new section "Brief intake — design ✦", discovery order gains step 0 "check BRIEF.md".
Hotfix (same-day, 2026-05-17)
- 🔴 Fix
bosia devbuild crashMultiple files share the same output pathon apps with multiple style-less+page.svelteroutes.
Same-day addition (2026-05-17)
- 🟡
bosia-frontend-design— new design skill forcing an aesthetic stance (direction/typography/dominant colour + sharp accent/one memorable detail) before any UI emit, avoiding the AI-default look. - 🟡
bosia-frontend-designwired intobosia-brief-intakeas step 4, so every BRIEF.md ends with a populated## Aestheticsection. Quick-start opener bumped 5 → 6 questions. - 🟡 Stance consumption wired downstream —
bosia-design-reviewgains a P1 check that each emit honors § Aesthetic without re-picking. Six page scaffolds (landing, saas-landing, blog, pricing, mobile-screen. - 🟡
bosia-brief-intakeships two reference files:references/quick-start-script.md(6-question opener with palette → direction inference) andreferences/example-brief.md(Dombaku-style filled BRIEF.md). Frontmattertargets.filesupdated.
v0.5.3 — API prerender ✅ (shipped 2026-05-16)
Same prerender ergonomics for
+server.tsroutes as pages already had. Drop the docs-only static-API post-build pipeline.
- 🟠 Framework:
+server.tshonorsexport const prerender = true—detectPrerenderRoutesscansmanifest.apis, dynamic routes callentries(),prerenderApiOutPath()writes one.jsonper route. Fetched body written verbatim. - 🟡 Dev runtime alias: API routes with
prerender = trueare also served at<path>.json, matching the URL static hosts will serve in prod. Non-prerender routes get no alias (packages/bosia/src/core/server.ts) - 🟡 Unit tests for
prerenderApiOutPathandsubstituteParamsrest-segment cases (packages/bosia/test/prerender-api.test.ts) - 🟡 Docs API routes migrated:
/api/skills,/api/skills/[name],/api/components,/api/components/[...path],/api/blocks,/api/blocks/[...path]all opt into framework prerender. - 🟡 Removed
generateSkillsApi()+generateRegistryApi()fromdocs/scripts/post-build.ts— post-build returns to sitemap-only
Hotfix (same-day, 2026-05-16)
- 🔴 Fix dev
.jsonalias resolution: catch-all sibling routes were absorbing the.jsonsuffix into their rest-segment param, causing 4xx in dev. - 🔴 Fix
/api/skills/<name>JSON shape: was emitting rawSKILL.mdmarkdown into a.jsonfile. Handler now returnsResponse.json({ name, content })with frontmatter stripped viagray-matter, matching the v0.5.2 post-build shape - 🟡 New
packages/bosia/test/apiResolver.test.ts— 10 cases covering flat-route alias, catch-all precedence,[name]precedence, non-prerender fall-through, andmodule()throw → fallback - 🟡 New
docs/test/api-prerender.test.ts— post-build sanity overdist/static/api/**/*.json: every artifact parses; list endpoints expose their array. - 🟡 Renamed registry detail field
mdFile→contentin/api/components/<path>and/api/blocks/<path>responses to match/api/skills/<name>shape (docs/src/lib/registry/list.ts) - 🔴 Fix production-build docs crash on every page with code blocks (
createHighlighternot a function). Lazyawait import("shiki")made Bun's splitter call into the parent before its exports initialized. - 🟡 Normalize
pathon/api/skills,/api/components,/api/blocksindex + detail responses to the full detail URL (e.g./api/components/ui/button.json)
v0.5.2 — CLI ergonomics & registry API ✅ (shipped 2026-05-15)
Multi-component install and AI-discovery parity with skills.
- 🟠
bosia addaccepts multiple component names in one call; new-y/--yesflag auto-confirms overwrite prompt for CI use - 🟡 Static
/api/components.json+/api/components/{path}.jsonand/api/blocks.json+/api/blocks/{path}.jsonemitted bydocs/scripts/post-build.ts(superseded in v0.5.3 by the framework prerender)
v0.4.4 — Build CSS collision hotfix ✅ (shipped 2026-05-09)
Republish of 0.4.3 with a missed regression in the Svelte build path fixed.
- 🔴 Restore
app.css→ JS no-op resolve incore/plugin.ts. Without it, every dynamic-imported route chunk reachingapp.cssproduces an identical CSS sidecar → Bun fails with "Multiple files share the same output path" - 🟡 Regression test
packages/bosia/test/svelte-build.test.ts— 12 dummy routes + shared app.css; fails without the no-op, passes with it
v0.4.3 — Request pipeline perf ✅ (shipped 2026-05-09)
Cut redundant work from the per-request hot path.
Done
- 🟠 Resolve page route once per request and thread through
renderSSRStream/renderPageWithFormData/ form-action handler - 🟡 Cache
getPublicDynamicEnv()at module scope - 🟠 Linear
parent()data merging in layout loaders — O(d²) → O(d) with per-layer snapshot - 🟡 Drop redundant
onBeforeHandleapiRoutes scan; non-GET catch-alls already cover every method - 🟠 Inline Svelte compile, drop
bun-plugin-svelte— own.svelte/.svelte.[tj]sonLoadwithcss: "injected"(browser) /css: "external"(server)
Open
- 🟠 Truly progressive SSR streaming —
renderSSRStreamis blocking before first byte (load → render → enqueue). The real blocker is a parallel-aware loader runner that flushes chunks as each loader resolves. - 🟠 Deferred loader data (Remix
defer-style) — Svelte{#await}already renders the SSR skeleton; Bosia lacks transport: promise markers, streamed resolve<script>s, NDJSON data endpoint. Plan:backup/PLAN_REMIX_LIKE.md. - 🟡 Reduce
safeJsonStringifycost on large loader payloads — done in v0.5.0 by moving page/layout/form data to<script type="application/json">islands read viaJSON.parse(...textContent)
Reference:
backup/PERFORM_ISSUES.md(full request-pipeline review, 2026-05-08).
v0.4.5 — Blocks & Themes Registry
Two new registry kinds: Blocks (composed UI sections) and Themes (token sets). Closes the design-quality gap for LLM-generated apps (Bosapi) and hand-coders alike. Primitives stay unchanged.
CLI
- 🟠
bun x bosia@latest add block <category>/<name>— install a block tosrc/lib/blocks/<path>/ - 🟠
bun x bosia@latest add theme <name>— install a theme tosrc/lib/themes/<name>.css, patchapp.cssimport - 🟡 Extend CLI dispatcher (
packages/bosia/src/cli/index.ts) foradd block/add themesub-args - 🟡 Refactor
add.ts— parameterize destination root;RegistryIndexgainsblocks: string[],themes: string[] - 🟡
block.tshandler — recursive primitive deps viaaddComponent(), optional font@importmerge intoapp.css - 🟡
theme.tshandler — copytokens.css, swap@importinapp.css(one-active-theme), font@importmerge
Registry content
- 🟠 Extend
registry/index.jsonwithblocksandthemesarrays - 🟠
registry/themes/neutral/— extracted from currentapps/demo/src/app.css@themeblock - 🟠
registry/themes/editorial/— warm cream palette + Instrument Serif display - 🟢 Six existing themes wired into sidebar nav + skill metadata (zinc, stone, claude, ocean, forest, rose) — v0.6.22 (2026-06-10)
- 🟢 Four new themes added: sunset (warm orange), midnight (indigo dark-first), mono (brutalist), amber (cozy hospitality) — v0.6.22 (2026-06-10)
- 🟠
registry/blocks/cards/feature-editorial/— first block; matches Open Design reference (eyebrow numeral, serif title, tight leading, circular CTA) - 🟡 Refactor
apps/demo/src/app.cssto@import "./lib/themes/neutral.css"(visually unchanged)
Docs
- 🟡
docs/content/docs/blocks/overview.md+ per-block pages - 🟡
docs/content/docs/themes/overview.md+ per-theme pages +creating-themes.md - 🟡
CardFeatureEditorialDemo.svelteregistered innav.tsand[...slug]/+page.sveltedemos map
v0.5.0 — Full Plugin Lifecycle
Complete the plugin surface; uninstall + virtual modules.
- 🟠
dev.onStart+dev.onFileChangewired indev.ts - 🟠
client.onHydrate+client.onNavigateincore/client/hydrate.ts+router.svelte.ts - 🟠 Virtual modules from plugins — extend
core/plugin.tsresolver pattern - 🟡 Plugin uninstall via
bosia feat - 🟡 Docs: full plugin authoring guide
v0.6.0 — E2E Testing & Docs (Phase 5 + 6)
Full browser testing with Playwright + comprehensive test docs.
- 🟠
startTestServer()— spin up a real Bosia server on a random port for E2E - 🟠
bosia test --e2e— auto-launch Playwright with the server - 🟡 Playwright config template in
bosia createscaffolding - 🟡 Test file examples in project templates
- 🟡
bosia feat testscaffolder for generating test files - 🟠 Docs: testing guide for end-user apps using
bun test(unit-level; integration/component/E2E pending utilities)
v0.7.0 — CSS Pipeline Overhaul (deferred)
Status (2026-07-04): deferred pending Bun upstream CSS-chunk dedup under
splitting: true(issue to file — see 0.8.5). Hashed delivery shipped in 0.8.5. Per-route Tailwind chunks rejected: Tailwind v4 emits one deduplicated utilities file by design; splitting would duplicate shared utilities and ship more bytes. Component styles are already scoped + code-split viacss: "injected". Acceptance items below stay open under the upstream dependency.
Problem
- Tailwind CLI runs separately from Bun build → bundler has no view of CSS module graph
- Bun's
splitting: trueemits one CSS sidecar per chunk that imports a shared CSS file → collision when N routes transitively importapp.css - Current fix (
plugin.tsinterceptsapp.css→ empty JS module) ships ALL utilities in onepublic/bosia-tw.cssregardless of which route uses them - Doesn't scale: 100+ route apps load every utility on every page; can't lazy-load route-specific CSS; can't tree-shake unused per-route styles
Goals
- 🟠 CSS module graph dedup — bundler tracks every CSS import, identical content emitted once, referenced by N entries (Vite-style)
- 🟠 Per-route CSS chunks — each route ships only the CSS it actually uses, loaded via
<link>injected at SSR - 🟠 Drop
app.cssno-op interception incore/plugin.tsonce dedup lands - 🟡 Component
<style>blocks: continue withcss: "injected"(already scoped + deduped viacssHash) - 🟡 Tailwind into bundler hot path — port
@tailwindcss/viteshape to Bun plugin API so utilities are scanned + emitted as part of the build, not a parallel CLI step
Approach Options
- Wait on Bun upstream — file/track issue for CSS chunk dedup under
splitting: true. Lowest effort, unbounded timeline. - Custom Bun plugin — own CSS pipeline in
core/cssPipeline.ts: intercept all.cssimports, hash contents, emit one shared chunk per unique source, track route → chunk mapping, inject<link>tags viarender.headper request. - Static layout import workaround — make root
+layout.sveltea static import (not dynamic) inroutes.client.ts. Collapsesapp.cssinto entry chunk → no per-route duplication. Cheapest fix, but loses dynamic layout chains.
Acceptance
- Builds with 100+ routes succeed without the
app.cssno-op - Each route ships ≤ what it imports (verified by inspecting
dist/client/*.csssizes) - Component
<style>still scoped viacssHash - No regression in
test/svelte-build.test.ts(CSS collision regression test)
Not Planned
Intentional omissions — out of scope for the framework:
+page.ts/+layout.tsuniversal load (decided against)- Image optimization (infrastructure concern)
- i18n (user's responsibility)
- Rate limiting (reverse proxy concern)
- General adapter API (Bun + Cloudflare Workers only; Deno/Vercel/Node not designed for)
- Service worker tooling (out of scope)