bosia 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -39
- package/package.json +56 -54
- package/src/ambient.d.ts +31 -0
- package/src/cli/add.ts +120 -114
- package/src/cli/build.ts +10 -10
- package/src/cli/create.ts +142 -137
- package/src/cli/dev.ts +7 -9
- package/src/cli/feat.ts +266 -258
- package/src/cli/index.ts +51 -42
- package/src/cli/registry.ts +136 -115
- package/src/cli/start.ts +17 -17
- package/src/cli/test.ts +25 -0
- package/src/core/build.ts +72 -56
- package/src/core/client/App.svelte +177 -156
- package/src/core/client/appState.svelte.ts +33 -31
- package/src/core/client/enhance.ts +83 -78
- package/src/core/client/hydrate.ts +95 -81
- package/src/core/client/prefetch.ts +101 -94
- package/src/core/client/router.svelte.ts +64 -51
- package/src/core/cookies.ts +70 -66
- package/src/core/cors.ts +44 -35
- package/src/core/csrf.ts +38 -38
- package/src/core/dedup.ts +17 -17
- package/src/core/dev.ts +196 -168
- package/src/core/env.ts +160 -148
- package/src/core/envCodegen.ts +73 -73
- package/src/core/errors.ts +48 -49
- package/src/core/hooks.ts +50 -50
- package/src/core/html.ts +184 -145
- package/src/core/matcher.ts +130 -121
- package/src/core/paths.ts +8 -10
- package/src/core/plugin.ts +113 -107
- package/src/core/prerender.ts +191 -122
- package/src/core/renderer.ts +359 -286
- package/src/core/routeFile.ts +140 -127
- package/src/core/routeTypes.ts +144 -83
- package/src/core/scanner.ts +125 -95
- package/src/core/server.ts +538 -424
- package/src/core/types.ts +25 -20
- package/src/lib/index.ts +8 -8
- package/src/lib/utils.ts +44 -30
- package/templates/default/.prettierignore +5 -0
- package/templates/default/.prettierrc.json +9 -0
- package/templates/default/README.md +5 -5
- package/templates/default/package.json +22 -18
- package/templates/default/src/app.css +80 -80
- package/templates/default/src/app.d.ts +3 -3
- package/templates/default/src/routes/+error.svelte +7 -10
- package/templates/default/src/routes/+layout.svelte +2 -2
- package/templates/default/src/routes/+page.svelte +30 -32
- package/templates/default/src/routes/about/+page.svelte +3 -3
- package/templates/default/tsconfig.json +20 -20
- package/templates/demo/.prettierignore +5 -0
- package/templates/demo/.prettierrc.json +9 -0
- package/templates/demo/README.md +9 -9
- package/templates/demo/package.json +22 -17
- package/templates/demo/src/app.css +80 -80
- package/templates/demo/src/app.d.ts +3 -3
- package/templates/demo/src/hooks.server.ts +9 -9
- package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
- package/templates/demo/src/routes/(public)/+page.svelte +96 -67
- package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
- package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
- package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
- package/templates/demo/src/routes/+error.svelte +10 -7
- package/templates/demo/src/routes/+layout.server.ts +4 -4
- package/templates/demo/src/routes/+layout.svelte +2 -2
- package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
- package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
- package/templates/demo/src/routes/api/hello/+server.ts +25 -25
- package/templates/demo/tsconfig.json +20 -20
- package/templates/todo/.prettierignore +5 -0
- package/templates/todo/.prettierrc.json +9 -0
- package/templates/todo/README.md +9 -9
- package/templates/todo/package.json +22 -17
- package/templates/todo/src/app.css +80 -80
- package/templates/todo/src/app.d.ts +7 -7
- package/templates/todo/src/hooks.server.ts +9 -9
- package/templates/todo/src/routes/+error.svelte +10 -7
- package/templates/todo/src/routes/+layout.server.ts +4 -4
- package/templates/todo/src/routes/+layout.svelte +2 -2
- package/templates/todo/src/routes/+page.svelte +44 -44
- package/templates/todo/template.json +1 -1
- package/templates/todo/tsconfig.json +20 -20
package/src/core/build.ts
CHANGED
|
@@ -27,20 +27,24 @@ const envVars = loadEnv(envMode);
|
|
|
27
27
|
const classifiedEnv = classifyEnvVars(envVars);
|
|
28
28
|
|
|
29
29
|
// 0b. Clean all generated output first
|
|
30
|
-
try {
|
|
31
|
-
|
|
30
|
+
try {
|
|
31
|
+
rmSync("./dist", { recursive: true, force: true });
|
|
32
|
+
} catch {}
|
|
33
|
+
try {
|
|
34
|
+
rmSync("./.bosia", { recursive: true, force: true });
|
|
35
|
+
} catch {}
|
|
32
36
|
|
|
33
37
|
// 1. Scan routes
|
|
34
38
|
const manifest = scanRoutes();
|
|
35
39
|
console.log(`📂 Found ${manifest.pages.length} page route(s):`);
|
|
36
40
|
for (const r of manifest.pages) {
|
|
37
|
-
|
|
41
|
+
console.log(` ${r.pattern} → ${r.page}${r.pageServer ? " (server)" : ""}`);
|
|
38
42
|
}
|
|
39
43
|
if (manifest.apis.length > 0) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
console.log(`📡 Found ${manifest.apis.length} API route(s):`);
|
|
45
|
+
for (const r of manifest.apis) {
|
|
46
|
+
console.log(` ${r.pattern} → ${r.server}`);
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// 2. Generate .bosia/routes.ts (single file replaces all old code generators)
|
|
@@ -58,12 +62,19 @@ generateEnvModules(classifiedEnv);
|
|
|
58
62
|
// 3. Start Tailwind CSS (async — runs concurrently with client+server builds)
|
|
59
63
|
const tailwindBin = resolveBosiaBin("tailwindcss");
|
|
60
64
|
const tailwindProc = Bun.spawn(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
[
|
|
66
|
+
tailwindBin,
|
|
67
|
+
"-i",
|
|
68
|
+
"./src/app.css",
|
|
69
|
+
"-o",
|
|
70
|
+
"./public/bosia-tw.css",
|
|
71
|
+
...(isProduction ? ["--minify"] : []),
|
|
72
|
+
],
|
|
73
|
+
{
|
|
74
|
+
cwd: process.cwd(),
|
|
75
|
+
env: { ...process.env, NODE_PATH: BOSIA_NODE_PATH },
|
|
76
|
+
stderr: "pipe",
|
|
77
|
+
},
|
|
67
78
|
);
|
|
68
79
|
const tailwindPromise = tailwindProc.exited;
|
|
69
80
|
|
|
@@ -74,85 +85,90 @@ const serverPlugin = makeBosiaPlugin("bun");
|
|
|
74
85
|
// Build-time defines: inline PUBLIC_STATIC_* and STATIC_* vars
|
|
75
86
|
const staticDefines: Record<string, string> = {};
|
|
76
87
|
for (const [key, value] of Object.entries(classifiedEnv.publicStatic)) {
|
|
77
|
-
|
|
88
|
+
staticDefines[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
78
89
|
}
|
|
79
90
|
for (const [key, value] of Object.entries(classifiedEnv.privateStatic)) {
|
|
80
|
-
|
|
91
|
+
staticDefines[`import.meta.env.${key}`] = JSON.stringify(value);
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
// 5. Build Tailwind + client + server bundles in parallel
|
|
84
95
|
console.log("\n📦 Building Tailwind + client + server...");
|
|
85
96
|
const clientPromise = Bun.build({
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
entrypoints: [join(CORE_DIR, "client", "hydrate.ts")],
|
|
98
|
+
outdir: "./dist/client",
|
|
99
|
+
target: "browser",
|
|
100
|
+
splitting: true,
|
|
101
|
+
naming: { chunk: "[name]-[hash].[ext]" },
|
|
102
|
+
minify: isProduction,
|
|
103
|
+
define: {
|
|
104
|
+
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "development"),
|
|
105
|
+
...staticDefines,
|
|
106
|
+
},
|
|
107
|
+
plugins: [clientPlugin, SveltePlugin()],
|
|
97
108
|
});
|
|
98
109
|
|
|
99
110
|
const serverPromise = Bun.build({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
entrypoints: [join(CORE_DIR, "server.ts")],
|
|
112
|
+
outdir: "./dist/server",
|
|
113
|
+
target: "bun",
|
|
114
|
+
splitting: true,
|
|
115
|
+
naming: { entry: "index.[ext]", chunk: "[name]-[hash].[ext]" },
|
|
116
|
+
minify: isProduction,
|
|
117
|
+
external: ["elysia"],
|
|
118
|
+
plugins: [serverPlugin, SveltePlugin()],
|
|
108
119
|
});
|
|
109
120
|
|
|
110
121
|
const [tailwindExitCode, clientResult, serverResult] = await Promise.all([
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
tailwindPromise,
|
|
123
|
+
clientPromise,
|
|
124
|
+
serverPromise,
|
|
114
125
|
]);
|
|
115
126
|
|
|
116
127
|
if (tailwindExitCode !== 0) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
const stderr = await new Response(tailwindProc.stderr).text();
|
|
129
|
+
console.error("❌ Tailwind CSS build failed:\n" + stderr);
|
|
130
|
+
process.exit(1);
|
|
120
131
|
}
|
|
121
132
|
console.log("✅ Tailwind CSS built: public/bosia-tw.css");
|
|
122
133
|
|
|
123
134
|
if (!clientResult.success) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
console.error("❌ Client build failed:");
|
|
136
|
+
for (const msg of clientResult.logs) console.error(msg);
|
|
137
|
+
process.exit(1);
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
if (!serverResult.success) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
console.error("❌ Server build failed:");
|
|
142
|
+
for (const msg of serverResult.logs) console.error(msg);
|
|
143
|
+
process.exit(1);
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
// 6. Collect output files for dist/manifest.json
|
|
136
147
|
const jsFiles: string[] = [];
|
|
137
148
|
const cssFiles: string[] = [];
|
|
138
149
|
for (const output of clientResult.outputs) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
const rel = relative("./dist/client", output.path);
|
|
151
|
+
if (output.path.endsWith(".js")) jsFiles.push(rel);
|
|
152
|
+
if (output.path.endsWith(".css")) cssFiles.push(rel);
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
// Entry is always "index.js" due to naming: { entry: "index.[ext]" }
|
|
145
|
-
const serverEntry =
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
const serverEntry =
|
|
157
|
+
serverResult.outputs
|
|
158
|
+
.find((o) => o.path.endsWith("index.js"))
|
|
159
|
+
?.path.split("/")
|
|
160
|
+
.pop() ?? "index.js";
|
|
148
161
|
|
|
149
162
|
// 8. Write dist/manifest.json
|
|
150
163
|
mkdirSync("./dist", { recursive: true });
|
|
151
164
|
const distManifest = {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
165
|
+
js: jsFiles,
|
|
166
|
+
css: cssFiles,
|
|
167
|
+
entry:
|
|
168
|
+
jsFiles.find((f) => f === "hydrate.js") ??
|
|
169
|
+
jsFiles.find((f) => f.startsWith("hydrate")) ??
|
|
170
|
+
"hydrate.js",
|
|
171
|
+
serverEntry,
|
|
156
172
|
};
|
|
157
173
|
writeFileSync("./dist/manifest.json", JSON.stringify(distManifest, null, 2));
|
|
158
174
|
console.log(`✅ Client bundle: ${jsFiles.join(", ")}`);
|
|
@@ -1,114 +1,125 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
2
|
+
import { router } from "./router.svelte.ts";
|
|
3
|
+
import { findMatch } from "../matcher.ts";
|
|
4
|
+
import { clientRoutes } from "bosia:routes";
|
|
5
|
+
import { consumePrefetch, prefetchCache, dataUrl } from "./prefetch.ts";
|
|
6
|
+
import { appState } from "./appState.svelte.ts";
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
ssrMode = false,
|
|
10
|
+
ssrPageComponent = null,
|
|
11
|
+
ssrLayoutComponents = [],
|
|
12
|
+
ssrPageData = {},
|
|
13
|
+
ssrLayoutData = [],
|
|
14
|
+
ssrFormData = null,
|
|
15
|
+
}: {
|
|
16
|
+
ssrMode?: boolean;
|
|
17
|
+
ssrPageComponent?: any;
|
|
18
|
+
ssrLayoutComponents?: any[];
|
|
19
|
+
ssrPageData?: Record<string, any>;
|
|
20
|
+
ssrLayoutData?: Record<string, any>[];
|
|
21
|
+
ssrFormData?: any;
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
let PageComponent = $state<any>(ssrPageComponent);
|
|
25
|
+
let layoutComponents = $state<any[]>(ssrLayoutComponents ?? []);
|
|
26
|
+
// In SSR mode, render directly from props (server module singletons must
|
|
27
|
+
// not hold per-request state). On the client, read/write through `appState`
|
|
28
|
+
// so `use:enhance` and other helpers can update the same cells.
|
|
29
|
+
const pageData = $derived(ssrMode ? (ssrPageData ?? {}) : appState.pageData);
|
|
30
|
+
const layoutData = $derived(ssrMode ? (ssrLayoutData ?? []) : appState.layoutData);
|
|
31
|
+
const routeParams = $derived(ssrMode ? (ssrPageData?.params ?? {}) : appState.routeParams);
|
|
32
|
+
const formData = $derived(ssrMode ? ssrFormData : appState.form);
|
|
33
|
+
let navigating = $state(false);
|
|
34
|
+
let navDone = $state(false);
|
|
35
|
+
// Skip bar on the very first effect run (initial hydration — data already present)
|
|
36
|
+
let firstNav = true;
|
|
37
|
+
let navDoneTimer: ReturnType<typeof setTimeout> | null = null;
|
|
38
|
+
|
|
39
|
+
$effect(() => {
|
|
40
|
+
if (ssrMode) return;
|
|
41
|
+
|
|
42
|
+
const path = router.currentRoute;
|
|
43
|
+
const pathname = path.split("?")[0].split("#")[0];
|
|
44
|
+
const match = findMatch(clientRoutes, pathname);
|
|
45
|
+
if (!match) return;
|
|
46
|
+
|
|
47
|
+
let cancelled = false;
|
|
48
|
+
|
|
49
|
+
const isFirst = firstNav;
|
|
50
|
+
firstNav = false;
|
|
51
|
+
if (isFirst) return; // Initial hydration — data already in SSR props, no fetch needed
|
|
52
|
+
|
|
53
|
+
appState.form = null;
|
|
54
|
+
if (navDoneTimer) {
|
|
55
|
+
clearTimeout(navDoneTimer);
|
|
56
|
+
navDoneTimer = null;
|
|
57
|
+
}
|
|
58
|
+
navDone = false;
|
|
59
|
+
navigating = true;
|
|
60
|
+
|
|
61
|
+
// Load components + data in parallel, then update state atomically
|
|
62
|
+
// to avoid a flash of stale/empty data before the fetch completes.
|
|
63
|
+
const cached = match.route.hasServerData ? consumePrefetch(path) : null;
|
|
64
|
+
prefetchCache.clear(); // clear remaining entries on navigation — matches SvelteKit behavior
|
|
65
|
+
const dataFetch = cached
|
|
66
|
+
? Promise.resolve(cached)
|
|
67
|
+
: match.route.hasServerData
|
|
68
|
+
? fetch(dataUrl(path))
|
|
69
|
+
.then((r) => r.json())
|
|
70
|
+
.catch(() => null)
|
|
71
|
+
: Promise.resolve(null);
|
|
72
|
+
|
|
73
|
+
Promise.all([
|
|
74
|
+
match.route.page(),
|
|
75
|
+
Promise.all(match.route.layouts.map((l: any) => l())),
|
|
76
|
+
dataFetch,
|
|
77
|
+
]).then(([pageMod, layoutMods, result]: [any, any[], any]) => {
|
|
78
|
+
if (cancelled) return;
|
|
79
|
+
navigating = false;
|
|
80
|
+
navDone = true;
|
|
81
|
+
navDoneTimer = setTimeout(() => {
|
|
82
|
+
navDone = false;
|
|
83
|
+
}, 400);
|
|
84
|
+
if (result?.redirect) {
|
|
85
|
+
router.navigate(result.redirect);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (result?.error || (result === null && match.route.hasServerData)) {
|
|
89
|
+
// Data fetch failed (e.g. static hosting with no server) — full page load
|
|
90
|
+
window.location.href = path;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
PageComponent = pageMod.default;
|
|
94
|
+
layoutComponents = layoutMods.map((m: any) => m.default);
|
|
95
|
+
appState.pageData = result?.pageData ?? {};
|
|
96
|
+
appState.layoutData = result?.layoutData ?? [];
|
|
97
|
+
appState.routeParams = result?.pageData?.params ?? match.params;
|
|
98
|
+
|
|
99
|
+
// Scroll to top on forward navigation (not on popstate/back-forward)
|
|
100
|
+
if (router.isPush) window.scrollTo(0, 0);
|
|
101
|
+
|
|
102
|
+
// Update document title and meta description from server metadata
|
|
103
|
+
if (result?.metadata) {
|
|
104
|
+
if (result.metadata.title) document.title = result.metadata.title;
|
|
105
|
+
if (result.metadata.description) {
|
|
106
|
+
let meta = document.querySelector(
|
|
107
|
+
'meta[name="description"]',
|
|
108
|
+
) as HTMLMetaElement | null;
|
|
109
|
+
if (!meta) {
|
|
110
|
+
meta = document.createElement("meta");
|
|
111
|
+
meta.name = "description";
|
|
112
|
+
document.head.appendChild(meta);
|
|
113
|
+
}
|
|
114
|
+
meta.content = result.metadata.description;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
cancelled = true;
|
|
121
|
+
};
|
|
122
|
+
});
|
|
112
123
|
</script>
|
|
113
124
|
|
|
114
125
|
<!--
|
|
@@ -117,62 +128,72 @@
|
|
|
117
128
|
-->
|
|
118
129
|
|
|
119
130
|
{#if navigating}
|
|
120
|
-
|
|
131
|
+
<div class="bosia-bar loading"></div>
|
|
121
132
|
{:else if navDone}
|
|
122
|
-
|
|
133
|
+
<div class="bosia-bar done"></div>
|
|
123
134
|
{/if}
|
|
124
135
|
|
|
125
136
|
{#if layoutComponents.length > 0}
|
|
126
|
-
|
|
137
|
+
{@render renderLayout(0)}
|
|
127
138
|
{:else if PageComponent}
|
|
128
|
-
|
|
139
|
+
<PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
|
|
129
140
|
{:else}
|
|
130
|
-
|
|
141
|
+
<p>Loading...</p>
|
|
131
142
|
{/if}
|
|
132
143
|
|
|
133
144
|
{#snippet renderLayout(index: number)}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
{@const Layout = layoutComponents[index]}
|
|
146
|
+
{@const data = layoutData[index] ?? {}}
|
|
147
|
+
|
|
148
|
+
{#if index < layoutComponents.length - 1}
|
|
149
|
+
<Layout {data}>
|
|
150
|
+
{@render renderLayout(index + 1)}
|
|
151
|
+
</Layout>
|
|
152
|
+
{:else}
|
|
153
|
+
<Layout {data}>
|
|
154
|
+
{#if PageComponent}
|
|
155
|
+
<PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
|
|
156
|
+
{:else}
|
|
157
|
+
<p>Loading...</p>
|
|
158
|
+
{/if}
|
|
159
|
+
</Layout>
|
|
160
|
+
{/if}
|
|
150
161
|
{/snippet}
|
|
151
162
|
|
|
152
163
|
<style>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
164
|
+
.bosia-bar {
|
|
165
|
+
position: fixed;
|
|
166
|
+
top: 0;
|
|
167
|
+
left: 0;
|
|
168
|
+
height: 2px;
|
|
169
|
+
width: 100%;
|
|
170
|
+
background: var(--bosia-loading-color, #f73b27);
|
|
171
|
+
z-index: 9999;
|
|
172
|
+
pointer-events: none;
|
|
173
|
+
transform-origin: left center;
|
|
174
|
+
}
|
|
175
|
+
.bosia-bar.loading {
|
|
176
|
+
animation: bosia-load 8s cubic-bezier(0.02, 0.5, 0.5, 1) forwards;
|
|
177
|
+
}
|
|
178
|
+
.bosia-bar.done {
|
|
179
|
+
animation: bosia-done 0.35s ease forwards;
|
|
180
|
+
}
|
|
181
|
+
@keyframes bosia-load {
|
|
182
|
+
from {
|
|
183
|
+
transform: scaleX(0);
|
|
184
|
+
}
|
|
185
|
+
to {
|
|
186
|
+
transform: scaleX(0.85);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
@keyframes bosia-done {
|
|
190
|
+
from {
|
|
191
|
+
transform: scaleX(1);
|
|
192
|
+
opacity: 1;
|
|
193
|
+
}
|
|
194
|
+
to {
|
|
195
|
+
transform: scaleX(1);
|
|
196
|
+
opacity: 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
178
199
|
</style>
|
|
@@ -11,10 +11,10 @@ import { dataUrl } from "./prefetch.ts";
|
|
|
11
11
|
import { router } from "./router.svelte.ts";
|
|
12
12
|
|
|
13
13
|
class AppState {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
pageData = $state<Record<string, any>>({});
|
|
15
|
+
layoutData = $state<Record<string, any>[]>([]);
|
|
16
|
+
routeParams = $state<Record<string, string>>({});
|
|
17
|
+
form = $state<any>(null);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const appState = new AppState();
|
|
@@ -25,31 +25,33 @@ export const appState = new AppState();
|
|
|
25
25
|
* `invalidateAll` default. No-op if the fetch fails or returns an error.
|
|
26
26
|
*/
|
|
27
27
|
export async function refreshData(path: string): Promise<void> {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(dataUrl(path));
|
|
30
|
+
if (!res.ok) return;
|
|
31
|
+
const result = await res.json();
|
|
32
|
+
if (result?.redirect) {
|
|
33
|
+
router.navigate(result.redirect);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (result?.error) return;
|
|
37
|
+
appState.pageData = result?.pageData ?? {};
|
|
38
|
+
appState.layoutData = result?.layoutData ?? [];
|
|
39
|
+
appState.routeParams = result?.pageData?.params ?? appState.routeParams;
|
|
40
|
+
if (result?.metadata) {
|
|
41
|
+
if (result.metadata.title) document.title = result.metadata.title;
|
|
42
|
+
if (result.metadata.description) {
|
|
43
|
+
let meta = document.querySelector(
|
|
44
|
+
'meta[name="description"]',
|
|
45
|
+
) as HTMLMetaElement | null;
|
|
46
|
+
if (!meta) {
|
|
47
|
+
meta = document.createElement("meta");
|
|
48
|
+
meta.name = "description";
|
|
49
|
+
document.head.appendChild(meta);
|
|
50
|
+
}
|
|
51
|
+
meta.content = result.metadata.description;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// best-effort — silently swallow
|
|
56
|
+
}
|
|
55
57
|
}
|