bosia 0.7.9 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/core/client/App.svelte +55 -2
- package/src/core/routeFile.ts +12 -0
- package/src/core/scanner.ts +6 -0
- package/src/core/server.ts +1 -0
- package/src/core/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing
|
|
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)
|
|
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
|
|
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] ?? {}}
|
package/src/core/routeFile.ts
CHANGED
|
@@ -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");
|
package/src/core/scanner.ts
CHANGED
|
@@ -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,
|
package/src/core/server.ts
CHANGED
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
|
/**
|