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.
Files changed (86) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -54
  3. package/src/ambient.d.ts +31 -0
  4. package/src/cli/add.ts +120 -114
  5. package/src/cli/build.ts +10 -10
  6. package/src/cli/create.ts +142 -137
  7. package/src/cli/dev.ts +7 -9
  8. package/src/cli/feat.ts +266 -258
  9. package/src/cli/index.ts +51 -42
  10. package/src/cli/registry.ts +136 -115
  11. package/src/cli/start.ts +17 -17
  12. package/src/cli/test.ts +25 -0
  13. package/src/core/build.ts +72 -56
  14. package/src/core/client/App.svelte +177 -156
  15. package/src/core/client/appState.svelte.ts +33 -31
  16. package/src/core/client/enhance.ts +83 -78
  17. package/src/core/client/hydrate.ts +95 -81
  18. package/src/core/client/prefetch.ts +101 -94
  19. package/src/core/client/router.svelte.ts +64 -51
  20. package/src/core/cookies.ts +70 -66
  21. package/src/core/cors.ts +44 -35
  22. package/src/core/csrf.ts +38 -38
  23. package/src/core/dedup.ts +17 -17
  24. package/src/core/dev.ts +196 -168
  25. package/src/core/env.ts +160 -148
  26. package/src/core/envCodegen.ts +73 -73
  27. package/src/core/errors.ts +48 -49
  28. package/src/core/hooks.ts +50 -50
  29. package/src/core/html.ts +184 -145
  30. package/src/core/matcher.ts +130 -121
  31. package/src/core/paths.ts +8 -10
  32. package/src/core/plugin.ts +113 -107
  33. package/src/core/prerender.ts +191 -122
  34. package/src/core/renderer.ts +359 -286
  35. package/src/core/routeFile.ts +140 -127
  36. package/src/core/routeTypes.ts +144 -83
  37. package/src/core/scanner.ts +125 -95
  38. package/src/core/server.ts +538 -424
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/index.ts +8 -8
  41. package/src/lib/utils.ts +44 -30
  42. package/templates/default/.prettierignore +5 -0
  43. package/templates/default/.prettierrc.json +9 -0
  44. package/templates/default/README.md +5 -5
  45. package/templates/default/package.json +22 -18
  46. package/templates/default/src/app.css +80 -80
  47. package/templates/default/src/app.d.ts +3 -3
  48. package/templates/default/src/routes/+error.svelte +7 -10
  49. package/templates/default/src/routes/+layout.svelte +2 -2
  50. package/templates/default/src/routes/+page.svelte +30 -32
  51. package/templates/default/src/routes/about/+page.svelte +3 -3
  52. package/templates/default/tsconfig.json +20 -20
  53. package/templates/demo/.prettierignore +5 -0
  54. package/templates/demo/.prettierrc.json +9 -0
  55. package/templates/demo/README.md +9 -9
  56. package/templates/demo/package.json +22 -17
  57. package/templates/demo/src/app.css +80 -80
  58. package/templates/demo/src/app.d.ts +3 -3
  59. package/templates/demo/src/hooks.server.ts +9 -9
  60. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  61. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  62. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  63. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  64. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  65. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  67. package/templates/demo/src/routes/+error.svelte +10 -7
  68. package/templates/demo/src/routes/+layout.server.ts +4 -4
  69. package/templates/demo/src/routes/+layout.svelte +2 -2
  70. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  71. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  72. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  73. package/templates/demo/tsconfig.json +20 -20
  74. package/templates/todo/.prettierignore +5 -0
  75. package/templates/todo/.prettierrc.json +9 -0
  76. package/templates/todo/README.md +9 -9
  77. package/templates/todo/package.json +22 -17
  78. package/templates/todo/src/app.css +80 -80
  79. package/templates/todo/src/app.d.ts +7 -7
  80. package/templates/todo/src/hooks.server.ts +9 -9
  81. package/templates/todo/src/routes/+error.svelte +10 -7
  82. package/templates/todo/src/routes/+layout.server.ts +4 -4
  83. package/templates/todo/src/routes/+layout.svelte +2 -2
  84. package/templates/todo/src/routes/+page.svelte +44 -44
  85. package/templates/todo/template.json +1 -1
  86. 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 { rmSync("./dist", { recursive: true, force: true }); } catch { }
31
- try { rmSync("./.bosia", { recursive: true, force: true }); } catch { }
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
- console.log(` ${r.pattern} → ${r.page}${r.pageServer ? " (server)" : ""}`);
41
+ console.log(` ${r.pattern} → ${r.page}${r.pageServer ? " (server)" : ""}`);
38
42
  }
39
43
  if (manifest.apis.length > 0) {
40
- console.log(`📡 Found ${manifest.apis.length} API route(s):`);
41
- for (const r of manifest.apis) {
42
- console.log(` ${r.pattern} → ${r.server}`);
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
- [tailwindBin, "-i", "./src/app.css", "-o", "./public/bosia-tw.css", ...(isProduction ? ["--minify"] : [])],
62
- {
63
- cwd: process.cwd(),
64
- env: { ...process.env, NODE_PATH: BOSIA_NODE_PATH },
65
- stderr: "pipe",
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
- staticDefines[`import.meta.env.${key}`] = JSON.stringify(value);
88
+ staticDefines[`import.meta.env.${key}`] = JSON.stringify(value);
78
89
  }
79
90
  for (const [key, value] of Object.entries(classifiedEnv.privateStatic)) {
80
- staticDefines[`import.meta.env.${key}`] = JSON.stringify(value);
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
- entrypoints: [join(CORE_DIR, "client", "hydrate.ts")],
87
- outdir: "./dist/client",
88
- target: "browser",
89
- splitting: true,
90
- naming: { chunk: "[name]-[hash].[ext]" },
91
- minify: isProduction,
92
- define: {
93
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "development"),
94
- ...staticDefines,
95
- },
96
- plugins: [clientPlugin, SveltePlugin()],
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
- entrypoints: [join(CORE_DIR, "server.ts")],
101
- outdir: "./dist/server",
102
- target: "bun",
103
- splitting: true,
104
- naming: { entry: "index.[ext]", chunk: "[name]-[hash].[ext]" },
105
- minify: isProduction,
106
- external: ["elysia"],
107
- plugins: [serverPlugin, SveltePlugin()],
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
- tailwindPromise,
112
- clientPromise,
113
- serverPromise,
122
+ tailwindPromise,
123
+ clientPromise,
124
+ serverPromise,
114
125
  ]);
115
126
 
116
127
  if (tailwindExitCode !== 0) {
117
- const stderr = await new Response(tailwindProc.stderr).text();
118
- console.error("❌ Tailwind CSS build failed:\n" + stderr);
119
- process.exit(1);
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
- console.error("❌ Client build failed:");
125
- for (const msg of clientResult.logs) console.error(msg);
126
- process.exit(1);
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
- console.error("❌ Server build failed:");
131
- for (const msg of serverResult.logs) console.error(msg);
132
- process.exit(1);
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
- const rel = relative("./dist/client", output.path);
140
- if (output.path.endsWith(".js")) jsFiles.push(rel);
141
- if (output.path.endsWith(".css")) cssFiles.push(rel);
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 = serverResult.outputs
146
- .find(o => o.path.endsWith("index.js"))
147
- ?.path.split("/").pop() ?? "index.js";
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
- js: jsFiles,
153
- css: cssFiles,
154
- entry: jsFiles.find(f => f === "hydrate.js") ?? jsFiles.find(f => f.startsWith("hydrate")) ?? "hydrate.js",
155
- serverEntry,
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
- 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) { clearTimeout(navDoneTimer); navDoneTimer = null; }
55
- navDone = false;
56
- navigating = true;
57
-
58
- // Load components + data in parallel, then update state atomically
59
- // to avoid a flash of stale/empty data before the fetch completes.
60
- const cached = match.route.hasServerData ? consumePrefetch(path) : null;
61
- prefetchCache.clear(); // clear remaining entries on navigation matches SvelteKit behavior
62
- const dataFetch = cached
63
- ? Promise.resolve(cached)
64
- : match.route.hasServerData
65
- ? fetch(dataUrl(path)).then(r => r.json()).catch(() => null)
66
- : Promise.resolve(null);
67
-
68
- Promise.all([
69
- match.route.page(),
70
- Promise.all(match.route.layouts.map((l: any) => l())),
71
- dataFetch,
72
- ]).then(([pageMod, layoutMods, result]: [any, any[], any]) => {
73
- if (cancelled) return;
74
- navigating = false;
75
- navDone = true;
76
- navDoneTimer = setTimeout(() => { navDone = false; }, 400);
77
- if (result?.redirect) {
78
- router.navigate(result.redirect);
79
- return;
80
- }
81
- if (result?.error || (result === null && match.route.hasServerData)) {
82
- // Data fetch failed (e.g. static hosting with no server) — full page load
83
- window.location.href = path;
84
- return;
85
- }
86
- PageComponent = pageMod.default;
87
- layoutComponents = layoutMods.map((m: any) => m.default);
88
- appState.pageData = result?.pageData ?? {};
89
- appState.layoutData = result?.layoutData ?? [];
90
- appState.routeParams = result?.pageData?.params ?? match.params;
91
-
92
- // Scroll to top on forward navigation (not on popstate/back-forward)
93
- if (router.isPush) window.scrollTo(0, 0);
94
-
95
- // Update document title and meta description from server metadata
96
- if (result?.metadata) {
97
- if (result.metadata.title) document.title = result.metadata.title;
98
- if (result.metadata.description) {
99
- let meta = document.querySelector('meta[name="description"]') as HTMLMetaElement | null;
100
- if (!meta) {
101
- meta = document.createElement("meta");
102
- meta.name = "description";
103
- document.head.appendChild(meta);
104
- }
105
- meta.content = result.metadata.description;
106
- }
107
- }
108
- });
109
-
110
- return () => { cancelled = true; };
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
- <div class="bosia-bar loading"></div>
131
+ <div class="bosia-bar loading"></div>
121
132
  {:else if navDone}
122
- <div class="bosia-bar done"></div>
133
+ <div class="bosia-bar done"></div>
123
134
  {/if}
124
135
 
125
136
  {#if layoutComponents.length > 0}
126
- {@render renderLayout(0)}
137
+ {@render renderLayout(0)}
127
138
  {:else if PageComponent}
128
- <PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
139
+ <PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
129
140
  {:else}
130
- <p>Loading...</p>
141
+ <p>Loading...</p>
131
142
  {/if}
132
143
 
133
144
  {#snippet renderLayout(index: number)}
134
- {@const Layout = layoutComponents[index]}
135
- {@const data = layoutData[index] ?? {}}
136
-
137
- {#if index < layoutComponents.length - 1}
138
- <Layout {data}>
139
- {@render renderLayout(index + 1)}
140
- </Layout>
141
- {:else}
142
- <Layout {data}>
143
- {#if PageComponent}
144
- <PageComponent data={{ ...pageData, params: routeParams }} form={formData} />
145
- {:else}
146
- <p>Loading...</p>
147
- {/if}
148
- </Layout>
149
- {/if}
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
- .bosia-bar {
154
- position: fixed;
155
- top: 0;
156
- left: 0;
157
- height: 2px;
158
- width: 100%;
159
- background: var(--bosia-loading-color, #f73b27);
160
- z-index: 9999;
161
- pointer-events: none;
162
- transform-origin: left center;
163
- }
164
- .bosia-bar.loading {
165
- animation: bosia-load 8s cubic-bezier(0.02, 0.5, 0.5, 1) forwards;
166
- }
167
- .bosia-bar.done {
168
- animation: bosia-done 0.35s ease forwards;
169
- }
170
- @keyframes bosia-load {
171
- from { transform: scaleX(0); }
172
- to { transform: scaleX(0.85); }
173
- }
174
- @keyframes bosia-done {
175
- from { transform: scaleX(1); opacity: 1; }
176
- to { transform: scaleX(1); opacity: 0; }
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
- pageData = $state<Record<string, any>>({});
15
- layoutData = $state<Record<string, any>[]>([]);
16
- routeParams = $state<Record<string, string>>({});
17
- form = $state<any>(null);
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
- 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('meta[name="description"]') as HTMLMetaElement | null;
44
- if (!meta) {
45
- meta = document.createElement("meta");
46
- meta.name = "description";
47
- document.head.appendChild(meta);
48
- }
49
- meta.content = result.metadata.description;
50
- }
51
- }
52
- } catch {
53
- // best-effort — silently swallow
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
  }