bosia 0.7.9 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.7.9",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
- "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
5
+ "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
7
7
  "bun",
8
8
  "svelte",
@@ -53,6 +53,12 @@
53
53
  const errorDepth = $derived(ssrMode ? ssrErrorDepth : appState.errorDepth);
54
54
  let navigating = $state(false);
55
55
  let navDone = $state(false);
56
+ // +loading.svelte skeleton shown during navigation to a route that has one.
57
+ let LoadingComponent = $state<any>(null);
58
+ let loadingDepth = $state(0); // # of shared layouts to keep mounted under the loader
59
+ let showLoading = $state(false);
60
+ let currentLayoutPaths: string[] = [];
61
+ let lastSettledPath = "";
56
62
  // Skip bar on the very first effect run (initial hydration — data already present)
57
63
  let firstNav = true;
58
64
  let navDoneTimer: ReturnType<typeof setTimeout> | null = null;
@@ -73,7 +79,11 @@
73
79
 
74
80
  const isFirst = firstNav;
75
81
  firstNav = false;
76
- if (isFirst) return; // Initial hydration — data already in SSR props, no fetch needed
82
+ if (isFirst) {
83
+ currentLayoutPaths = (match.route as any).layoutPaths ?? [];
84
+ lastSettledPath = pathname;
85
+ return; // Initial hydration — data already in SSR props, no fetch needed
86
+ }
77
87
 
78
88
  appState.form = null;
79
89
  if (navDoneTimer) {
@@ -83,6 +93,28 @@
83
93
  navDone = false;
84
94
  navigating = true;
85
95
 
96
+ // ─── +loading.svelte skeleton ─────────────────────────────────────
97
+ // On a real path change, show the destination's loading skeleton nested
98
+ // inside the layouts shared with the current page. The destination's own
99
+ // added layouts aren't mounted yet, so the skeleton draws that chrome.
100
+ const destLayoutPaths = ((match.route as any).layoutPaths as string[]) ?? [];
101
+ const loadingImport = (match.route as any).loading as (() => Promise<any>) | null;
102
+ if (loadingImport && pathname !== lastSettledPath) {
103
+ let s = 0;
104
+ while (
105
+ s < currentLayoutPaths.length &&
106
+ s < destLayoutPaths.length &&
107
+ currentLayoutPaths[s] === destLayoutPaths[s]
108
+ )
109
+ s++;
110
+ loadingImport().then((m) => {
111
+ if (cancelled) return; // superseded nav
112
+ loadingDepth = s;
113
+ LoadingComponent = m.default;
114
+ showLoading = true;
115
+ });
116
+ }
117
+
86
118
  // ─── Loader cache: decide which loaders need to re-run ─────────────
87
119
  // For each layout depth + the page, compare the cached entry (if any)
88
120
  // against the live URL/params and the dirty set. If everything is
@@ -163,6 +195,13 @@
163
195
  if (cancelled) return;
164
196
  navigating = false;
165
197
  navDone = true;
198
+ // Tear down the loading skeleton and record identity for the next nav.
199
+ // Superseded navs have cancelled === true and never reach here, so they
200
+ // can't clobber a newer nav's loader.
201
+ showLoading = false;
202
+ LoadingComponent = null;
203
+ currentLayoutPaths = destLayoutPaths;
204
+ lastSettledPath = pathname;
166
205
  navDoneTimer = setTimeout(() => {
167
206
  navDone = false;
168
207
  }, 400);
@@ -348,7 +387,9 @@
348
387
  <div class="bosia-bar done"></div>
349
388
  {/if}
350
389
 
351
- {#if ErrorComponent}
390
+ {#if showLoading && LoadingComponent}
391
+ {@render renderLoading(0)}
392
+ {:else if ErrorComponent}
352
393
  {@const depth = errorDepth ?? 0}
353
394
  {#if depth > 0 && layoutComponents.length > 0}
354
395
  {@render renderLayout(0, depth)}
@@ -363,6 +404,18 @@
363
404
  <p>Loading...</p>
364
405
  {/if}
365
406
 
407
+ {#snippet renderLoading(index: number)}
408
+ {#if index < loadingDepth}
409
+ {@const Layout = layoutComponents[index]}
410
+ {@const data = layoutData[index] ?? {}}
411
+ <Layout {data} {params}>
412
+ {@render renderLoading(index + 1)}
413
+ </Layout>
414
+ {:else}
415
+ <LoadingComponent />
416
+ {/if}
417
+ {/snippet}
418
+
366
419
  {#snippet renderLayout(index: number, leafDepth: number)}
367
420
  {@const Layout = layoutComponents[index]}
368
421
  {@const data = layoutData[index] ?? {}}
@@ -97,6 +97,11 @@ export function consumePrefetch(path: string): any | null {
97
97
 
98
98
  /** Prefetches data for a path and stores in cache. No-op if already cached/in-flight. */
99
99
  export async function prefetchPath(path: string): Promise<void> {
100
+ // Warm the route's +loading.svelte chunk alongside its data, so the skeleton
101
+ // paints instantly on click instead of cold-importing after the old page lingers.
102
+ const warmMatch = findMatch(clientRoutes, new URL(path, window.location.origin).pathname);
103
+ (warmMatch?.route as { loading?: (() => Promise<unknown>) | null } | undefined)?.loading?.();
104
+
100
105
  const existing = prefetchCache.get(path);
101
106
  if (existing && Date.now() - existing.ts <= 30_000) return;
102
107
  if (existing) prefetchCache.delete(path);
@@ -45,6 +45,8 @@ export function generateRoutesFile(manifest: RouteManifest): void {
45
45
  // (re-run). One id per layout in order, plus an optional page id.
46
46
  lines.push(" pageId: string | null;");
47
47
  lines.push(" layoutIds: (string | null)[];");
48
+ lines.push(" loading: (() => Promise<any>) | null;");
49
+ lines.push(" layoutPaths: string[];");
48
50
  lines.push("}> = [");
49
51
  for (const r of pages) {
50
52
  const layoutImports = r.layouts
@@ -75,6 +77,10 @@ export function generateRoutesFile(manifest: RouteManifest): void {
75
77
  lines.push(` trailingSlash: ${JSON.stringify(r.trailingSlash)},`);
76
78
  lines.push(` pageId: ${pageId},`);
77
79
  lines.push(` layoutIds: [${layoutIds}],`);
80
+ lines.push(
81
+ ` loading: ${r.loading ? `() => import(${JSON.stringify(toImportPath(r.loading))})` : "null"},`,
82
+ );
83
+ lines.push(` layoutPaths: ${JSON.stringify(r.layouts)},`);
78
84
  lines.push(" },");
79
85
  }
80
86
  lines.push("];\n");
@@ -175,6 +181,8 @@ function generateClientRoutesFile(
175
181
  lines.push(' trailingSlash: "never" | "always" | "ignore";');
176
182
  lines.push(" pageId: string | null;");
177
183
  lines.push(" layoutIds: (string | null)[];");
184
+ lines.push(" loading: (() => Promise<any>) | null;");
185
+ lines.push(" layoutPaths: string[];");
178
186
  lines.push("}> = [");
179
187
  for (const r of pages) {
180
188
  const layoutImports = r.layouts
@@ -202,6 +210,10 @@ function generateClientRoutesFile(
202
210
  lines.push(` trailingSlash: ${JSON.stringify(r.trailingSlash)},`);
203
211
  lines.push(` pageId: ${pageId},`);
204
212
  lines.push(` layoutIds: [${layoutIds}],`);
213
+ lines.push(
214
+ ` loading: ${r.loading ? `() => import(${JSON.stringify(toImportPath(r.loading))})` : "null"},`,
215
+ );
216
+ lines.push(` layoutPaths: ${JSON.stringify(r.layouts)},`);
205
217
  lines.push(" },");
206
218
  }
207
219
  lines.push("];\n");
@@ -8,6 +8,7 @@ import type { PageRoute, ApiRoute, RouteManifest, TrailingSlash } from "./types.
8
8
  // Conventions (SvelteKit-compatible):
9
9
  // +page.svelte — page component
10
10
  // +page.server.ts — server loader for the page
11
+ // +loading.svelte — client skeleton shown during navigation
11
12
  // +layout.svelte — layout component (wraps all children)
12
13
  // +layout.server.ts — server loader for the layout
13
14
  // +server.ts — API route (GET, POST, etc.)
@@ -93,6 +94,10 @@ export function scanRoutes(): RouteManifest {
93
94
  ? join(dir, "+page.server.ts")
94
95
  : null;
95
96
 
97
+ const loadingFile = items.some((i) => i.isFile() && i.name === "+loading.svelte")
98
+ ? join(dir, "+loading.svelte")
99
+ : null;
100
+
96
101
  const pageTs = pageServerFile ? readTrailingSlash(join(ROUTES_DIR, pageServerFile)) : null;
97
102
  const effectiveTs: TrailingSlash = pageTs ?? currentTrailingSlash;
98
103
 
@@ -101,6 +106,7 @@ export function scanRoutes(): RouteManifest {
101
106
  page: join(dir, "+page.svelte"),
102
107
  layouts: [...currentLayouts],
103
108
  pageServer: pageServerFile,
109
+ loading: loadingFile,
104
110
  layoutServers: [...currentLayoutServers],
105
111
  errorPages: [...currentErrorPages],
106
112
  trailingSlash: effectiveTs,
@@ -1003,6 +1003,7 @@ function loadBuiltManifest(): RouteManifest {
1003
1003
  page: "",
1004
1004
  layouts: [],
1005
1005
  pageServer: r.pageServer ? "" : null,
1006
+ loading: null,
1006
1007
  layoutServers: [],
1007
1008
  errorPages: [],
1008
1009
  trailingSlash: r.trailingSlash,
package/src/core/types.ts CHANGED
@@ -13,6 +13,8 @@ export interface PageRoute {
13
13
  layouts: string[];
14
14
  /** Path to +page.server.ts if exists, relative to src/routes/ */
15
15
  pageServer: string | null;
16
+ /** Path to +loading.svelte if exists, relative to src/routes/ (client skeleton). */
17
+ loading: string | null;
16
18
  /** Chain of +layout.server.ts files root → leaf, with their layout depth */
17
19
  layoutServers: { path: string; depth: number }[];
18
20
  /**