Bosia Bosia v1.2.9

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.serve had no websocket, fetch dropped server. +server.ts exports websocket; 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.
  • 🟡 $types was generated only for page dirs; +server.ts-only dirs get Params + WebSocketHandler now. Workers guard flags websocket exports.
  • 🟡 Review: one shared UPGRADED Response 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 named BODY_SIZE_LIMIT and called 0 "Infinity"; a requested subprotocol failed silently (now a warning). One redirect and one seconds parser.
  • 🟡 Review: X-Forwarded-Proto: wss (Traefik) → 403; Upgrade without Sec-WebSocket-Key ran upgrade(); 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 strict applyForwarded() (proxy.ts); CSRF compares url.origin only.
  • 🔴 On main too: page cache key was path + query only, but url follows Host/X-Forwarded-*. A spoofed one poisoned pages rendering url.origin. Key now has url.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). Add ORIGIN/ALLOWED_HOSTS and reject the rest.
  • 🟡 X-Forwarded-Proto: constructor hit an inherited key of SCHEMES and dropped a valid forwarded host. Now a Map (proxy.ts), as is cookies' sameSite table (same bug).
  • ⚪ Review: a path with a raw | (/files/a|b) was indexed as /files/a, so invalidate("/files/a|b") missed it. pathOfKey cuts at ? or |o=.
  • ⚪ Review: a data request's _invalidated mask went raw into the cache key (1?x broke path invalidation). Only a 0/1 last 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") returned Object. 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 read constructor sees Object. 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 _invalidated name 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 without Secure, so Chrome drops the cookie silently. Warn, or keep Secure for None.
  • ⚪ 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 (protocols on 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, relative use:enhance and loader redirects matched no route: full reload (since 0.5.10). router.navigate resolves them (toRouterPath).
  • 🟡 Review: goto("//other.site/x") stayed a raw path; with a root catch-all route it matched and pushState threw. Other origins now go out as a full URL.
  • ⚪ Review: a root path skipped the URL parser, so goto("/café") matched slug = "café" (server: caf%C3%A9), /a/../b and /\host matched raw. Every URL is parsed now.
  • 🔴 Review: goto("javascript:…") (also data:, vbscript:) went to window.location and ran: XSS via goto(params.get("next")). toRouterPath throws on them like redirect().
  • 🟠 Review: redirect() tested the raw string: java\tscript:, \x01javascript: passed, htt\tps://evil.com and /\evil.com counted as relative. Parsed with new URL now; one DANGEROUS_PROTOCOL (basePath.ts) for both sides.
  • 🟡 Review: same-origin blob: URLs became https://host/uuid (404); only http(s) counts as in-app. Error says navigation:, not goto():. Docs say "rejects", skill no longer calls ?next= safe.
  • 🔴 Review: parsing against the http://bosia.invalid sentinel let http:evil.com pass 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, with allowExternal too (was passed through). Security guide documents both.
  • 🔴 Review: a same-origin // path (/.//evil.com) came out of toRouterPath as //evil.com, read as a host: off-site. It stays a full URL now; link clicks pass anchor.href, readDataResponse returns res.url.
  • 🔴 Review: popstate, hydration and the hash-link branch stored raw location.pathname; Back onto //evil.com/x hard-loaded evil.com. All go through locationPath() now; hover prefetch skips // links.
  • 🔴 Review: on main too, a catch-all route 308'd //evil.com/x/ to Location: //evil.com/x, and SSR page.url read evil.com. The server now 308s any //x path to /x before routing.
  • 🟡 Review: hydrate's stale-chunk reload (raw pathname) and trailing-slash replaceState (throws) on a //x page fixed; buildTarget takes params from new 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 against location.href (location.origin can be "null": threw); links take the page's scheme.
  • 🟡 Review 8: the // 308 used idempotent withBase: /sso//sso/x went to /sso/x. URL.canParse (no Safari 16) swapped for try/new URL. Stale reload appends _v as text (searchParams re-encoded the query), dropping the last one.
  • 🟡 Review 11: _v strip 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 /logout loader 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 compare locationPath(). _v drops whole & parts; rel splits on ASCII space.
  • ⚪ goto("settings") resolves against location.href, links against document.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 //x 308 (server.ts) returns before CORS, hooks and finalizeResponse: no security headers, a cross-origin preflight to //api/x fails. 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). Also redirect(303, "/sso/x") (errors.ts). Needs a base + path helper.
  • ⚪ Prefetch is keyed pathname + search, consumed by currentRoute (hash, canonical slash): /docs#intro or /about/ links fetch twice.
  • ⚪ goto("") on /a#x drops the hash: a new history entry and a scroll to top.
  • 🟡 A loader redirect navigate() refuses (hook Response with a bad/javascript: Location) throws in App.svelte's redirect branch: .catch releases 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: under BASE_PATH it skips the prefix. Send the trimmed value.
  • ⚪ redirect() gives //evil "protocol-relative" but /\evil "external URL"; the raw // check could go. DANGEROUS_PROTOCOL sits in basePath.ts; toRouterPath parses pageHref twice.
  • ⚪ A relative use:enhance redirect ("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] ← New next to live blog/new fails (a macOS/Windows host would serve blog/New.html at /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 findMatch again (one pass had swapped it for a second matcher); case pass walks routes once, part count first; matchPartsFolded in 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: matchPartsFolded moved 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, matchPartsFolded and 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 toLowerCase only: 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) failed check with 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.ts GET 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.ts GET sat in RAM per download, coalesced waiters waited the whole download, and an endless non-SSE stream held the miss gate forever. readBodyForCache stops 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. One isNoStoreCacheControl for all three.
  • 🟠 CORS replaced Vary: Accept-Encoding with Vary: Origin, so a CDN could send brotli to a client that didn't ask. Origin is merged in now; a loader's setHeaders({ vary }) no longer replaces it either (varyWithEncoding), and cached API hits keep the handler's own Vary.
  • 🟠 A Vary the key can't honor (anything but Accept-Encoding: Cookie, a handler's own Origin, 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/data request. 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 is fresh (dirty keys/URLs too, not just a tick); redirects and errors aren't kept for later clicks; a prefetch is used only when its _invalidated mask 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 with CACHE_MAX_BODY_BYTES=0), so raw + two compressed copies fit and one entry can't flush the cache.
  • ⚪ mergeVary (cors.ts) and varyWithEncoding (html.ts) share logic; getCorsHeaders still returns Vary that finalizeResponse skips.
  • ⚪ A re-hover right after a navigation re-sends a prefetch whose earlier fetch was dropped by clearPrefetches().
  • 🟠 Verified 2026-10-15: with LOAD_TIMEOUT unset (= 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 saw ECONNRESET at ~12s from IDLE_TIMEOUT. Now LOAD_TIMEOUT/METADATA_TIMEOUT default to 30000/10000 (5000/3000 failed healthy pages: a page's timer covers await parent()), parsed strictly (10s was 10ms). New CACHE_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 a stale leader and starts a new data-endpoint dedup call, 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 and dedup join): 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. dedup joiners give up at maxAgeMs too (_fresh and cache = false requests skip the gate). The derived default is clamped to 2^31-1.
  • ⚪ A hung loader still gets ECONNRESET at ~12s (IDLE_TIMEOUT 10) before the 30s timeout's 500. MAX_INFLIGHT is unbounded by default; a timed-out loader keeps running in the background (no AbortSignal for 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 / saveData check; 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 file 0). Removed from templates, demo apps and docs examples. New opt-in 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 new BosiaApp.onResponse hook (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. One console.log per batch: process.stdout.write lost its queue at exit and fs.writeSync(1) did partial writes on Bun's non-blocking stdout (870 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.
  • ⚪ access-log on Workers: no ctx.waitUntil plumbing, so an async write can be cut off and batching is off (one console.log per request); performance.now() is frozen during CPU there, so ms reads 0.
  • 🟠 Query strings fragment the cache. Every query value was its own entry, including ones that never change the page (utm_*, fbclid, gclid from ads and social links). Measured: /blog cached 19k rps vs /blog?q=<random> ~1.6k rps (12×). New CACHE_IGNORE_PARAMS (default: utm_* and common click ids; ref left 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 render page.url). An entry is queryAware when a loader or metadata() read an ignored param or the whole query (search, href, iterating searchParams) 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_TIMEOUT now on by default, a page whose metadata() 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 with LOAD_TIMEOUT/METADATA_TIMEOUT=Infinity, which the new parser still reads as off.
  • ⚪ A loader cache-control: s-maxage still lets a CDN keep a response whose metadata() failed.
  • ⚪ Tracking params: with no usable plain entry (evicted, or queryAware — e.g. metadata() builds canonical: url.href), each unique fbclid/gclid still 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), keep utm_*.
  • ⚪ Slow loaders: LOAD_TIMEOUT (30s) and CACHE_WAIT_TIMEOUT (60s) outlast Bun's default IDLE_TIMEOUT (10s), so the client of a 15s loader is cut off before either applies; documented, raise IDLE_TIMEOUT too.
  • 🟠 Client nav skipped a loader that read the whole query (url.search, Object.fromEntries(url.searchParams)): ?page=1 → 2 kept page 1. Deps now carry a "*" search param; the client compares the full query.
  • 🟠 Review: the cache regex scan missed export forms (destructuring, export let cache;, ; in a string), leaking per-user pages. Scan removed; routeCacheFlag reads the real exports each request.
  • 🟡 // export const prerender = true prerendered the page. The prerender check now skips comments (one regex, not Bun.Transpiler: it runs macros); ssr/actions still read raw source (fail-safe).
  • ⚪ stripComments doesn't know regex literals or ${} quotes: /'/ before a // export const prerender = true still prerenders. Gone: the analyse step reads mod.prerender.
  • ⚪ Serializing the url proxy after load() returns (return { link: url }, console.log(url)) calls toJSON/toString and 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 = false in +layout.server.ts / +layout.svelte was ignored. routeCacheFlag now reads every layout module too: a layout opt-out covers all pages below it.
  • 🟡 Client loader cache didn't track parent(). Deps now carry uses_parent; maskBitsFor (shared by nav + prefetch) re-runs a parent() caller when a layout above it re-runs.
  • 🟡 metadata() deps stayed on the server: a load() reading metadata kept 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 after load() read metadata miss the page deps.
  • ⚪ The metadata getter merges deps on any read, so {...event} or logging the event re-runs load() on metadata()'s deps too (extra re-runs, never stale).
  • 🟡 Prerender/ssr/actions/trailingSlash were read by regex (missed export { p as prerender }, let). New analyseRoutes() 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/lib stays 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() saw import.meta.dir under .bosia/analyse. A source-path rewrite broke strings and missed dist/server; dropped. Both point at the bundle; docs say use process.cwd().
  • ⚪ Review 3: dev wipes the analyse cache on start; cached manifest is dev-only; Tailwind killed on any exit (process exit hook); prod skips mtime stats; shared ROUTES_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 on import.meta.dir/url in route files.
  • 🟡 Review 4: .env edits re-read dev flags; a file deleted mid-rebuild no longer crashes dev; docs: live load() must not read src/ (images ship dist/ 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 catches import.meta.path/filename.
  • 🔴 Review 6: analyse after postScan clobbered plugin-set flags and bundled plugin routes outside src/routes → back before postScan; prerender raw-imports unanalysed (plugin) routes.
  • 🟡 Review 6: only a timed-out dev import waits for a save (a throw retries); fresh keeps last flags as fallback and forces a scan; late rejection checked before prerender + postBuild.
  • 🔴 Review 7: postScan-added routes read prerender from their module again; analyse stubs bosia: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 prerender from 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 prerender set (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 true gets 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's prerender = 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 analysedExports from 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.
  • ⚪ The bosia:routes stub is empty during analyse, and prerender calls entries() on those modules: an entries() 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.meta warning scans route files only; a $lib helper using import.meta.dir in entries() 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 sync writes .bosia/routes.ts with placeholder trailingSlash/prerender (no analyse, so bun run check in CI needs no DB). Build overwrites it.
  • ⚪ detectPrerenderRoutes re-reads every page server the scanner already checked. It now uses route.prerender (pages + APIs); the skip warning moved to the analyse step.
  • ⚪ routeCacheFlag treats a module that fails to import as cache = false without logging, and re-imports it twice per request (once() drops rejections); the cause is never shown.
  • ⚪ routeCacheFlag imports 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.ts or +page.svelte that throws on import gives a plain-text 500, not +error.svelte (SSR); already so on main.
  • ⚪ Plain-URL lookup + queryAware code 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 handle hook or plugin render using the param isn't tracked (served the param-less copy); new URL(path, url) reads toString: re-runs on any query change (accepted; use url.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 the null match; renderErrorPage matches again).
  • ⚪ Data endpoint calls computeCacheKey twice (sort + sha256 each). SSR ?_invalidated writes an entry no normal request reads.
  • ⚪ app.html segments re-interpolated per request; public env script and safeJsonStringify escape map rebuilt per call; plugin fragments awaited one by one in two passes.
  • ⚪ Permanent MutationObserver on document.body even with no viewport links; scroll/snapshot maps never trimmed; use:enhance double submit sends two POSTs; loaderCache.layouts keeps every layout visited.
  • ⚪ Files over ASSET_CACHE_MAX_FILE_BYTES retry cache.fill() (stat + promise) every request.
  • ⚪ parseBytes lives in both cache.ts and assetCache.ts (assetCache is Bun-only, cache.ts also runs on Workers).

bosia 1.2.5 (2026-10-15) — prerender checks the server's reply

  • 🟡 prerenderOne saved any reply and logged ✅. Now redirect: "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] ← new with src/routes/blog/new fails as "served by". Replaces the clash ❌ for that case.
  • ⚪ The trailing-slash 308 runs before the prerendered lookup: /always/one 308s to /always/one/, /about/ (never) to /about.
  • ⚪ Kept the isStaticPath page rule in every slash mode (review 2): the reply check catches .png too, but later and less clearly, and always never fetches /logo.png.
  • ⚪ test/prerender-server.test.ts builds and boots real fixtures: 308s on saved pages, and a failing build listing every bad reply.
  • ⚪ docs: [...slug] skips id (the id landing owns /id); the id landing is prerendered on purpose. content/docs/id/index.md is 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); describeError keeps an error's code and cause.
  • ⚪ Review: docs skip every locale index via LOCALES (not a hard-coded id); routing and cloudflare skills say what now fails.
  • 🟡 Review 2 (regression): one AbortSignal.timeout was 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. prerenderDataProblem rejects redirect/error payloads.
  • ⚪ Review 2: page and data fetched side by side (Promise.allSettled; an unread reply is cancelled); describeError reads plain-object causes, 3 deep.
  • ⚪ Review 2: the 404 probe shares prerenderReplyProblem (status 404) and still follows redirects: a catch-all always page 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 to infinity (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 ❌; one fetchText helper; 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=255 and LOAD_TIMEOUT=PRERENDER_TIMEOUT in the child (Bun's idle timer is coarse, ~4-5s at 1).
  • 🟡 Review 5: prerender child gets IDLE_TIMEOUT=0 (off, not 255) and METADATA_TIMEOUT=PRERENDER_TIMEOUT; a timed-out metadata() was swallowed and the page saved without its title.
  • 🟡 Review 5: PRERENDER_CONCURRENCY caps 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 main saved).
  • 🟡 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.0 worked in 1.2.4).
  • ⚪ Review 9: fetchFailure gets 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 (/login is 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=1 flag the server reads would cover all.
  • ⚪ Review 4: data fetch errors carry data (timeouts too); 404 probe drops its body via dropBody; ready poll uses HEALTH_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.
  • ⚪ findMisownedTargets copies matchRoutes' 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.json alias (/api/x.json served by /api/[slug]); a narrow case, add it if anyone hits it.
  • ⚪ PrerenderEntriesError now also carries reply/timeout/server-start failures; rename to PrerenderError (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); 0 or infinity means 5000. Share one documented "off" value.
  • ⚪ The "no load or metadata limit" test checks env strings only; test the effect (a 300ms loader with .env LOAD_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.aborted first.
  • ⚪ 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/about are one file on macOS/Windows); values ending .html/.json and Windows device names (con, nul) fail too.
  • ⚪ Combined PrerenderEntriesError keeps every route's error in cause. 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 index value fails: written as <parent>/index.html, it replaced the parent URL's page (even a live one). .html/.json checks only the last part.
  • ⚪ Clash check dedupes repeated values, compares every folder level ignoring case (A/x vs a/y) and includes __bosia/data files (/ vs /index).
  • 🟡 Clash check: a file and a folder at one path clash (a.json vs a.json/edit); dedupe keys on slash mode; the saved 404.html is owned. Values can't start with ..
  • 🟡 Dedupe only within one route: a static /about and slug about in the same slash mode were merged, dropping a page. Only a slug 404 is barred from 404.html; a static /404 page may replace it.
  • ⚪ PARAM_NAME is 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 as report.pdf.html; a static host asked for /files/report.pdf may 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/, and isStaticPath endings (.png) for pages. Case-sensitive like the server.
  • ⚪ Static prerender routes skip the value rules: src/routes/_health saves the health JSON; a static logo.png or index folder too. Scanner check; separate branch.
  • ⚪ A public/ file at a prerendered path (public/about.html vs slug about) is silently replaced by generateStaticSite. Seed the clash check from public/.
  • ⚪ A static folder index at a route's end (src/routes/blog/index) in never mode writes blog/index.html, replacing /blog/. Scanner check; separate branch.
  • ⚪ Device-name check rejects com0/lpt0, which Windows doesn't reserve; use com[1-9]|lpt[1-9].
  • 🟡 isStaticPath moved to side-effect-free staticPath.ts: importing it from html.ts loaded that module in the build before loadEnv(), 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 mode logo.png passed as /logo.png/, but /logo.png 404s before the 308.
  • ⚪ A slug __bosia-404-probe that a loader answers with 200 breaks the 404 probe. Reserve the probe path.
  • 🟡 URLs can't start with dist/__bosia in any case: on macOS/Windows DIST/ swallowed the dist/client bundle copied after it. Pure checkPrerenderPaths().
  • 🟡 test/prerender-invariants.test.ts: sweeps values × patterns × modes; checks outcomes (server reach via real isStaticPath, 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 /_health live once in staticPath.ts (isFrameworkPath, HEALTH_PATH); server.ts and prerender share them.
  • ⚪ ~ is allowed in values, so on Windows with 8.3 names GUIDES~1/intro can point at another route's guides-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) and dev.ts still hard-code /_health; use HEALTH_PATH from staticPath.ts.

bosia 1.2.3 (2026-10-13) — same-page refresh keeps scroll

  • 🟠 invalidate*()/use:enhance on the same URL jumped to top: settleScroll read a leftover router.isPush. The nav effect now sets navPending on 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:enhance redirects go through internal go(..., invalidateAll): every loader re-runs (logout must refresh layouts), a same-page redirect refetches, and a stale form is cleared.
  • 🟡 goto(sameUrl, { invalidateAll: true }) bumps the tick and refetches; it used to resolve with stale data.
  • 🟡 router.navigate() returns started/same/cancelled/left; goto() marks dirty, bumps, and sets noScroll only for a nav that happens. Test: goto.test.ts.
  • 🟡 Hash-only shortcut drains goto() resolvers (a #hash redirect hung use:enhance); every dirty.all setter also bumps the tick, so it refetches.
  • 🟢 /about/ on /about returns same after canonicalizing; no duplicate history entry.
  • 🟠 A loader redirect that doesn't start a nav (cancelled/same/left) drains resolvers; awaited use:enhance hung 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 noScroll or 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 awaited goto() resolves anyway (pre-existing).
  • 🟠 A rejected route chain (stale chunk, throwing module) tears down the loading UI, drains resolvers, then rethrows so hydrate.ts still runs its cache-busting reload.
  • 🟡 Reload fallbacks use hardLoad(): href = path without its #hash (with it, href only scrolled; reload() could re-POST a native form entry).
  • 🟡 An invalidate()/goto(sameUrl, { invalidateAll }) from a child's onMount refetches; hydration's first effect absorbed the tick and hung the goto().
  • 🟢 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 via abandonRun().
  • 🟡 A child's onMount goto('/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 /other as a normal nav; it hung before.
  • 🟡 A +loading.svelte chunk resolving after its run settled no longer re-shows the skeleton over the loaded page (pre-existing).
  • ⚪ A use:enhance redirect cancelled by beforeNavigate clears form and doesn't refetch, though the action mutated. Needs go() to expose its NavResult.
  • 🟡 Loader redirects replace the redirecting entry, so Back no longer bounces off it. The hop keeps the nav type, but popstate/enter become goto (it is a cancellable push).
  • ⚪ owedRefresh re-runs every loader with fresh; keeping the cancelled run's dirty set until it applies would refetch less.
  • 🟠 owedRefresh clears only when fresh data applies (error/failed-fetch branch kept it); lastNavKey/lastSettledPath set only after the redirect check.
  • 🟡 A cancelled run's chunk rejection is swallowed, so hydrate.ts doesn'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 reset suppressScroll, so a superseded noScroll goto 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 on router.loading (set by App's run), not the resolver queue — it missed link-click navs.
  • 🟢 hardLoad() uses location.replace (no extra entry); a loader redirect keeps a noScroll goto's flag; same-page hash clicks reset suppressScroll.
  • 🟡 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.loading clears at settle.
  • 🟡 A loader redirect back to the shown page runs as a real nav (forgets lastNavKey); off-app redirects location.replace; settle() drains before afterNavigate so a listener's goto() waits.
  • ⚪ Relative goto("#x") matches no route, leaves via location.href, and the null-state popstate resets historyIndex to 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.svelte chunk is swallowed: no skeleton, and no hydrate.ts reload of a page that loaded fine.
  • 🟢 navigate() sets router.loading on started, so a same-tick goto() 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 moved false).
  • ⚪ A redirect back to the shown page superseded before settling leaves lastNavKey = "": next Back refetches and clears form.
  • 🟠 export const cache = false in +page.server.ts was silently ignored. Now honored: routeCacheFlag reads both modules' real exports; either false opts out.
  • 🟡 Navigating to a route with +loading.svelte remounts its layouts (skeleton branch renders them separately): layout state like an open sidebar resets.
  • ⚪ await goto() resolves before scroll-to-top is applied, so a scrollTo() right after it gets overwritten.
  • 🟠 Route order: sortRoutes tie-broke same-tier routes by localeCompare ([ < letters), so /[a]/[b] beat /label/[id]. Now per segment: static > dynamic > catch-all.
  • 🟡 compilePattern copied a catch-all's prefix literally: /[lang]/[...rest] never matched. Legacy matchPattern (same bug) removed; findMatch compiles an unseen array once.
  • 🟡 Segment kinds were parsed four ways (matcher, sort, scanner, route types) and disagreed on mid-path/hyphenated [...x]; one segmentRank() in matcher.ts now 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.
  • ⚪ isExact and prerender used includes("["); both now use isDynamic() (segmentRank), so /api/[id].json is static everywhere.
  • ⚪ segmentRank is strict (SEGMENT regex): malformed segments rank static, checkParams runs 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 with findMatch on 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 ...; prerender replace read $& in values (function replacer now).
  • ⚪ substituteParams goes per segment (segmentRank): throws on a missing entries() 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 (no process.exit); missing entries() export fails too. [path..]/[x…] typos caught.
  • ⚪ Prerender let %2e%2e/%2F through (home HTML saved as blog/%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/%2F pass. 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: SEGMENT runs 2–3× per segment, and checkParams keeps 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.html with a 200 for unknown paths. Prerender now saves the server's 404 as dist/static/404.html; docs' own copy in post-build.ts is 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.html only when wrangler sets 404-page (server boots just for it).
  • ⚪ /404.html (and /404 on Cloudflare/Netlify/Pages) answers with a 200 serving the 404 page. Any file name does this; add noindex if crawlers index it.
  • 🟠 Review: the ready loop ignored the child's exit, so a foreign server's /_health passed. 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.html was missing; it skips only before a build now. Deploy docs: 404-page is for assets-only deploys.
  • 🟡 A failed 404 probe fails the build when wrangler.* sets not_found_handling: "404-page" and there's no public/404.html (needsStatic404); else it warns. Test: needsStatic404.test.ts.
  • ⚪ needsStatic404 reads 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 (see docs/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.svelte whose chunk hasn't loaded no longer counts as shown when deciding what remounts.
  • 🟡 --target=workers skips the 404 probe: the uploaded 404.html only answered /404.html with 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.
  • ⚪ needsStatic404 also matches 404-page in an env section 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 receivedSignal is set.
  • ⚪ A prerendered /404 route (trailingSlash: "never") overwrites the probe's 404.html in generateStaticSite, 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 (nested load, page load unless masked, metadata when 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.finally is not a function) when LOAD_TIMEOUT was set. withTimeout wraps it in Promise.resolve.
  • 🟡 npm tarball lacked LICENSE (MIT requires the notice). Copied into packages/bosia; npm packs it without a files entry. Copyright: "Bosapi and contributors"; AUTHORS lists people.
  • 🟢 packages/bosia/LICENSE is a hand copy of the root one; copy it in a prepack script 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>, else Bosia App. Demo ui.e2e.ts test, 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-time headTitle/layoutTitles scan 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=2 reset 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.html title entities (&mdash;) showed raw. Build ships the raw text; the browser decodes it (a <textarea>; raw text under Trusted Types).
  • 🟡 Review: bosia sync ignored app.html's title. It reads it too; the title argument is required now. headSplit moved to appHtml.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.ts once (readSource), not once for prerender and again for trailingSlash.
  • 🟠 Review 3: an app with no root layout kept the old title when the layout count went to or from zero (keptLayouts was -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, uses titleText, 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, currentLayoutPaths named 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 afterNavigate that 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 window with a dev warning; /title-live really dynamic; clientDefaultTitle() and sysScriptBody() helpers.
  • ⚪ Not caught: an {#if} page title on one dynamic route (/blog/a → /blog/b), and a layout title set in $effect/onMount. Documented; use metadata().title.
  • ⚪ A layout that moves to or from the page's spot (layout count changes) remounts, since renderLayout uses another branch there. Title code allows for it; worth a look for state loss.
  • 🟠 Review 7: Object.hasOwn in the title patch broke hydration on Safari 15.0–15.3; uses hasOwnProperty.call now.
  • 🟢 Review 6: the decoded default title collapses whitespace like document.title; bosia sync warns 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.title patch with an explicit layout title (layout metadata().title or a title registry). The patch leans on Svelte effect order.
  • ⚪ A +loading.svelte chunk 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 Found title and drop the last page's metadata tags; an error/layout <title> still wins.
  • 🟡 Review: error → error with the same +error.svelte keeps its own title; status title format shared via errorTitle() (headScan.ts).
  • 🟡 Review: prerender watches child.exited (signal kills); 404-page builds also fail when the server won't start, no routes prerender, or the 404 isn't HTML.
  • ⚪ needsStatic404 misses single-quoted TOML ('404-page') and checks config files in a different order than wrangler.
  • ⚪ The Bun server serves dist/static/404.html at /404.html with 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=workers never requires 404.html; SSR-only 404-page builds 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 no metadata().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 its data to page load() (was null on client nav). Form re-render overlaps them too, like SSR.
  • ⚪ Hook-thrown redirect()/error() now go through finalizeResponse (security headers, CSP, CORS, cookies). 4 new cases in hooks-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 pin prettier 3.9.9 + prettier-plugin-svelte 3.5.2 exactly; repo reformatted. Bump all five together.
  • 🟠 <svelte:head> SSR output goes in the raw <head> via buildMetadataChunk. The insertAdjacentHTML script and buildHtmlTail's head param 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 and headClose skipped %bosia.nonce%. Both builders now share titleFallback(); buildMetadataChunk takes nonce.
  • 🟡 <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; findHeadEnd scans for the first real </head …> (else <body), skipping comments and script/style; split cached per segments in a WeakMap. errMeta uses shared hasTitle(). Docs: escape < in JSON-LD.
  • ⚪ buildHtml takes headExtras; buildMetadataChunk takes options. hasTitle is 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) and hasTitle (html.ts); their rules differ (e.g. <script-loader>, svg/noscript/template, unclosed tags).
  • 🟢 buildHtml takes an options object (17 positional params; a misplaced undefined goes unnoticed) and decides the error-page title slot itself, instead of the renderer (errMeta).
  • ⚪ Error pages never render plugin head fragments, so a plugin <title> or tag is missing on 404/500 (renderErrorPage).

bosia 1.1.6 (2026-10-08) — HEAD = GET

  • 🟡 +server.ts exporting only GET answers HEAD (was JSON 405). Allow lists HEAD with GET. An explicit HEAD export still wins.
  • 🟡 HEAD reads the GET cache entry on pages and API routes (never coalesces or writes), so curl -I no longer reruns loaders.
  • ⚪ Cache-hit HEAD sends the real Content-Length (BosiaApp nulls HEAD bodies, which made it 0).
  • 🟢 HEAD misses still report Content-Length: 0: BosiaApp nulls 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 handleRequest before handle (same order as resolve(): 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 real params.
  • ⚪ 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 to guardHandle.
  • test/route-id.test.ts: hook guard stops SSR, client nav, form POST; metadata() counter proves it never ran.
  • 🟡 page.route.id on the client page (SvelteKit parity): clientRoutes emit id, derived from page.url via findMatch. null on 404. Tested in ssr-page-url.test.ts.
  • ⚪ Rule: guard in hooks. metadata() stays parallel with layout loaders (no wait); a layout redirect can't stop it. Pinned in route-id.test.ts; security + metadata guides say so.
  • 🔴 Chrome e2e found: bosia_session not in CACHE_KEYS → signed-in pages cacheable across users. auth feature + shop/store .env.example set it; bosia feat ignores # 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. serveStatic read Bun.file from disk on every request.

  • 🟠 New core/assetCache.ts: byte-budget LRU, filled lazily on first hit (no boot I/O, cold start unchanged). serveStatic answers hits from memory; .br/.gz cached per variant.
  • Env ASSET_CACHE_MAX_BYTES (50MB, 0 = off) + ASSET_CACHE_MAX_FILE_BYTES (1MB), in FRAMEWORK_VARS. Not STATIC_*: 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: /about br 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-env skill, .env.example.
  • 🟡 FRAMEWORK_VARS was missing CACHE_*, IDLE_TIMEOUT, MAX_INFLIGHT, TRUST_PROXY, CSP_DIRECTIVES… Added, BOSIA_* reserved via isFrameworkVar; a test scans src/ for env reads.
  • 🟡 Brotli split: q3 per request, q5 for cache entries. Uncached /blog br 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).
  • ⚪ serveStatic sends a strong ETag (size+mtime, per encoding) and answers If-None-Match with 304. ETag only: no Last-Modified, so no heuristic freshness on public/.
  • ⚪ 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 _invalidated mask 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 real readdir names.
  • 🟠 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/data cached (URL + CACHE_KEYS + mask, _fresh=1 after invalidate), ETag 304 on hits.
  • zstd per request, Bun.gzipSync; hover/touch/focus prefetch loads page chunks; Link preload header; dev Svelte compile cache, no prerender in dev.
  • 🟡 Server tests' afterAll kills the child without await child.exited, so Windows rmSync hits EBUSY (flaky CI). 5 files: data-endpoint-cache, layout-guard, dev-static, hooks-redirect, preload-link.
  • ⚪ A redirect thrown from hooks.server.ts ships 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.ts copies registry/blocks → docs/.bosia/blocks with __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/.gz for dist/client + dist/prerendered; serveStatic negotiates. Hydrate chunk 108KB → 31KB br.
  • ⚪ public/ text files (svg/txt/json) still ship raw — the precompress pass skips the app's source dir. Precompress into dist/static if it matters.
  • ⚪ compress() is untestable for encoding under bun test (isDev read at import). Integration coverage is apps/demo curl + ui.e2e.ts; add a built-server encoding test.
  • 🟠 User handle hook runs for every static asset: static branches live inside resolve(). A session-lookup hook = one DB query per chunk. Serve manifest hits in handleRequest before hooks.
  • 🟡 Cache-write compression blocked the first byte (microtask). Fixed 1.1.3: deferCacheWrite uses setImmediate on 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; make parent() await the earlier layers (SvelteKit's model).
  • 🟡 metadata() adds a serial step: before loaders on SSR (needed only if it returns data), after loaders on the data endpoint (data unused there). Also check: page load gets metadata: null on client nav.
  • ⚪ public/ files ship with no Cache-Control (heuristic caching). Prerendered HTML max-age=3600 can 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.ts maps each route to its layout+page chunks plus static imports → manifest.preload.
  • Shell and buildHtml emit a modulepreload per route chunk (skipped when csr=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 bosia in both.
  • 🟢 Send the preloads as an HTTP Link header / 103 Early Hints so they start before the HTML arrives.
  • 🟢 Verify wrangler dev (Workers) and a BASE_PATH build 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 keeps page.params; this is Bosia's choice.

  • Removed the page.params getter + dead SSR appState.routeParams seed. SSR error layouts now get params via ssrPageData. Test probes the prop.
  • Moved the use:enhance success 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 stripped data.params. Now uses the params prop.
  • +error.svelte gets params as a prop (nested + root, SSR + client). ErrorProps.params adds string | undefined for child-route keys. Tested; demo /blog/boom shows the slug (Chrome: SSR + client nav).
  • 🟡 Dev: inspector /__bosia/errors SSE 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 declare engines.bun >=1.4. Safe floor because Elysia (hit by the 1.4.0–1.4.1 var/let bundler bug) is gone.

  • .bun-version 1.4.2, @types/bun 1.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 the as { kind?: string } cast in build.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 from import.meta.url, copies CSP nonce). Kept; modulePreload: false turns 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.ts now emits the hashed entry verbatim; test pins it.
  • Verified in Chrome: prod, dev and wrangler dev hydrate + 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 (force hid it), so rebuilds kept stale dist/. clearOutput resolves paths and finishes leftovers entry by entry.
  • 🟢 Report the Windows rmSync relative-path ENOENT to Bun with a minimal repro; drop the fallback in clearOutput once 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 parseTopFrame skipped frames with (group) paths → pointed at renderer.ts. One FRAME_RE (sourcemap.ts) now serves server, overlay, resolveStack; Windows paths too.
  • 🟡 use:enhance success never showed form: App's nav effect ran on the post-action invalidation and set form = 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.WebView drives demo in prod + dev. Hydration, SPA nav, title, back, form actions, guard, catch-all, no console errors. macOS only.
  • 🟢 Run ui.e2e.ts in CI (backend: "chrome", ubuntu job) once it proves stable locally.
  • ⚪ Add the parent() client-nav regression (0.6.17 section) to ui.e2e.ts; needs a layout+page server pair in demo.
  • 🟢 Try bun test --parallel in CI.
  • 🟢 Consider minChunkSize for the client build (fewer tiny chunks).
  • 🟢 Consider Bun.serve { dir } routes in BosiaApp for 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-in BosiaApp, 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 uses backend not needed: BosiaApp is small, so server-timing and inspector run on it everywhere.
  • 🟡 Unrouted methods (TRACE etc.) answered 500 via Elysia's NOT_FOUND. Now 405 + Allow on Bun and Workers.
  • 🟢 BosiaApp routes: add :param paths 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-test success 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.ts covers 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; sync node:crypto/node:zlib work there.

  • 🟠 Phase 0 spike: target: "browser" + external: ["node:*"]; Elysia CloudflareAdapter handles "*" routes + onError; a global Bun shim breaks Elysia's runtime detection.
  • 🔴 Phase 1: config.ts rebuilt config as { plugins }, dropping strictImports (and any future target). Now keeps every field; tests cover source + prebuilt paths.
  • 🟠 Phase 2: build artifacts read via core/artifacts.ts readArtifact(); build writes .bosia/artifacts.ts (inlined JSON), swapped in by makeBosiaPlugin(_, "workers").
  • 🔴 window.__BOSIA_ENV__ was always empty: declared PUBLIC_* names lived in the CLI process, not the spawned server. Build now stamps publicEnv into manifest.json.
  • 🟡 .bosia/artifacts.ts + runtime.workers.ts are now generated only for the workers target.
  • 🟠 Phase 3: Bun.CryptoHasher/Bun.gzipSync → node:crypto/node:zlib in cache.ts + html.ts; identity hash pinned by test. No bosia:platform: lifecycle code lives in the Bun-only listen() tail.
  • 🔴 Phase 4: server.ts → createApp(); entries server.bun.ts (hooks off disk, listen, signals) + server.workers.ts (static hooks/config via bosia: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_LIMIT also checked from Content-Length in handleRequest — Workers has no maxRequestBodySize. Chunked uploads fall back to Cloudflare's cap.
  • 🟡 Workers plugin keeps bare node builtins external — a browser-target build silently swapped fs for {}. paths.ts tolerates import.meta.dir being 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: target config + --target=workers (BOSIA_TARGET) → dist/worker/index.js, manifest target, wrangler.jsonc (written once), bosia start → wrangler dev; client chunks chunk-[hash].
  • 🟠 event.platform.env verified reaching load() — end-to-end build test imports the worker and calls fetch(req, env).
  • 🟢 Runtime vars on Workers come from wrangler.jsonc vars / secrets / .dev.vars, not .env — documented in deployment.md + bosia-env.
  • 🟡 Phase 6: workersGuard.ts fails workers builds on Bun.*, fs/node:fs, bun/bun:* in server files (routes, hooks, lib/server). BOSIA_WORKERS_GUARD=0 warns instead.
  • 🟢 Guard scans server files by path — a Bun call in a plain src/lib/*.ts a loader imports is missed. Upgrade: check in the workers plugin's onLoad.
  • 🟡 Phase 7: deployment/cli/api docs (+ id), bosia-cloudflare skill, bosia-bun-runtime + bosia-env Workers 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 through handleRequest's rebuild.
  • 🟡 apps/demo-worker: Workers mirror of apps/demo (no uploads routes, /platform reads a wrangler var). apps/demo stays Bun-only.
  • 🟡 Live demo-worker: routes, actions, CSRF, guard, 413, platform.env OK. 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+actions pages 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: findWorkersIncompatible returned \ paths (report already printed /). Hits now use posix paths on every OS.
  • Workers shared cache via caches.default skipped: per-colo, no tag/prefix purge, no-op on workers.dev.
  • 🟢 Tests ran twice on publish pushes. ci.yml → test.yml; publish.yml runs only on a package.json change 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 when package.json changed. 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.svelte in the default template created a second / next to (public)/+page.svelte. Nothing complained, and one page was silently unreachable.

  • 🔴 scanner.ts preventConflicts throws RouteConflictError when pages or +server.ts APIs match once groups and param names are ignored ([id] ≡ [slug]) — SvelteKit's prevent_conflicts, Next.js's "two parallel pages".
  • 🟠 build.ts + bosia sync print 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/) and bosia-routing skill R4 updated.
  • 🟢 CI: cleared the Node 20 deprecation — checkout@v7, upload-artifact@v7, download-artifact@v8, wrangler-action@v4 (Wrangler 4; pages deploy flags unchanged). setup-bun@v2 already 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 .bin shims and lsof.

  • 🔴 build.ts wrote \-separated client assets into manifest.json and picked the server entry via split("/") → broken asset URLs and bosia start pointing at a full path. Now toPosix(relative()) + basename().
  • 🔴 safePath.ts checked root + "/", so on Windows every public/dist/static file resolved as "outside" and 404'd. Uses sep now.
  • 🟠 paths.ts: BOSIA_NODE_PATH joins with path.delimiter (C: split the old : list); resolveBosiaBin tries .exe/.cmd/.bunx on win32 so Tailwind is found. New shared toPosix().
  • 🟠 scanner.ts builds manifest paths with / by hand instead of join; inspector data-loc, sourcemap cwd-relative paths and create --local file: paths also normalized.
  • 🟡 port.ts falls back to netstat -ano on Windows (listener = foreign :0, since the state column is localized). Parsers parseLsof/parseNetstat unit-tested.
  • 🟠 New ci.yml: tests + scaffold/build/start/asset-probe smoke on ubuntu + windows. publish.yml split into decide/test/build-templates/publish-npm/release/refresh-assets; publishing now waits on both OSes.
  • 🟡 .gitattributes forces LF so Windows checkouts don't fail prettier --check. Getting Started lists Windows (native or WSL2).
  • 🔴 First CI run failed on prettier --check: bun.lock was gitignored, so CI installed prettier 3.9.9, not 3.8.3. Now committed (without untracked apps/demo-worker); Bun pinned via .bun-version.
  • 🟠 Tests blocked the 0.9.7 publish, but decide only published when the push diff touched package.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: resolveLocalRegistry used new URL(import.meta.url).pathname (/D:/… on Windows) → import.meta.dir. Tests: scanner helper split on /; svelte-audit deleted its cwd (EBUSY).
  • 🟢 windows-latest CI green (513 tests + create/build/start/asset smoke); 0.9.7 published via workflow_dispatch after the npm-based decide change.
  • 🟠 dev-smoke CI job (ubuntu + windows, .github/scripts/dev-smoke.sh): dev serves, picks up a page edit + new route, pidsOnPort finds the proxy, and a restart after a hard kill reaps the orphaned app server.
  • 🟡 Manual bosia dev on 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.json held "css": [] and .landing__title lived in a +page-*.js chunk. Every scoped <style> in every app arrived with the JS bundle, so the streamed SSR markup painted first and hydration moved it.

  • 🔴 svelteCompiler.ts compiled the browser target with css: "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.ts collects result.css.code in the onLoad hook and writes ONE bosia-css-<hash>.css, rather than letting Bun emit per-chunk sidecars. Keeps splitting: true at zero CSS outputs, so the collision class stays out of reach whether or not Bun regresses. Mirrors twHash.ts: rebase url(…) before hashing, or a BASE_PATH mount serves a component's background image off the origin.
  • 🔴 The inspector plugin does its own .svelte compile and registers first, so its onLoad wins in dev. Changing only svelteCompiler.ts would have fixed production and left bosia dev flashing — the exact split that hides a bug until deploy. Both compile external now; 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.css was already rendered as <link> at four sites in html.ts — but ahead of twCssLink(). Scoped rules used to be appended to document.head at hydration, i.e. last, and the stylesheets app.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.ts threads +loading.svelte down the walk the way layoutChain already 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.svelte needed nothing — loadingDepth reads the shared-layout prefix, not the file's location.
  • 🟠 test/svelte-build.test.ts kept its cssOutputs.length === 0 assertion (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.ts gained the ordering assertion that never existed, test/scanner.test.ts the cascade with a nearer file overriding.
  • 🟠 apps/demo gained /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 under BASE_PATH=/demo it becomes url(/demo/favicon.svg) and the seal loads — the failure mode this guards is a silent 404, not an error.
  • 🟡 /loading-test/nested carries no +loading.svelte and 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, .landing resolving to bosia-css-*.css. Dev SSR class names match the dev stylesheet — the inspector uses fnv on 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.url is the page URL before the hooks run, not after. The /__bosia/data/<route>.json → /admin rewrite sat inside resolve() (server.ts:253), so a guard — which runs before await resolve(event) — saw the transport path and its startsWith("/admin") was false. curl /__bosia/data/admin.json was the whole exploit.
  • 🔴 throw redirect() / throw error() from a hook could never work: dist/hooks.server.js keeps bosia external (build.ts:24), so the hook's Redirect came from the app's node_modules while the server bundle carried its own class. instanceof compared two different objects and was always false → real 500. Branded both classes with Symbol.for and swapped all 23 instanceof sites for isRedirect()/isHttpError().
  • 🟠 A hook's raw 3xx on a data request becomes {redirect, status} JSON. fetch follows a 303 silently, so the router used to receive the login page's HTML at 200. Location ships verbatim — redirect() already rebased it through withBase(), so rebasing again would double a BASE_PATH prefix.
  • 🟠 The router stopped reading "not JSON" as "the loader crashed". readDataResponse() reads redirected, the status and the content-type that were sitting on the response all along and were thrown away by .catch(() => null). A hook's text/plain 404 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 — url is the same either way on purpose.
  • 🟡 The parse is keyed on the incoming Request in a WeakMap, since event.url no longer carries the marker and hooks call resolve(event) themselves. Handing resolve() a fabricated Request detaches it — documented on Handle, pinned by a test rather than papered over with a path sniff that could not fire anyway.
  • 🟠 test/hooks-redirect.test.ts boots 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/demo gained /guard-test. Verified in Chrome against both builds: unfixed, the click renders the panel and its rahasia sentinel 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-shell and bosia-sidebar both teach active-nav state from page.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.ts returned http://localhost/ with no window, and router.currentRoute initialised to "/". renderer.ts has the real url and already passes it to every load() and metadata().
  • 🟠 One renderWithPageContext() wrapper seeds router.currentRoute/.origin and appState.routeParams, then renders — at all four render() 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.ts strips BASE_PATH once at the top of handleRequest, but hydrate.ts assigns raw window.location.pathname and clientRoutes carry the prefix. Seeding the stripped path would render /profil and let hydration flip it to /sso/profil — the same jump, inverted. withBase(currentBase(), …) puts it back.
  • 🟡 A deliberate exception to the rule in App.svelte and appState.svelte.ts (server singletons hold no per-request state): page is imported directly by user components, so props cannot reach it. Safe only because bosia destructures render() synchronously — svelte's RenderOutput is PromiseLike, so this is bosia's commitment, not svelte's. The wrapper makes it structural.
  • 🟡 page.params was {} on the server for the same reason — appState.routeParams is never written during SSR. Seeded from pageDataFull.params, the source App.svelte already reads. Dropped the write-only router.params cell (router is not public API).
  • 🟠 test/ssr-page-url.test.ts boots 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 with Received: "/". A client-side test would agree with both the broken and fixed code — the fallback never fires in a browser.
  • 🟠 test/basePath-server.test.ts gained the mounted case, asserting the prefixed path. Verified failing with an app-space seed (data-path="/profil"). BASE_PATH must be set at build time too, so it extends the existing fixture rather than standing up a second build.
  • 🟡 apps/demo nav now sources active state from page.url.pathname per bosia-page-shell R6, plus a /page-url-test route 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, canonical and og:title still held the homepage's values. Two defects — the data endpoint dropped the fields, and the router applied only the two that survived.

  • 🟠 /__bosia/data/*.json sends meta, link and lang alongside title/description. Explicit whitelist, not a spread: metadata.data feeds load() server-side and may hold secrets, so it stays off the wire.
  • 🟠 The router rebuilds the head from that payload instead of poking document.title and one description tag. metadataTags() stamps data-bosia-meta on what it owns, so headExtras and <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.ts asserts the data endpoint carries every head field and omits metadata.data. Verified failing on the old server before the fix — the DOM half is straight-line createElement, 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 to Bosia App on two unrelated routes. renderPageWithFormData never called loadMetadata() and rendered through a buildHtml that 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 from buildMetadataChunk and shared with buildHtml, 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.data reaches load() on POST. It was hardcoded null, so a loader consuming it silently returned different content after a submit — the one part of this that was never cosmetic. metadata.lang and plugin head/bodyEnd fragments restored on the same path.
  • 🟠 redirect() / error() thrown inside metadata() work. loadMetadata caught everything, so every caller's Redirect/HttpError branch was unreachable dead code; both are now re-thrown ahead of the catch-all log.
  • 🟡 Error pages title themselves 404 — Not Found instead of Bosia App. Synthesized only when the error component set no <title>, so a custom +error.svelte still wins. The route's own metadata() is deliberately not run — the route may be what failed.
  • 🟠 test/formAction-metadata.test.ts boots a real built server and submits a real form: title, description, og tag, lang, metadata.data, GET parity, redirect() from metadata(), and the 404 title. A unit test on buildHtml would 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_PATH in .env.production, build.ts calls loadEnv() after its imports, so the import-time paths.BASE_PATH was "" while the lazy currentBase() was /sso — CSS silently kept origin-root url()s.

  • 🟡 One base source. paths.BASE_PATH deleted; twHash.ts, html.ts, server.ts, prerender.ts and client/base.ts all read currentBase(). Fixes the .env.production divergence: twHash.ts now reads post-loadEnv.
  • 🟡 basePath stamped into manifest.json at build; the server warns on boot when it disagrees. !== undefined so older dist/ 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/imagesrcset rebased per candidate, descriptors and spacing intact. A value containing data: is skipped whole — its commas are not separators.
  • 🟠 test/basePath-server.test.ts boots 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, every redirect() emits a root-absolute Location, 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-authored href="/daftar" walks off to the neighbouring app, and hydrate.ts matches location.pathname against a route table that has no prefix, so the app mounts into a permanent "Loading…".

  • 🔴 BASE_PATH=/sso — normalized in core/basePath.ts (pure, env-free, shared by both bundles so the two halves of the router cannot disagree). server.ts strips the prefix once, before anything reads a pathname; off-mount requests 404.
  • 🔴 clientRoutes are 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. serverRoutes stay app-space, since the server strips at the edge and app code should never learn it is mounted.
  • 🔴 Root-absolute literals in .svelte markup 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.
  • 🟠 Redirect rebases in its constructor — one choke point, so every redirect() 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 + CSS url()), never the JSON data islands — those are loader output and a path rewrite would corrupt them. Covers src/app.html via interpolateSegment.
  • 🟠 Compiled Tailwind CSS rebased at build time (twHash.ts, before hashing). A @font-face src inside a .css file 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.
  • 🟡 base exported from bosia, for URLs built in <script> — the compile-time rewrite only sees markup. Verified against the Fisika design system: Icon.svelte builds url("/icons/…") in a $derived and needs it.
  • 🔴 Trailing-slash 308 was emitting an app-space Location (server.ts), so /sso/about/ bounced the visitor to /about. A trailingSlash: "always" route did it on every request. Rebased — it was the one Location not coming from Redirect.
  • 🔴 Prerendering never ran under a base: prerender.ts fetched the spawned server at the origin root, which now 404s, so /_health never 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.ts masked <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 the form prop silently loses it under the default use:enhance.

client/enhance.ts:53 sets appState.form = result.data, then (with invalidateAll on, the default) bumps appState.invalidationTick at :60. That tick fires the nav effect in client/App.svelte, which does appState.form = null at :144 before 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 failure branch (enhance.ts:48-51) sets appState.form and returns early without invalidating, so fail() 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.form on the form's own invalidation. Fixed in 1.1.0 (84763a4): App.svelte clears it only when pathname+search changes.
  • 🟡 Dev warning when invalidateAll discards success data not needed, the data is no longer discarded.
  • 🟡 Document invalidateAll: false vs latching in the forms guide not needed: default update() keeps form and refreshes loaders.
  • ⚪ Ujiku (akun/profil, akun/keamanan) latches success into local $state before update(). 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 check failed with Module '"$env"' has no exported member 'PUBLIC_GTM_ID'. Runtime values already come from process.env as designed — but envCodegen.ts derives the export names from whatever loadEnv(mode) found, so a runtime var vanishes from $env when its file isn't loaded. CI has only .env.production, and bosia sync runs dev mode unless NODE_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 of process.env so ?? default fallbacks survive.
  • 🟡 Test: .env.production alone still yields the PUBLIC_* export (empty string), no .env needed. Plus mode-scoped values, .env.example names, .envrc ignored, shell env fills a name-only key, malformed off-mode file throws.
  • 🟢 guides/environment-variables (EN+ID) "Names vs values" section + bosia-env R4/R5 — .env* is the name manifest; a host-supplied var still must be declared or bun run check fails.

docs 0.8.8 (2026-07-30) — Purge stale `ui/icon` refs

The <Icon> wrapper died in 2bdf95d (2026-06-06) but its references outlived it. bosia-dashboard and bosia-mobile-screen still ran bosia add … ui/icon — a registry id that no longer exists, so the CLI fails mid-workflow.

  • 🟠 ui/icon dropped from bosia-dashboard + bosia-mobile-screen (frontmatter requires.components and the bosia add line). No replacement dep — @lucide/svelte is npm, and registry components declare it in their own npmDeps.
  • 🟡 bosia-design-review P1 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 from components/ui/icon.md (EN + ID); the npmDeps note folded into Install. Docs now have zero mentions of the old API.
  • 🟢 bosia-icon added 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 start processes sat on :9000 at once, no error. Elysia's Bun adapter defaults to reusePort: true (elysia/dist/adapter/bun/index.js:163-190), so SO_REUSEPORT let the second bind succeed and the kernel load-balanced between two different dist/ 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.
  • 🟡 reusePort off in server.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 start now propagates the child's exit code.
  • 🟢 BOSIA_REUSE_PORT=1 opts back in, for the N-workers-one-port case Elysia's default was meant for. Docs + skill + CLI reference updated.
  • 🟢 pidsOnPort() extracted to core/port.ts from dev.ts's inline lsof; 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 dynamic entries() imports concurrently via Promise.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 cpSync static mirror — marginal gain on a once-per-build copy, real staleness risk.

bosia 0.8.13 (2026-07-23) — CSRF webhook exemption

  • 🟢 CSRF_EXEMPT_PATHS env 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-.json paths skip the microtask hop.
  • 🟢 Lazy cookie parsing; skip page-module import on cache hits via build-time cache flag (flag removed in 1.2.8, see routeCacheFlag); module-level TextEncoder; 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.

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 with onDecision hook.
  • 🟢 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.xml fed by src/features/seo/config.ts; PUBLIC_BASE_URL envVar, request-origin fallback.
  • 🟢 Docs: blocks/{newsletter,consent}.md, guides/{newsletter,seo}.md + id/ mirrors, 2 demos, nav; bosia-sections → 27 blocks / 13 families; bosia-seo points at feat seo.

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-sections skill → 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-blog skill 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 via ui/typography), related row.
  • 🟢 registry/pages/blog/{index,post} — navbar/footer compositions; optional posts / post / bodyHtml / related props 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), seed 003_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-blog skill now requires the blocks and points at feat 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 (action prop 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-sections skill → 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-list and stats/sentiment-bar named 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 docks block category, adapted to semantic tokens + @lucide/svelte.

  • 🟢 registry/blocks/docks/{edge,floating,pill,fab}/ — self-contained <nav> tab bars; active tab bg-primary/10 text-primary, FAB bg-primary, count/dot badges; registered in registry/index.json.
  • 🟢 Docs — blocks/docks.md (+ DocksDemo wired into slug route), overview section, bosia-docks skill.

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-walked src/routes every build, and slept a flat 200ms before broadcasting reload.

  • 🟡 core/devWatch.ts (new) — shouldIgnoreForRebuild (md/test/spec/swap files) + affectsRouteManifest (only + files & dirs under src/routes matter); test/devWatch.test.ts.
  • 🟡 core/dev.ts — watcher + mtime poll skip ignored files; routesDirty flag → BOSIA_SKIP_ROUTE_SCAN env on the build spawn (always scans in managed mode); Bun.sleep(200) → waitForAppHealthy() polling /_health (50ms, 10s cap).
  • 🟡 core/build.ts — when flagged, reuses previous route-manifest.json (read before cleanup) instead of scanRoutes(); falls back to a real scan if missing.
  • ⚪ Known ceiling: apps importing .md from src/ lose live-reload for those files; drop .md from the ignore list if it bites.

0.8.9 (2026-07-14) — `create` into current/existing folder

create required a name and refused any existing dir. Users want create . / 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); thread projectName (not path) through copy/prebuilt/finish; drop misleading cd when 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 fixed r.bottom+6 — off-screen for elements near the bottom of the page/iframe (rukoku editor embed).

  • 🟠 plugins/inspector/overlay.ts — mount at 0,0, measure offsetWidth/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 the default template's check script ran sync — demo/shop/store (rukoku apps) passed bun run check with __BRAND__ still in the footer.

  • 🟠 templates/{demo,shop,store}/package.json — check now runs bosia sync && first (also fixes tsc checking stale .bosia/ codegen).
  • ⚪ Existing scaffolded apps keep their baked check script; 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 by history.state.bosiaIndex, restored post-tick() after the destination mounts; persisted to sessionStorage on unload (JSON-serializable values only).

  • 🟠 core/client/router.svelte.ts — pageSnapshots map + savePageSnapshot() at the four saveScroll() sites; pendingSnapshot handoff (popstate + reload-restore in init()); 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.getPageSnapshot closure; settleSnapshot() in settleScroll() + first-hydration branch.
  • ⚪ Snapshot<T> type in bosia/client + generated $types.d.ts (routeTypes.ts); routeTypes.test.ts assertion; docs guides/navigation (en+id) "Snapshots" section; bosia-navigation skill R2b; CHANGELOG.md appended.
  • ⚪ Deliberately skipped: layout snapshots (need per-depth instance refs in the recursive layout snippets); non-JSON values (no devalue); inherits the beforeunload mobile 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-cookie and duplicates throw → existing 500 path. Headers land on SSR HTML, the response-cache write, form-action re-renders, and /__bosia/data/*.json. Data endpoint: loader cache-control wins over the computed default unless cookies were accessed — then private, no-cache wins (privacy beats intent).

  • 🟡 hooks.ts — LoadEvent.setHeaders type + makeSetHeaders() (lives here so tests avoid bosia:routes); renderer.ts wires it into both loader events, returns loaderHeaders, spreads onto response sites + cache write.
  • ⚪ compress() + serveCached() base keys lowercased so extraHeaders override instead of comma-joining.
  • ⚪ server.ts data endpoint destructures loaderHeaders out of the JSON body, merges with cookie-privacy override.
  • ⚪ test/setHeaders.test.ts; docs guides/server-loaders (en+id) incl. missing depends row; bosia-routing skill R3b; roadmap check-off; CHANGELOG.md; version 0.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.svelte skeletons). 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 in history.state.bosiaIndex; save on push/hash-push/popstate/unload; pendingScroll handoff; sessionStorage persistence + reload restore.
  • 🟠 core/client/App.svelte — shared settleScroll() replaces the two duplicated top-scroll blocks; popstate branch restores router.pendingScroll after tick().
  • ⚪ Docs guides/navigation (en+id) "Scroll behavior" section; bosia-navigation skill notes it's built-in (never hand-roll in afterNavigate). CHANGELOG.md appended.
  • ⚪ 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) — add visibilitychange/pagehide fallback 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.build harness). Prod was never at risk — its client compiler uses css:"injected" (styles in JS, no chunking). Only the dev inspector used css:"external" + a hand-rolled runtime <style> injection to dodge the (now-fixed) collision — functionally identical to injected, just more code.

  • 🟠 plugins/inspector/bun-plugin.ts — inspector compile now css: generate==="client" ? "injected" : "external", mirroring svelteCompiler.ts. Deleted the virtualCss map, the VIRTUAL_NS virtual-module resolve/load handlers, and the runtime injection block (~30 lines); dropped unused basename import.
  • ⚪ test/svelte-build.test.ts — new dev-inspector case: many +page routes 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.css with 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 via twHash.ts); tw field in dist/manifest.json; all 4 html link sites via twCssLink(), no cache buster; fallback /bosia-tw.css for old artifacts. Immutable caching free via HASHED_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 to css:"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.brotliCompressSync doesn'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_BYTES per-entry cap (default 2MB, 0 unlimited); guard in cacheSet + early-skip before compression.
  • 🟡 P1 — brotli via node:zlib quality 5 (also un-breaks brotli entirely).
  • 🟡 P2 — prerendered pages: boot-time manifest (buildPrerenderManifest) instead of per-request exists() probes.
  • 🟡 P3 — percent-encoded static paths 404 fixed (decodeURIComponent in lookupStatic + dev fallthrough).
  • 🟠 Identity-aware dedup — data-endpoint dedup key now includes the CACHE_KEYS identity hash (computeCacheKey), so per-user routes dedup safely. BREAKING: (private) no longer skips dedup; custom session cookies must be in CACHE_KEYS.
  • 🟡 P4 — LRU eviction is O(evicted.tags), not O(all tags): LRU.set returns the evicted {key,value} so cacheDeleteIndexOnly uses entry.tags directly.
  • 🟠 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 next round-trip — registry/features/auth login/register now honor ?next= via safeNext (same-site paths only, else /dashboard); was always forcing /dashboard.
  • 🟠 Route scope deleted — zero runtime consumers after identity-aware dedup; (private) is an ordinary route group (scanner/types/ambient/codegen field removed).
  • 🟠 Miss coalescing — coalesceMiss gate 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 flips accessed/readNames, so it can't force Cache-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 into pages/storefront/product; docs product family + demo.
  • 🟠 Phase B — empty-state, cart-lines, wishlist-grid blocks; pages/storefront/{cart,wishlist}; order-summary gains a cta label prop; docs family + 2 page docs + 3 demos.
  • 🟠 Phase C — search-overlay block + pages/storefront/search (header already had onSearch); layout family docs + search page docs + demo wiring.
  • 🟠 Phase D — store/orders.ts samples; 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-view modal (+ product-card onQuickView eye button), mega-menu standalone 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-switcher component. 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-tokens skill — "Runtime light/dark switch" note so agents reach for ui/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, fail bosia 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 — scan src/, fail on leftover __BRAND__.
  • ⚪ bosia-block-compose skill — R8 + checklist gate for the placeholder.
  • ⚪ Version bump bosia 0.8.3 + docs 0.8.1 + both CHANGELOG.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/rebuild and reads the real { ok } result. Default unset → normal bosia dev byte-for-byte unchanged.

  • 🟠 packages/bosia/src/core/dev.ts — BOSIA_DEV_MANAGED=1 gate; skip the fs.watch + mtime-poll starts.
  • 🟠 packages/bosia/src/core/dev.ts — buildAndRestart() returns Promise<boolean> (real build result).
  • 🟠 packages/bosia/src/core/dev.ts — POST /__bosia/rebuild sibling 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.svelte in a route folder renders automatically during nav, nested inside the layouts shared with the current page. No app-side resolver, no beforeNavigate wiring.

  • 🟠 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 — emit loading import + layoutPaths identity keys in both clientRoutes generators.
  • 🟠 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 includes loading: null.
  • ⚪ test/scanner.test.ts — detect-vs-null +loading.svelte case.
  • ⚪ Docs (en+id): guides/routing (Loading Skeletons), guides/navigation (cross-ref), project-structure table, reference/sveltekit-differences row.
  • ⚪ Skills: bosia-routing (R5b + checklist + source), bosia-navigation (R9 + source).
  • ⚪ Version bump 0.8.0 (framework + docs) + both CHANGELOG.md.
  • ⚪ Follow-up: nearest-ancestor +loading.svelte inheritance (full Next.js segment semantics) — currently per-folder only.
  • ⚪ Follow-up: warm the +loading.svelte chunk on hover/viewport in prefetch.ts so 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 via buildContext(el) in the payload comment, so the visible header is pure clutter — remove it.

  • 🟠 packages/bosia/src/core/plugins/inspector/overlay.ts — openForm() no longer builds the chain/header; form.innerHTML starts at the <textarea>. buildContext/submit unchanged, 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: SAMEORIGIN to every response after the user handle runs, 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 — export NO_FRAME_GUARD_HEADER (internal marker header).
  • 🟠 packages/bosia/src/core/server.ts — when a response carries the marker, strip it and skip only X-Frame-Options (other security headers stay).
  • ⚪ packages/bosia/src/lib/index.ts — re-export NO_FRAME_GUARD_HEADER from the public API.
  • ⚪ Version bump 0.7.7 (framework) + CHANGELOG.md.

0.7.5 (2026-06-21) — `store` template (Postgres + MinIO/S3)

shop bakes SQLite + empty S3 vars. Add a sibling store template that defaults to Postgres (native Bun.SQL via drizzle-orm/bun-sql, no driver dep) and MinIO/S3 uploads — the production-shaped stack — so create … --template store needs no post-scaffold rewiring.

  • 🟠 packages/bosia/templates/store/ — copy of shop; template.json all dialects postgres; .env.example Postgres URL + MinIO defaults; instructions.txt + README.md reworded; data/ dropped (S3 uploads, no SQLite file).
  • 🟠 packages/bosia/src/cli/create.ts — register store in TEMPLATE_DESCRIPTIONS.
  • ⚪ Docs — getting-started template table + reference/cli --template line & bullets (en + id).
  • ⚪ bosia-shop-template skill — cover the store sibling (postgres default, no driver/@aws-sdk deps).
  • ⚪ 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 to primary; dark bars invert to bg-foreground text-background, glass uses backdrop-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 in index.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-navbars design 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 layout navbars/*). Rule documented in bosia-heros/bosia-block-compose R7/bosia-page-shell R1.
  • 🟠 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/login block. 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 reusable auth/* 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-line variant swap, mirroring the storefront purpose pattern. Backend stays in bosia-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 in index.json.
  • 🟠 6 pages under registry/pages/auth/{login,register,forgot,magic-link,otp,sso}/ — composition only; each meta.json lists its auth/* block deps so the installer recurses; default variant="centered", swappable to split.
  • ⚪ Docs — new blocks/auth page + 6 pages/auth/* pages (7 demos registered), Pages overview gains an auth section, nav gains Blocks → Auth and Pages → Auth groups.
  • ⚪ New bosia-login design 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 shop was slow: installFeature fetched 150+ files serially from GitHub raw. Fix: CI bakes the finished scaffold into a version-locked Release tarball; create downloads + extracts it. Registry path stays the fallback.

  • 🟠 packages/bosia/templates/shop/template.json — opt in with "prebuilt": true (only heavy templates need it; default/demo have no template.json and are already instant).
  • 🟠 cli/create.ts — when prebuilt && !--local && !BOSIA_BUILDING_PREBUILT, download v<version>/<template>.tar.gz, extract, substitute {{PROJECT_NAME}}, restore .env; falls back to registry on 404/offline.
  • 🟠 scripts/build-prebuilt-templates.ts (new) — iterate templates/*, skip unless prebuilt; scaffold with literal {{PROJECT_NAME}} + --no-install via local registry; tar -czf dist/prebuilt/<t>.tar.gz.
  • ⚪ packages/bosia/package.json — build:templates script; version 0.7.3.
  • ⚪ .github/workflows/publish.yml — Bun setup + bun run build:templates; attach dist/prebuilt/*.tar.gz to the release.
  • ⚪ .github/workflows/refresh-prebuilt.yml (new) — registry/template changes without a version bump rebuild artifacts and gh release upload --clobber onto the existing release. Keeps the create fast path in sync.
  • ⚪ Docs — reference/cli.md documents 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_block before brief_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) — ## Todo seed 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), component leaf, capped text, framework-stripped tree — so the AI traces props back to the page, not the leaf.

  • 🟠 inspector/overlay.ts — isFrameworkFrame/fileOf/userFrames/findPageFile/ownText/buildContext helpers; submit() sends buildContext(el) + "---" + comment; form header shows the framework-stripped chain.
  • ⚪ Docs — guides/inspector.md aiEndpoint section rewritten with the labeled format + why pageFile matters.
  • ⚪ Skill — bosia-inspector-edit/SKILL.md rewritten to the new [Inspector] contract (split on ---, default to pageFile over the component leaf, use url/text to disambiguate).

0.7.0 — Deprecated `page.params` fallback

Re-adds a deprecated, working params getter on the page object (bosia/client) so legacy code reaching for page.params keeps working. Returns reactive appState.routeParams, dev-only warn-once, removed in 1.0.0. $props() stays taught.

  • 🟠 page.svelte.ts — add get params() returning appState.routeParams; @deprecated JSDoc + dev-only console.warn (warn-once, guarded by NODE_ENV !== "production" so it tree-shakes out of prod).
  • ⚪ Docs — guides/routing.md deprecated callout; reference/roadmap.md line updated; reference/changelog.md entry. Skills left as the correct $props() pattern.

Shop template defaults to sqlite-file

Flips the shop template 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 for data/.

  • 🟠 packages/bosia/templates/shop/template.json — all five featureOptions dialects postgres → sqlite.
  • 🟠 packages/bosia/templates/shop/.env.example — DATABASE_URL → sqlite://./data/app.db; instructions.txt wording → SQLite.
  • 🟠 registry/features/drizzle/drizzle-index.sqlite.ts — mkdirSync(dirname(path)) before opening the file (skip :memory:) so db:migrate against ./data/app.db doesn't ENOENT.
  • ⚪ _gitignore ignores data/*.db*; ship data/.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 page registry tier (page = group of blocks, no backend) via bosia add page. 24 storefront/* blocks compose 4 pages sharing one runes cart, mapped to existing themes + a new clay theme.

  • 🟠 New page registry tier + CLI — cli/page.ts (runAddPage), routed in addRouter.ts (+ pages/... alias); pages added to Manifest and RegistryIndex; bosia add list shows 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 in index.json.
  • 🟠 Added the clay theme (registry/themes/clay/) — warm paper neutrals, terracotta primary, soft warm shadows, Newsreader/Hanken Grotesk/Geist Mono fonts; registered in registry/index.json (19 themes).
  • 🟠 Four pages under registry/pages/storefront/{home,listing,product,checkout}/ — composition only, one shared createCart() wired through header/grids/drawer; each meta.json lists 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; new bosia-storefront skill; $lib/blocks/$pages aliases + registry/pages Tailwind @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 to primary; 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.json so /api/blocks lists each with its own install line); single-hero previews; new bosia-heros skill; nav grouped by vertical; skills-index row.

0.6.24 — Port the Cardstock card system (29 blocks + 6 themes)

Replaces the single cards/feature-editorial block (now removed — it painted its numeral with accent, so it never followed the brand). The new cards map the brand colour to primary and use semantic tokens only, so they restyle across themes.

  • 🟠 Extended the theme contract on all 12 existing themes — added --font-mono and theme-scoped elevation (--shadow-xs/sm/md/lg wired 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 in registry/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 the bosia-landing/bosia-saas-landing/bosia-block-compose skills to cards/feature (now takes title/body/icon/cta props).
  • ⚪ Docs — 6 per-category card pages with live demo galleries + per-card install lines; new bosia-cards skill cataloguing all 29.

0.6.24 — Remove the Todo template + feature ahead of rebuilding card blocks

The todo starter template (and its todo feature/components) was the original CRUD demo. We're rebuilding the card blocks from scratch, so it's being pulled to clear the way. The editorial card block stays until replacements exist.

  • 🟠 Deleted packages/bosia/templates/todo/, registry/features/todo/, registry/components/todo/, and docs/content/docs/components/todo/.
  • 🟠 registry/index.json — dropped todo from features and the four todo* components entries.
  • 🟠 packages/bosia/src/cli/{create,index,add,feat}.ts — removed the todo template description/example and re-pointed the stale todo doc-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, the nav.ts Todo group, and the bosia-block-compose example.

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 one THEME_INIT_JS constant; now resolves an explicit system value live against prefers-color-scheme, compatible with stored dark/light.
  • 🟠 navbar.svelte — replaced the buggy isDark boolean (never persisted, out of sync with FOUC) with a 3-state light/dark/system cycle that persists to localStorage, 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 svelte to ^5.56.3 in the 5 stale package.json files (docs, 4 templates); root, packages/bosia, and apps/demo were already current.

0.6.24 — `add theme` strips the template's default `:root`/`.dark` block

A theme's tokens.css defines its own :root/.dark, but the template's app.css ships a default block earlier in the file. After add theme both coexisted at the same specificity and the template's won.

  • 🟠 templates/{default,shop,demo}/src/app.css — wrapped the default :root/.dark token block in /* bosia-theme-vars */ sentinels so removal targets only the template defaults, never user :root rules. @theme {} stays above, untouched.
  • 🟠 cli/theme.ts — runAddTheme now calls stripTemplateThemeVars(appCssPath) after wiring the @import, removing the sentinel-bounded block (idempotent). The installed theme's :root becomes 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 shop template and patched 5 install-blocking bugs. Three reflect cross-cutting gotchas future authors will hit again, and the shop package.json still carried a stale postgres dep.

  • 🟠 core/dev.ts — the proxy's "App server is starting…" 503 is now an HTML page carrying the /__bosia/sse reload 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/hold suppresses the SSE reload broadcast, POST /__bosia/resume flushes one reload if any rebuild fired. hold doubles as a heartbeat (90s safety timer)
  • 🟠 packages/bosia/templates/shop/package.json — drop unused "postgres": "^3.4.0"; scaffold uses Bun-native drizzle-orm/bun-sql against Bun.SQL, no userland driver needed.
  • 🟠 bosia-bun-runtime/SKILL.md — new ## Postgres — Bun.SQL section with the object-form snippet + Bun 1.3.x FailedToOpenSocket gotcha; banned-packages table gains postgres/pg rows pointing at Bun.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") covering meta.json#files[].target depth, with the auth feature'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 fills children from whitespace around named snippets).
  • 🟠 features/shop/admin-sidebar.svelte — Orders is now a parent grouping All orders/Pending/Refunds under one SidebarMenuItem. Logo <img> tags gain h-5 w-5 so 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, no postgres/pg/@aws-sdk, routes under (private)/dashboard/, services not db.select
  • 🟠 core/server.ts — API-route handlers now treat throw redirect(...)/throw error(...) like page actions (303 / typed JSON) instead of a generic 500; also supports return redirect(...). Found via shop POST /logout.
  • 🟠 templates/{default,shop,todo,demo}/src/app.css — add @custom-variant dark (&:where(.dark, .dark *));. Tailwind v4 defaults dark: to prefers-color-scheme, so the navbar's classList.toggle("dark") was a no-op.
  • 🟠 features/shop/admin-sidebar.svelte — header rebuilt per SidebarDemo: theme-aware logo button doubling as a collapse toggle, brand row, working <SidebarTrigger>
  • 🟠 registry/features/shop/meta.json — add "ui/breadcrumb" to components so bosia create … --template shop actually scaffolds the breadcrumb files.
  • 🟠 templates/shop/src/routes/(private)/+layout.svelte — derive segments from page.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, --local flag drop, dialect default under skipPrompts:true) deliberately omitted — code-only fixes, no public API to document.
  • 🟠 cli/block.ts — session-level installed Set mirrors add.ts:40; runAddBlock early-returns when a block is already installed this session. Without it files/upload-area was re-written during shop scaffold (two pullers).
  • ⚪ registry/features/shop/meta.json — drop ui/typography (unused) and ui/form / ui/input / ui/button (already declared by auth; component installer dedupes via add.ts:116 so 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-dialect user/session tables, Bun.password.hash argon2id.
  • 🟠 registry/features/rbac/ — permission table with composite PK, can(userId, resource, action, scope?) with * wildcards, resources.ts registry, auth-handle.ts rewritten to attach locals.can(...), seed grants * to the first user.
  • 🟠 registry/features/shop/ — multi-dialect product/order/order_item/cart_item tables, valibot validators, repositories + services, PublicNavbar + AdminSidebar, resources.append.ts adds eight shop.* permissions.
  • 🟠 templates/shop/ — thin glue: template.json declares features: ["auth","rbac","file-upload","shop"]
  • ⚪ packages/bosia/src/cli/create.ts — TEMPLATE_DESCRIPTIONS map gains shop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart".
  • ⚪ docs/content/docs/getting-started.md — template table gains a shop row.

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 in DropdownMenu swallowed the href; (2) skipping the user footer.

  • ⚪ components/ui/sidebar.md — new "Choosing the right item shape" table with a "never wrap in DropdownMenu" 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, icon snippet, bind:collapsed, user-footer, floating/side/align, trigger="hover". P0 checklist gates.
  • 🟠 dropdown-menu.svelte — context exposes triggerEl/setTriggerEl. Trigger bind:this syncs via untrack
  • ⚪ SidebarDemo.svelte — leading "Dashboard" leaf with LayoutDashboard icon (R1); user footer rebuilt with DropdownMenu + Avatar, justify-start on 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-editorial 404'd (CLI only routed block singular); (2) bosia add ui/typography aborted on EACCES overwriting a foreign-uid file.

  • 🟠 packages/bosia/src/cli/addRouter.ts — new dispatcher; routes blocks/<cat>/<name> tokens to runAddBlock, splits mixed batches (components + blocks) into one runAdd call plus per-block calls.
  • 🟠 packages/bosia/src/cli/index.ts — add case calls routeAdd with injected runners; help text adds the alias.
  • 🟠 packages/bosia/src/cli/registry.ts — new writeRegistryFile(dest, content) helper does unlink + retry on EACCES/EPERM, then surfaces a chown hint if still failing.
  • 🟠 packages/bosia/src/cli/add.ts and block.ts — component/block file write loops route through the new helper.
  • 🟠 core/client/page.svelte.ts (new) — reactive page object backed by $derived over router.currentRoute; exposes page.url/page.pathname. No params getter — Bosia already plumbs params into +page/+layout via $props().
  • 🟠 packages/bosia/src/lib/client.ts — re-export page.
  • 🟠 docs/content/skills/bosia-block-compose/SKILL.md — canonical bosia add block cards/feature-editorial example.
  • 🟠 docs/content/skills/bosia-saas-landing/SKILL.md and bosia-landing/SKILL.md — split single install line into per-category calls (theme / components / block).
  • 🟠 docs/content/skills/bosia-svelte-runes/SKILL.md R6.5 — illustrative "removed import" example switched from page to a deleted cn utility so the lesson no longer contradicts the now-real page export.
  • ⚪ packages/bosia/test/registry.test.ts — coverage for routeAdd dispatch (block, alias, mixed batch, multi-block, plain components), readRegistryJSON blocks-category path, writeRegistryFile happy-path/overwrite.
  • ⚪ packages/bosia/test/page-store.test.ts (new) — compile-and-wiring checks for the page module (compiles via svelte/compiler, output references derived, bosia/client re-exports page).

0.6.21 (2026-06-09) — `bosia-image-dialog` skill + block (multi-image picker)

AI-generated apps seed images.unsplash.com placeholders but there was no UI primitive to swap them without clobbering. New files/image-dialog block composes UploadArea + an External URL tab + an existing-images gallery.

  • 🟠 registry/blocks/files/image-dialog/ — block.svelte (Dialog + Tabs + selection state, embeds UploadArea, fetches /api/files once on mount) and meta.json (deps: button, dialog, input, label, sonner, tabs, upload-area).
  • 🟠 registry/index.json — add files/image-dialog to blocks.
  • ⚪ 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.svelte registered in docs/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 — add ui/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 in docs/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.webmanifest 404'd.

  • 🟠 packages/bosia/src/core/server/staticServer.ts — add .webmanifest to the isStaticPath whitelist.
  • 🟠 packages/bosia/src/core/build.ts:156-170 — hash the client entry filename (naming.entry: "[name]-[hash].[ext]") so staticManifest serves it as immutable and per-build URL changes invalidate the browser cache automatically.
  • 🟠 core/client/hydrate.ts — add a production-only unhandledrejection handler that detects failed dynamic import() and triggers location.replace(?_v=…). Loop-guard via sessionStorage["bosia:reload-attempt"] (10s window).
  • ⚪ Follow-up: surface a stale-build event on the router (onStaleChunk hook?) 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.css 404'd because generateStaticSite() early-returned when dist/prerendered/ didn't exist, so public/ never mirrored to dist/static/. Fix: always mirror public/

  • 🟠 packages/bosia/src/core/prerender.ts — generateStaticSite() now copies public/ unconditionally; SSG-specific copies (prerendered + client mirror) gated on dist/prerendered/ existing.
  • 🟠 Test: build emits dist/static/ with public/ 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/svelte dep.
  • 🟠 registry/components/ui/icon/ — delete; remove ui/icon from registry/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 docs SidebarDemo.
  • 🟠 components/ui/icon.md rewritten as a @lucide/svelte guide with a deprecation callout; overview.md, navbar.md, upload-area.md, crop-image.md callouts updated; IconGrid.svelte deleted.
  • ⚪ New docs/content/skills/bosia-icon/ skill steering AI agents toward @lucide/svelte (never lucide-svelte).

Same-day addition (2026-06-06) — production runtime needed `src/app.html`

Bug from komba Dockerfile: runner copies only dist/. Container boots → renderer.ts reads src/app.html from 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 — add writeAppHtmlSegments(segments, outDir) (serializes to ${outDir}/app-html.json); getAppHtmlSegments(cwd) now tries persisted artifact first, falls back to loadAppHtmlTemplate(cwd).
  • 🟠 packages/bosia/src/core/build.ts — after writing route-manifest, call writeAppHtmlSegments(appHtml) so production runtime has the segments inside dist/.

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.ts read src/hooks.server.ts from cwd with no fallback → locals.user undefined. Same for bosia.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 — new bundleRuntimeUserFiles(cwd) step after the server bundle: externalize npm + bosia/elysia/bun/svelte, Bun.build src/hooks.server.ts → dist/hooks.server.js and bosia.config.* → dist/bosia.config.js
  • 🟠 packages/bosia/src/core/server.ts — hook loader checks ${OUT_DIR}/hooks.server.js first, falls back to src/hooks.server.ts. Log line reports which path won.
  • 🟠 packages/bosia/src/core/config.ts — loadBosiaConfig checks ${OUT_DIR}/bosia.config.js first, 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 of public/ written by build) so prod images can ship only dist/. addOnce keeps public/ 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()).farmId worked on SSR but returned undefined on 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 a parentSnapshots payload in the POST body (the client already has cached data per skipped layer); merge into parentData before 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 in App.svelte (nav) + prefetch.ts via a shared buildParentSnapshots helper — POST with { parentSnapshots } only when non-empty, else keep the cacheable GET.
  • 🟠 packages/bosia/src/core/server.ts — /__bosia/data branch parses parentSnapshots from the POST body (guarded try/catch → undefined on GET/malformed) and forwards into loadRouteData via a new arg.
  • 🟠 core/renderer.ts — loadRouteData accepts parentSnapshots
  • ⚪ bosia-routing/SKILL.md — added a rule under R3: don't use parent() for scope identifiers (farmId/orgId/userId), read from event.locals; parent() is fine for view-layer data, locals is the source of truth for cross-loader scope.
  • ⚪ Regression test under apps/demo: a layout server returning { orgId }, a child +page.server.ts asserting (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>.webp reach +server.ts, but the default handler had no auth/ownership check and file had no userId

  • 🟠 registry/features/file-upload/file.{sqlite,pg,mysql}.table.ts — add user_id NOT 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 on locals.user; uploads-static responds with Content-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 to public/uploads, drop user_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>.webp 404'd even with a +server.ts at /uploads/[...path]. Root cause: the static-files block (matches by extension) ran before resolveApiMatch, so any static-extension URL short-circuited into static lookup.

  • 🟠 packages/bosia/src/core/server.ts — move the API-match block (+server.ts resolution + 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-test renders 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-level const. 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 drizzle and got sqlite://:memory:, losing data on restart. Three drifts: meta.json DATABASE_URL was a comment string with :memory: last; the runtime fallback was :memory:

  • 🟠 registry/features/drizzle/meta.json — single concrete DATABASE_URL=sqlite://./data/app.db, no inline comment options.
  • 🟠 registry/features/drizzle/drizzle-index.ts + drizzle.config.ts — runtime fallback now sqlite://./data/app.db, not sqlite://:memory:.
  • 🟠 docs/content/skills/bosia-database-setup/SKILL.md — default scheme updated to sqlite://./data/app.db; references to src/features/drizzle/db.ts corrected to index.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 function over plain http (LAN IP, preview subdomains)

  • 🟠 field, tooltip, popover, hover-card, nav-menu-item, menubar-menu — replace crypto.randomUUID().slice(0, 8) with $props.id(). menubar-menu now prefixes its id with menubar- for consistency with siblings.
  • 🟠 registry/components/ui/tabs/tabs.svelte, sidebar/sidebar-menu-item.svelte, accordion/accordion.svelte, collapsible/collapsible.svelte — replace Math.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 separate Math.random() calls into one $props.id() shared by titleId and descriptionId.
  • ⚪ Server-side crypto.randomUUID in features/file-upload/file.service.ts and file.{mysql,sqlite}.table.ts left as-is — runs in Node where crypto is 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 via brief_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 the brief_request_approval tool call.
  • 🟠 bosia-database-setup/SKILL.md — new skill replacing bosia-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 by bosia-database-setup.
  • ⚪ docs/content/skills/SKILL.md catalog — count bumped 44 → 45; added a bosia-database-setup row to the "Conventions — framework ·" section.
  • ⚪ docs/content/skills/bosia-drizzle-usage/SKILL.md + references/troubleshooting.md — swapped every bosia-brief-database reference to bosia-database-setup.

Bosia's SPA router intercepted every <a> click, re-ran the full page load, then scrollTo(0,0). Hash-only links like <a href="#features"> never scrolled because e.preventDefault() killed the browser default.

  • 🟠 core/client/router.svelte.ts — short-circuit same-page hash nav in the click handler: when anchor.pathname + anchor.search matches the current location and a hash is present, skip navigate()
  • 🟠 core/client/router.svelte.ts — export scrollToHash(hash) helper: decodes the fragment, resolves getElementById, calls scrollIntoView(), returns whether it found a target. Used by both the click handler and App.svelte.
  • 🟠 core/client/App.svelte — replace unconditional window.scrollTo(0,0) after nav settle with tick().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.svelte instead of +layout.svelte, (b) hand-rolling <table> instead of ui/data-table, (c) shipping repository list with no limit/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.ts to produce data.user. R5 forbids hand-rolled <table>
  • 🟠 bosia-query-defaults/SKILL.md — new skill. R1 fixes the repo signature to list(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_install and got a silent 404 (the block path lives under blocks/, not components/). The retry hung on @clack/prompts because the MCP runner never closes stdin. Framework fixes only — no per-app patching.

  • 🟠 cli/feat.ts — FeatureMeta gains blocks?: string[]. After component install, iterate meta.blocks and call runAddBlock so block deps route to the block installer (was 404'ing through addComponent
  • 🟠 cli/block.ts — runAddBlock accepts InstallOptions, gates the "Replace existing block?" prompt behind !skipPrompts, threads options into recursive addComponent calls, honors -y
  • ⚪ packages/bosia/src/cli/index.ts — add block dispatch now passes through -y (was filtering to -- long flags only).
  • ⚪ registry/features/file-upload/meta.json — moved files/upload-area and files/crop-image from components to the new blocks field.
  • 🟡 bosia-file-upload/SKILL.md — R5 now shows the explicit import UploadArea from "$lib/blocks/files/upload-area/block.svelte" and full props; workflow step 5 gains db: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-upload and hit three regressions: (1) Bun.Image.open/decode doesn't exist, uploads crash; (2) PUBLIC_BASE_URL bakes the wrong host into stored URLs.

  • 🔴 features/file-upload/file.service.ts — rewrite the Bun.Image pipeline. Drop the decodeImage shim. Use new Bun.Image(bytes), read dims from metadata(), positional resize(w, h, opts), .webp({ quality: 85 }).bytes()
  • 🟠 features/file-upload/storage-local.ts — save() returns relative /uploads/${key} (no PUBLIC_BASE_URL prefix). The browser resolves against the page origin → works for lvh.me preview, prod domains, and localhost without env tuning.
  • ⚪ registry/features/file-upload/meta.json — drop misleading PUBLIC_BASE_URL=http://localhost:3000 default (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.ts if 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 — new Bun.Image section: constructor, metadata(), positional resize, per-format encoders (.webp({ quality: 0–100 })), .bytes(). Anti-pattern callout for Bun.Image.open/decode. (F4)
  • ⚪ docs/content/skills/bosia-file-upload/SKILL.md — R2 cross-references the new Bun.Image section in bosia-bun-runtime for 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.ts with HTTP verb exports
  • 🟠 Error pages — +error.svelte

Data Loading

  • 🔴 Plain export async function load() pattern (no wrapper)
  • 🟠 $types codegen — auto-generated PageData, 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)
  • 🟡 /_health endpoint
  • 🟠 Cookie support (cookies.get, cookies.set, cookies.delete)
  • 🟡 Cookie sameSite accepts both casings (lax/Lax) — normalized to canonical header
  • 🟠 Protocol-aware Secure cookies — auto-downgrade over HTTP with warn; TRUST_PROXY=true honors x-forwarded-proto
  • 🟠 Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
  • 🟡 DISABLE_X_FRAME_OPTIONS=true env var to omit X-Frame-Options for intentional cross-origin iframe embedding
  • 🟠 Graceful shutdown handler (SIGTERM/SIGINT)
  • 🟠 .env file support with $env virtual module
  • 🟡 CORS configuration (framework-level)
  • 🟠 Session-aware fetch (cookies forwarded in internal API calls)
  • 🟡 Request timeouts on load() and metadata() 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; outer bun run reports 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/ complements fs.watch so 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, sameSite values
  • 🟠 PUBLIC_ env scoping — only expose vars declared in .env files
  • 🟠 Streaming error safety — validate route match before creating stream
  • 🟡 safeJsonStringify crash guard — try-catch for circular reference protection
  • 🟠 Open redirect validation on redirect()
  • 🟡 Cookie RFC 6265 validation — validate names against HTTP token spec; use encodeURIComponent only 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-preload attribute for hover/viewport prefetch
  • 🟠 Fix client-side navigation with query strings/hashes
  • 🟡 Use insertAdjacentHTML for 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
  • 🟠 $lib alias → src/lib/*
  • 🟡 bosia:routes virtual module
  • 🟡 Validate Tailwind CSS binary exists before build
  • 🟡 Prerender fetch timeout
  • 🟡 Fix withTimeout timer leak
  • ⚪ Remove duplicate static file serving
  • 🟠 Static site output — merge prerendered HTML + client assets + public into dist/static/ for static hosting
  • 🟡 Validate .env variable names — reject invalid identifiers that break codegen
  • 🟡 .env parser 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 --template flag and interactive picker)
  • 🟠 bosia add — registry-based UI component installation
  • 🟠 bosia feat — registry-based feature scaffolding
  • 🟡 bosia add index-based path resolution — resolves component names from index.json instead of blindly prefixing ui/
  • 🟡 bosia feat nested feature dependencies — features field in meta.json for recursive installation
  • 🟡 bosia feat overwrite prompt — asks before replacing existing files
  • 🟡 bosia add multi-component install — bosia add button card input installs all in one call
  • 🟡 bosia add -y / --yes flag — auto-confirm overwrite prompts for CI / scripts

Templates & Features

  • 🟠 todo template (formerly drizzle) — PostgreSQL + Drizzle ORM with full CRUD todo demo
  • 🟠 drizzle feature — bosia feat drizzle scaffolds DB connection, schema aggregator, migrations dir, seed runner
  • 🟠 Multi-engine drizzle feature — adapter, drizzle.config.ts, and seed-runner branch on DATABASE_URL scheme (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.ts replaces drizzle-kit migrate for sqlite/postgres/mysql apps (drizzle-kit's sqlite migrate needs better-sqlite3
  • 🟠 bosia-brief-database skill + hook into bosia-brief-intake — captures DB engine + connection during brief intake, writes ## Database block to BRIEF.md
  • 🟠 todo feature — bosia feat todo scaffolds todo schema, repository, service, routes, components, and seed data
  • 🟡 todo component — bun x bosia@latest add todo installs todo-form, todo-item, todo-list components
  • 🟡 Registry as single source of truth — bosia create --template todo installs features from registry via template.json instead of duplicating files

Hooks & Middleware

  • 🟠 hooks.server.ts with Handle interface
  • 🟡 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 — Metadata type supports lang and link fields; dynamic <html lang>; <link> tag rendering in streaming SSR

v0.1.0

  • 🟡 Rename framework from bosbun to bosia
  • ⚪ Dead code cleanup (renderSSR, buildHtmlShell, unexported internals)
  • 🟡 splitCsvEnv helper 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.md for full context, attack scenarios, and proposed diffs.

  • Cookie secure defaults — default HttpOnly; Secure; SameSite=Lax on cookies.set() with opt-out
  • Auto-detect Cache-Control on /__bosia/data/ — private, no-cache when cookies accessed; public, max-age=0, must-revalidate otherwise
  • 🔴 load() fetch cookie scoping — makeFetch now forwards the Cookie header only to same-origin requests or origins in the INTERNAL_HOSTS allowlist; third-party hosts get no cookie. User-supplied init.headers.cookie is preserved
  • 🔴 Audit #1 — allowExternal redirect validation — still validate against javascript:, data:, vbscript: schemes even when allowExternal: true (move DANGEROUS_SCHEMES check above the early return in errors.ts:32)
  • 🟠 Audit #4 — Trusted proxy configuration — TRUST_PROXY env to control when X-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-in CSP_DIRECTIVES env emits matching Content-Security-Policy header
  • 🟠 Audit #2 — CORS preflight validation — validate Access-Control-Request-Method / Access-Control-Request-Headers against allowed config in handlePreflight (cors.ts:53-69)
  • 🟠 Audit #3 — CORS Vary: Origin on all responses when CORS is configured — prevent CDN caching bugs on non-matching origins (set at server.ts request level, not only in getCorsHeaders)
  • 🟡 Audit #5 — Validate prerender entries() return values — reject /, \, .. in dynamic segment values before URL substitution (prerender.ts:44-50)
  • 🟡 Escape lang attribute in HTML shell — <html lang="${lang}"> injects lang raw; if a metadata() derives lang from URL/user input it can break out of the attribute
  • ⚪ Validate CORS_MAX_AGE env — reject non-numeric values instead of producing NaN header

Security test coverage (from audit)

  • 🟡 Test: allowExternal: true still rejects javascript: / data: / vbscript: URLs
  • 🟡 Test: handlePreflight rejects when Access-Control-Request-Method is not in allowedMethods
  • 🟡 Test: Vary: Origin is 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_PROXY env gates X-Forwarded-* header trust in CSRF checks

Performance

  • 🟠 Parallelize client + server builds — run both Bun.build() calls with Promise.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.desiredSize to prevent memory buildup on slow/disconnected clients
  • 🟠 Streaming SSR error recovery — render proper error page instead of bare <p>Internal Server Error</p> when render() throws mid-stream
  • 🟠 renderPageWithFormData loader error handling — catch HttpError/Redirect thrown from loadRouteData() 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.exited after kill()), use random port instead of hardcoded 13572
  • 🟡 Fix buildAndRestart recursive tail call — replace recursion with while loop to prevent stack growth under rapid file changes

Client

  • 🟡 Bound prefetch cache size — prefetchCache grows 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.length on a UTF-8 string under-counts multi-byte content; switch to Buffer.byteLength or TextEncoder().encode(...).length before threshold check
  • 🟡 .env parser inline-comment stripping — KEY="value" # note currently keeps # note as 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-Proto to the inner app — without them the CSRF check derives the wrong expectedOrigin, 403'ing same-origin POST/form actions in dev. DX-only, prod unaffected.
  • 🟡 Stale env cleanup in dev — reset removed .env vars on hot-reload

v0.2.1 — Features & DX

New capabilities and developer experience improvements.

Data Loading

  • 🟠 depends() and invalidate() — selective data reloading
  • 🟡 Prefetch sends the loader cache mask — hover/viewport data-bosia-preload was warming the data endpoint with no mask, re-running every loader server-side; now it sends the same _invalidated bits 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
  • 🟠 beforeNavigate / afterNavigate lifecycle hooks — exported from bosia/client; fired by SPA router around pushState/popstate navs and on full-page unload (willUnload=true); cancel support via cancel() on programmatic navs
  • 🟠 Scroll restoration and snapshot support (export const snapshot) — scroll restoration shipped 0.8.6; snapshot shipped 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: ssr toggle (export const ssr = false)
  • 🟡 Page option: trailingSlash configuration

Forms

  • 🟠 use:enhance progressive enhancement — client-side fetch submission with automatic form state management (like SvelteKit)

Types

  • 🟠 Typed route params — generate { slug: string } from [slug] instead of Record<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 /_health instead of Bun.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 RegExp at 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.ts walks the dirs once at boot.
  • 🟡 Collapse SSR render() calls — root App.svelte + error pages render in separate Svelte render() 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/error in server.ts with a level-based logger that emits JSON in prod (pretty in dev) with a request ID.
  • ⚪ Tunable shutdown timers — server.ts:906 hardcodes the 2 s force-exit window and 10 s drain. Expose via SHUTDOWN_DRAIN_MS / SHUTDOWN_FORCE_MS for deploys with long-running streaming responses. Source: 2026-05-23 pre-prod audit
  • ⚪ Startup banner shows resolved hostname — server.ts:880-882 logs http://localhost:${PORT} even though Bun binds 0.0.0.0 by 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 feat per-file strategies — meta.json files: FileEntry[] with a strategy field: write (default), skip-if-exists, append-line, append-block, merge-json. Replaces the all-or-nothing replace prompt for shared files.
  • 🟡 Document meta.json schema and strategies in docs/ (CLI / bosia feat page)
  • 🟡 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 write strategy (force one to declare append-line/append-block)
  • 🟠 auth feature scaffold — uses append-block to register hooks in src/hooks.server.ts and routes barrel
  • 🟡 s3/storage feature → shipped as file-upload in v0.6.4: bun x bosia feat [-y] file-upload [-d sqlite|postgres|mysql] scaffolds Drizzle metadata + local/S3 adapter + /api/files POST (WebP @0.85, fit 1920×1080)
  • 🟡 Track installed features per project (bosia.json at root, committed) — enables bosia 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.json so scaffolded projects format-on-create. Pre-commit hook auto-formats staged files. No lint.

CLI

  • 🟠 bosia test command — wraps bun test with framework-aware defaults
  • 🟡 Auto-load .env.test (fallback .env) before running tests
  • 🟡 Set BOSIA_ENV=test automatically
  • 🟡 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, prerender path/URL helpers)

Test Utilities (`bosia/testing`)

  • 🟠 createRequestEvent() — mock factory for testing +server.ts handlers and hooks
  • 🟠 createLoadEvent() — mock factory for testing load() functions
  • 🟡 createMetadataEvent() — mock factory for testing metadata() functions
  • 🟠 mockCookies() — in-memory cookie jar implementing Cookies interface
  • 🟡 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-process BosiaApp from the route manifest
  • 🟠 testRequest() — send HTTP requests to the test app, get standard Response back
  • 🟠 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/svelte compatibility 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.ts loader — packages/bosia/src/core/config.ts; resolve from process.cwd(), compile via Bun.build({ target: "bun" }), cache, default to { plugins: [] }
  • 🔴 Public types in packages/bosia/src/lib/index.ts — BosiaPlugin, BosiaConfig, BuildContext, DevContext, RenderContext, defineConfig helper

Elysia Hooks

  • 🔴 backend.before / backend.after mount points in server.ts — before runs raw routes (e.g. /openapi.json) bypassing framework middleware; after receives RouteManifest for introspection

Build Hooks

  • 🟠 build.preBuild / build.postScan / build.postBuild in build.ts — call preBuild before loadEnv, postScan after scanRoutes(), postBuild after generateStaticSite()
  • 🟠 build.bunPlugins(target) merged into client + server Bun.build() plugin arrays

Render Hooks

  • 🟠 render.head fragments injected before </head> in buildMetadataChunk
  • 🟠 render.bodyEnd fragments injected before </body> in buildHtmlTail
  • 🟠 RenderContext (request, route, metadata) threaded from renderer.ts into html.ts builders

First-Party Plugin

  • 🟠 bosia/plugins/server-timing — exercises backend.before; adds Server-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/openapi first-party plugin
  • 🟠 build.postScan reads RouteManifest, emits dist/openapi.json
  • 🟠 Runtime mount via backend.before — GET /openapi.json, GET /docs (Scalar/Swagger UI)
  • 🟡 Optional schema export on +server.ts (TypeBox or Zod, decide later)
  • 🟡 Docs: OpenAPI usage page

v0.4.2 — OpenTelemetry Plugin

Tracing + metrics for production apps.

  • 🟠 bosia/plugins/opentelemetry first-party plugin
  • 🟠 OTLP exporter config via env vars (OTEL_EXPORTER_OTLP_ENDPOINT, etc.)
  • 🟠 Trace backend.before request → response, load() calls, render time
  • 🟡 Verify dev parity — telemetry must work in bosia 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/inspector first-party plugin (dev-only)
  • 🟠 Contributes Bun plugin via build.bunPlugins() — runs before SveltePlugin() and replaces its .svelte onLoad with an injecting variant
  • 🟠 Parses .svelte source with svelte/compiler parse(), walks RegularElement nodes, injects data-bosia-loc="<relpath>:<line>:<col>" via magic-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 captures data-bosia-loc
  • 🟠 POST /__bosia/locate endpoint (mounted via backend.before) — receives { file, line, col }, opens editor (or POSTs to aiEndpoint with comment)
  • 🟡 Editor integration — code -g file:line (configurable via inspector({ 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 check out of the box.

  • 🟠 Ship .gitignore with bun x bosia create — npm pack strips .gitignore, so templates store it as _gitignore and copyDir restores the dotfile name on copy
  • 🟡 Ignore generated Tailwind output public/bosia-tw.css in template .prettierignore and .gitignore (default, demo, todo) so bun run check succeeds on a clean scaffold
  • 🟡 bun run check:templates — packs via bun pm pack, extracts the tarball, and asserts each templates/* 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.ts so freshly scaffolded projects get Alt+click-to-source out of the box.

  • 🟡 Add bosia.config.ts to templates/{default,demo,todo}/ enabling inspector({ editor: "code" }). copyDir copies it as-is; no substitutions needed. Production-safe (plugin self-disables under NODE_ENV=production).
  • ⚪ Note preconfigured state in docs/content/docs/guides/inspector.md so 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/; standalone bun run build keeps writing to ./dist/.

  • 🟠 Decouple URL namespace (/dist/client/...) from on-disk location via OUT_DIR in paths.ts (reads BOSIA_OUT_DIR, default ./dist)
  • 🟠 dev.ts hardcodes .bosia/dev and passes BOSIA_OUT_DIR to spawned build + app-server children; never reads the env itself
  • 🟠 build.ts, prerender.ts, html.ts, server.ts, cli/start.ts all read from OUT_DIR instead of hardcoded ./dist literals
  • 🟡 Verification path: BOSIA_OUT_DIR=.bosia/verify bun run build produces full artifacts without touching ./dist. Catches what tsc --noEmit + svelte-check miss (route scan, prerender child, server-entry compile). Verified at apps/demo.

v0.5.6 — Build/dev `.bosia/` cleanup collision ✅ (shipped 2026-05-18)

Follow-up to v0.5.5. OUT_DIR was split, but build.ts still blanket-wiped ./.bosia at startup — clobbering a concurrently-running bosia dev whose compiled server lives at .bosia/dev/. Cleanup is now scoped.

  • 🔴 build.ts cleanup is scoped to OUT_DIR plus 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.svelte receive params as a sibling prop of data, not nested under data.params. Network protocol (data endpoint payload, SSR injection) is unchanged — params is stripped at the component boundary.

  • 🟠 App.svelte passes params as a separate prop on pages and layouts; SSR branch strips merged params off pageData via local helper
  • 🟠 hydrate.ts seeds appState.pageData without the merged params key (still seeds appState.routeParams from same payload)
  • 🟠 routeTypes.ts codegen: PageData / LayoutData no longer intersect { params: Params }; PageProps / LayoutProps declare params: Params as a sibling of data
  • 🟡 Update demo + template blog/[slug]/+page.svelte and docs (README.md, docs/content/docs/guides/routing.md) to consume params as a top-level prop
  • 🟡 Standardize default and todo starter templates on the (public)/ route group convention used by demo, 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 → aiEndpoint handoff. Live-only (no buffer/replay), dev-only (prod unaffected).

  • 🟠 Server capture: Elysia .onError() + uncaughtException/unhandledRejection listeners installed lazily inside backend.before(). uncaughtException rethrows so crash-recovery still triggers. 500ms dedup prevents render-loop floods.
  • 🟠 SSE broadcaster at /__bosia/errors — module-scoped controller Set, event: bosia-error data frames, 25s :ping keepalive, abort-driven cleanup. No replay buffer (live-only contract)
  • 🟠 Reorder the Elysia onError chain in server.ts: the base 500 responder now registers AFTER the plugin.backend.before loop so plugin handlers fire first.
  • 🟠 Client capture in overlay.ts: window.error + unhandledrejection listeners + 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.ts emits sourcemap: "linked" in dev ("none" in prod). New inspector/sourcemap.ts lazy-resolves compiled frames → source via @jridgewell/trace-mapping, only for the clicked error.
  • 🟡 Last-interaction context: track the most recent data-bosia-loc the user clicked/keyed and append Last user interaction: <file>:<line>:<col> to the payload.
  • 🟡 errorsEnabled?: boolean (default true) config flag on InspectorOptions — 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 with RangeError: Maximum call stack size exceeded on first render. Svelte's dev output wraps the binding in a named function get(); Bun rewrites $.get to a named import get

  • 🔴 Post-process Svelte compile output in inspector/bun-plugin.ts and svelteCompiler.ts to rename the inner get/set to $$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 a loader: "css" virtual module.

v0.5.9 — `src/app.html` template ✅ (shipped 2026-05-20)

SvelteKit-style document shell. Users create src/app.html with %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.ts builders (buildHtml, buildHtmlShellOpen, buildMetadataChunk, buildHtmlTail) to accept optional segments and slot user chrome
  • 🟠 Update renderer.ts to 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.html in templates (default, todo) and demo with %bosia.lang% and data-theme attributes
  • 🟡 Favicon detection: if user's headOpen contains rel="icon", skip framework default favicon injection
  • 🟡 Unit tests: template loading, validation, parsing, caching, interpolation, segment structure
  • 🟡 New skill bosia-app-css documenting src/app.css order + 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, reuses mergeFontImports()). Prepends @import url(...) to src/app.css with 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 for window.location.href because goto() wasn't exported (full reload, lost state)

  • 🟠 goto(url, opts?) exported from bosia/client. Returns a Promise resolving after the nav settles. Honors replaceState, 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 via onDestroy.
  • 🟠 Router exposes navigation type ("link"|"goto"|"popstate"|"form"|"enter") and the Navigation object threading into both lifecycle phases.
  • 🟠 router.navigate(path, { replace, source }) supports history.replaceState (used by goto({ replaceState: true })) and threads the source through to the Navigation object
  • 🟡 beforeunload fires beforeNavigate with willUnload: true so listeners can observe (cancellation requires native beforeunload event — out of scope)
  • 🟡 Hydration safety net — wrapped main() in core/client/hydrate.ts in 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 empty body — non-streaming SSR skips it; streaming SSR and ssr=false still get it for the TTFB gap.
  • 🟡 Demo route apps/demo/src/routes/(public)/nav-test/+page.svelte exercises all four patterns plus the cancel/event-log flow
  • 🟡 New docs page docs/content/docs/guides/navigation.md covers the four patterns and the lifecycle hooks; added to the Guides sidebar in docs/src/lib/docs/nav.ts
  • 🟡 New bosia-navigation skill so AI agents pick the right navigation pattern and use the lifecycle hooks correctly. Catalog index bumped 34 → 35; cross-references added in bosia-routing and bosia-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, .env reload failures, port conflicts.

  • 🟠 core/dev.ts captures 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-error over /__bosia/sse (same wire shape as inspector's ServerError). The SSE handler flushes recent buffered errors to new clients so pre-connect errors stay visible.
  • 🟠 New core/dev-error-page.ts renders the fallback HTML the dev proxy returns when fetch(app) throws on an HTML nav. Embeds the overlay, pre-seeds buffered errors, subscribes to /__bosia/sse reload to swap itself out.
  • 🟡 .env reload 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 between beforeNavigate and 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.href stall 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(), layout load(), page load(), render(), and Bun.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) with Accept-Encoding: br|gzip|identity negotiation, 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.ts GET handlers cached with the same key rules. v0.6 invalidates API entries by URL/prefix only (no depends() for API yet).
  • 🟠 Public API — invalidate(key) / invalidateAll(prefix) from bosia mirror the existing browser-side invalidate() 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.ts exports a CacheOption type alias for IDE support.
  • 🟡 Env vars — CACHE_KEYS (default session,sid,auth,token,jwt,Authorization) controls identity-hash inputs; CACHE_MAX_ENTRIES (default 500, 0 disables). Documented in guides/environment-variables and the response-cache guide (EN+ID).
  • 🟡 Author guidance — new bosia-response-cache skill walks agents through when to call invalidate() from server code, how to tag loaders with depends(), and when to opt a route out.
  • 🟠 Dev proxy now forces the inner app to Accept-Encoding: identity. Previously it forwarded gzip,br, the inner returned compressed bytes, Bun's fetch() auto-decoded but left Content-Encoding: gzip
  • 🟠 core/cache.ts guards process.env reads — re-exported through the public bosia barrel, it evaluated in the browser bundle and threw ReferenceError: process on hydration in Safari.
  • 🟠 Server-only response-cache exports moved to bosia/server — core/cache.ts still evaluated client-side via the shared barrel. Added ./server to exports, created lib/server.ts, removed them from the shared barrel.
  • 🟡 Inspector dev-error reporter type alignment — devErrorReport.ts declared source?: "server"|... but pushServerError accepted "elysia"|..., failing bun run check (TS2322)

Deferred to v0.7+

  • 🟡 Key-based invalidation for +server.ts endpoints — give API handlers a depends() argument or support export const tags = [...] so invalidate("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 = false cascade — done in 1.2.9 (routeCacheFlag reads 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>, but index.ts exports Card/CardContent, not Root. bosia build succeeded 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 — switched compile() to modernAst: true, wired the audit into onLoad, added module-scoped per-file dedupe (Map<absPath, Promise>) so it runs once across the parallel browser+bun targets.
  • 🟠 Promotes select svelte/compiler warnings 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 from plugin.ts and inspector/bun-plugin.ts so the audit and the resolver share one alias/tsconfig-paths/relative-path implementation and one lineColFromOffset helper.
  • 🟡 BosiaConfig.strictImports (boolean | { unbound, namespaceMember, warnings }) — per-component opt-out. BOSIA_STRICT_IMPORTS=0 env var downgrades to a console.warn at 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 via UploadArea

  • 🟡 demos/FilesUploadCropDemo.svelte — single combined demo. UploadArea (enableCrop) → on crop, captures the (file, done) pair, opens CropImage against URL.createObjectURL(file), wraps the Blob as a File, calls done(file)
  • 🟡 docs/src/routes/api/demo-upload/+server.ts — tiny POST returning { url, ok } so the demo Upload button doesn't 500.
  • ⚪ Both blocks/files/{crop-image,upload-area}.md frontmatter demo: now points at FilesUploadCropDemo. [...slug]/+page.svelte imports only the new demo; deleted FilesCropImageDemo.svelte and FilesUploadAreaDemo.svelte.
  • 🟡 blocks/files/crop-image/block.svelte — switched the 400px viewport from h-[400px] to style="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 into bosia-tw.css from the docs build alongside registry/components/ui.
  • 🟡 docs/src/lib/docs/content.ts — contentDir/demoFile no longer resolve relative to import.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-area block shipped since v0.6.3 but bosia had no server-side counterpart.

  • 🟠 registry/features/file-upload/ — full backend. file.service.ts validates MIME, decodes via Bun.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 to resolveFeatureOptions(), parsed against the feature's own meta.json options schema. 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 to runFeat. Help text updated so feature-specific flags follow the feature name.
  • ⚪ packages/bosia/src/cli/registry.ts — InstallOptions gained featureOptions (resolved values) and featureArgs (raw tokens for the root feature). No CLI-level dialect type — dialect is now file-upload-specific.
  • ⚪ registry/index.json — features array gains file-upload.
  • 🟡 docs/content/docs/guides/file-upload.md — install / env / wiring / S3 swap docs; cross-link added from blocks/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>.json saw the SKILL.md body but not the companion reference files that carry the actionable detail, so they guessed paths or scraped the site.

  • 🟡 listSkillReferences(name) in docs/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 gained references: 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, realpath traversal guard mirrors the [name] route. Returns { name, file, path, content } with max-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 wrapping svelte-easy-crop
  • 🟡 blocks/files/upload-area/ — drag-drop + click-to-pick with preview, size validation, XMLHttpRequest progress, Progress bar. Props: uploadUrl (required), accept, maxSizeMB, fieldName, extraFields, headers, enableCrop
  • ⚪ registry/components/ui/icon/icons.ts — added crop and zoom-in paths (lucide-static).
  • ⚪ registry/index.json — blocks array gains files/crop-image and files/upload-area.
  • ⚪ Docs pages docs/content/docs/blocks/files/crop-image.md and upload-area.md; Files group added to docs/src/lib/docs/nav.ts; FilesCropImageDemo and FilesUploadAreaDemo registered in [...slug]/+page.svelte.
  • 🟡 core/build.ts — added conditions: ["svelte"] to both Bun.build calls so Svelte libs (like svelte-easy-crop) resolve to their svelte export. An earlier generic onResolve handler 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.ts loaders and skipped a service/repository layer.

  • 🟡 New bosia-clean-architecture skill — eight rules (R1–R8): no db in 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 using warung-nasi), shared-folder.md (what belongs in features/shared/ and what doesn't).
  • 🟡 bosia-drizzle-feature updated — 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-usage updated — Quick Start rewritten so loaders call CatalogService.summary() not db.select(...); workflow writes the repository first, then service; new red flags for db in routes.
  • 🟡 Catalog docs/content/skills/SKILL.md bumped 38 → 39 skills; bosia-clean-architecture added 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 shared Button.svelte showed only Button.svelte:5:1

  • 🟠 Compile-time injection of <!--bosia:o=...--> / <!--bosia:c--> markers around Component/SvelteComponent/SvelteSelf nodes in injectLocs
  • 🟠 Runtime collectStack(el) walks DOM ancestors + previous siblings with a depth counter matching each bosia:c to its bosia: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.md updated to describe the chain feature and extend the prod-output grep to check for both markers.
  • 🟡 bosia-inspector-edit skill updated for the new payload — parses the Component 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) surfaced 403 Cross-origin request blocked and the AI kept reaching for CORS env vars.

  • 🟡 New bosia-env skill — four-tier prefix (PUBLIC_STATIC_/PUBLIC_/STATIC_/none), $env virtual module for user vars, process.env for framework-reserved vars.
  • 🟡 New bosia-cors skill — CORS env recipe (CORS_ALLOWED_ORIGINS + methods/headers/credentials/max-age), the Vary: Origin invariant, and a triage table distinguishing a real CORS failure from Bosia's CSRF rejection.
  • 🟡 Catalog SKILL.md updated 35 → 37 skills; both entries added under framework conventions and into discovery-order step 2; cross-references wired both ways and to bosia-security-review/bosia-elysia-routes.

v0.5.11 — `$types` resolution inside `.svelte` files

tsc --noEmit resolves ./$types from .svelte via the rootDirs trick, so check/build type-check correctly. But svelte-language-server doesn't honor rootDirs in its virtual TS document.

Acceptance: in a freshly scaffolded app, hovering PageProps in +page.svelte shows the generated type, autocomplete on params. 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 moduleResolution for $types from .svelte files; (b) fork/extend svelte-language-server config.
  • 🟠 Ship the plugin/shim from packages/bosia and wire it into the scaffolding templates' tsconfig.json (compilerOptions.plugins or svelte.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 shows Params = { slug: string }, autocomplete on params. lists slug, typing params.foo red-squiggles.
  • 🟡 Document the editor setup step in docs/content/docs/guides/routing.md (or a new "Editor setup" guide) — what extension to install, what tsconfig.json looks 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.md at 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, writes BRIEF.md, chains bosia-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. Runs bosia_add_theme + --primary/--accent override.
  • 🟡 bosia-brief-platform — form factors, primary surface, ID format regex, number/date Intl formatters, 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.md index updated — 25 → 31, new section "Brief intake — design ✦", discovery order gains step 0 "check BRIEF.md".

Hotfix (same-day, 2026-05-17)

  • 🔴 Fix bosia dev build crash Multiple files share the same output path on apps with multiple style-less +page.svelte routes.

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-design wired into bosia-brief-intake as step 4, so every BRIEF.md ends with a populated ## Aesthetic section. Quick-start opener bumped 5 → 6 questions.
  • 🟡 Stance consumption wired downstream — bosia-design-review gains a P1 check that each emit honors § Aesthetic without re-picking. Six page scaffolds (landing, saas-landing, blog, pricing, mobile-screen.
  • 🟡 bosia-brief-intake ships two reference files: references/quick-start-script.md (6-question opener with palette → direction inference) and references/example-brief.md (Dombaku-style filled BRIEF.md). Frontmatter targets.files updated.

v0.5.3 — API prerender ✅ (shipped 2026-05-16)

Same prerender ergonomics for +server.ts routes as pages already had. Drop the docs-only static-API post-build pipeline.

  • 🟠 Framework: +server.ts honors export const prerender = true — detectPrerenderRoutes scans manifest.apis, dynamic routes call entries(), prerenderApiOutPath() writes one .json per route. Fetched body written verbatim.
  • 🟡 Dev runtime alias: API routes with prerender = true are 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 prerenderApiOutPath and substituteParams rest-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() from docs/scripts/post-build.ts — post-build returns to sitemap-only

Hotfix (same-day, 2026-05-16)

  • 🔴 Fix dev .json alias resolution: catch-all sibling routes were absorbing the .json suffix into their rest-segment param, causing 4xx in dev.
  • 🔴 Fix /api/skills/<name> JSON shape: was emitting raw SKILL.md markdown into a .json file. Handler now returns Response.json({ name, content }) with frontmatter stripped via gray-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, and module() throw → fallback
  • 🟡 New docs/test/api-prerender.test.ts — post-build sanity over dist/static/api/**/*.json: every artifact parses; list endpoints expose their array.
  • 🟡 Renamed registry detail field mdFile → content in /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 (createHighlighter not a function). Lazy await import("shiki") made Bun's splitter call into the parent before its exports initialized.
  • 🟡 Normalize path on /api/skills, /api/components, /api/blocks index + 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 add accepts multiple component names in one call; new -y/--yes flag auto-confirms overwrite prompt for CI use
  • 🟡 Static /api/components.json + /api/components/{path}.json and /api/blocks.json + /api/blocks/{path}.json emitted by docs/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 in core/plugin.ts. Without it, every dynamic-imported route chunk reaching app.css produces 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 onBeforeHandle apiRoutes scan; non-GET catch-alls already cover every method
  • 🟠 Inline Svelte compile, drop bun-plugin-svelte — own .svelte/.svelte.[tj]s onLoad with css: "injected" (browser) / css: "external" (server)

Open

  • 🟠 Truly progressive SSR streaming — renderSSRStream is 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 safeJsonStringify cost on large loader payloads — done in v0.5.0 by moving page/layout/form data to <script type="application/json"> islands read via JSON.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 to src/lib/blocks/<path>/
  • 🟠 bun x bosia@latest add theme <name> — install a theme to src/lib/themes/<name>.css, patch app.css import
  • 🟡 Extend CLI dispatcher (packages/bosia/src/cli/index.ts) for add block/add theme sub-args
  • 🟡 Refactor add.ts — parameterize destination root; RegistryIndex gains blocks: string[], themes: string[]
  • 🟡 block.ts handler — recursive primitive deps via addComponent(), optional font @import merge into app.css
  • 🟡 theme.ts handler — copy tokens.css, swap @import in app.css (one-active-theme), font @import merge

Registry content

  • 🟠 Extend registry/index.json with blocks and themes arrays
  • 🟠 registry/themes/neutral/ — extracted from current apps/demo/src/app.css @theme block
  • 🟠 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.css to @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.svelte registered in nav.ts and [...slug]/+page.svelte demos map

v0.5.0 — Full Plugin Lifecycle

Complete the plugin surface; uninstall + virtual modules.

  • 🟠 dev.onStart + dev.onFileChange wired in dev.ts
  • 🟠 client.onHydrate + client.onNavigate in core/client/hydrate.ts + router.svelte.ts
  • 🟠 Virtual modules from plugins — extend core/plugin.ts resolver 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 create scaffolding
  • 🟡 Test file examples in project templates
  • 🟡 bosia feat test scaffolder 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 via css: "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: true emits one CSS sidecar per chunk that imports a shared CSS file → collision when N routes transitively import app.css
  • Current fix (plugin.ts intercepts app.css → empty JS module) ships ALL utilities in one public/bosia-tw.css regardless 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.css no-op interception in core/plugin.ts once dedup lands
  • 🟡 Component <style> blocks: continue with css: "injected" (already scoped + deduped via cssHash)
  • 🟡 Tailwind into bundler hot path — port @tailwindcss/vite shape to Bun plugin API so utilities are scanned + emitted as part of the build, not a parallel CLI step

Approach Options

  1. Wait on Bun upstream — file/track issue for CSS chunk dedup under splitting: true. Lowest effort, unbounded timeline.
  2. Custom Bun plugin — own CSS pipeline in core/cssPipeline.ts: intercept all .css imports, hash contents, emit one shared chunk per unique source, track route → chunk mapping, inject <link> tags via render.head per request.
  3. Static layout import workaround — make root +layout.svelte a static import (not dynamic) in routes.client.ts. Collapses app.css into entry chunk → no per-route duplication. Cheapest fix, but loses dynamic layout chains.

Acceptance

  • Builds with 100+ routes succeed without the app.css no-op
  • Each route ships ≤ what it imports (verified by inspecting dist/client/*.css sizes)
  • Component <style> still scoped via cssHash
  • 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.ts universal 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)