bosia 0.5.8 → 0.5.10
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 +1 -1
- package/src/cli/font.ts +45 -0
- package/src/cli/index.ts +5 -0
- package/src/core/appHtml.ts +106 -0
- package/src/core/build.ts +15 -0
- package/src/core/client/App.svelte +23 -3
- package/src/core/client/appState.svelte.ts +4 -0
- package/src/core/client/hydrate.ts +5 -1
- package/src/core/client/navListeners.ts +51 -0
- package/src/core/client/navigation.ts +99 -0
- package/src/core/client/router.svelte.ts +69 -5
- package/src/core/html.ts +70 -4
- package/src/core/plugins/inspector/bun-plugin.ts +12 -7
- package/src/core/renderer.ts +19 -4
- package/src/lib/client.ts +8 -1
- package/templates/default/public/logo-dark.svg +14 -0
- package/templates/default/public/logo-light.svg +14 -0
- package/templates/default/src/app.html +11 -0
- package/templates/demo/public/logo-dark.svg +14 -0
- package/templates/demo/public/logo-light.svg +14 -0
- package/templates/demo/src/app.html +11 -0
- package/templates/todo/public/logo-dark.svg +14 -0
- package/templates/todo/public/logo-light.svg +14 -0
- package/templates/todo/src/app.html +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"type": "module",
|
|
5
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.",
|
|
6
6
|
"keywords": [
|
package/src/cli/font.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { mergeFontImports } from "./fonts.ts";
|
|
4
|
+
|
|
5
|
+
// ─── bun x bosia@latest add font <family> <url> ─────────────
|
|
6
|
+
// Prepends a Google-Fonts-style `@import url(...)` to src/app.css
|
|
7
|
+
// with a `/* bosia-font: <family> */` marker so it's idempotent
|
|
8
|
+
// and survives Tailwind v4 / LightningCSS @import-ordering rules.
|
|
9
|
+
//
|
|
10
|
+
// Why a dedicated command: appending the @import at the bottom
|
|
11
|
+
// (or anywhere after `@import "tailwindcss"`) causes LightningCSS
|
|
12
|
+
// to silently drop the rule from public/bosia-tw.css. Hand-edits
|
|
13
|
+
// frequently get this wrong; this command always prepends.
|
|
14
|
+
|
|
15
|
+
export async function runAddFont(family: string | undefined, url: string | undefined) {
|
|
16
|
+
if (!family || !url) {
|
|
17
|
+
console.error(
|
|
18
|
+
"❌ Please provide a font family and URL.\n" +
|
|
19
|
+
' Usage: bun x bosia@latest add font "<Family>" "<@import url>"\n' +
|
|
20
|
+
' Example: bun x bosia@latest add font "Fredoka" \\\n' +
|
|
21
|
+
' "https://fonts.googleapis.com/css2?family=Fredoka:wght@400;700&display=swap"',
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const appCssPath = join(cwd, "src", "app.css");
|
|
28
|
+
|
|
29
|
+
if (!existsSync(appCssPath)) {
|
|
30
|
+
console.error(`❌ src/app.css not found at ${appCssPath}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const added = mergeFontImports(appCssPath, { [family]: url });
|
|
35
|
+
|
|
36
|
+
if (added.length === 0) {
|
|
37
|
+
console.log(`⬡ Font already present: ${family} (no changes)`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`✅ Added font: ${family}`);
|
|
42
|
+
console.log(` ✍️ src/app.css ← @import url("${url}");`);
|
|
43
|
+
console.log("\n💡 Next: declare it on a token in src/app.css, e.g.");
|
|
44
|
+
console.log(' @theme { --font-display: "' + family + '", sans-serif; }');
|
|
45
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -48,6 +48,9 @@ async function main() {
|
|
|
48
48
|
const themeFlags = args.filter((a) => a.startsWith("--"));
|
|
49
49
|
const { runAddTheme } = await import("./theme.ts");
|
|
50
50
|
await runAddTheme(positional[1], themeFlags);
|
|
51
|
+
} else if (sub === "font") {
|
|
52
|
+
const { runAddFont } = await import("./font.ts");
|
|
53
|
+
await runAddFont(positional[1], positional[2]);
|
|
51
54
|
} else {
|
|
52
55
|
const { runAdd } = await import("./add.ts");
|
|
53
56
|
await runAdd(positional, flags);
|
|
@@ -77,6 +80,7 @@ Commands:
|
|
|
77
80
|
add <component...> [-y] Add one or more UI components from the registry
|
|
78
81
|
add block <cat>/<name> Add a composed block from the registry
|
|
79
82
|
add theme <name> Add a theme (tokens.css) from the registry
|
|
83
|
+
add font <family> <url> Prepend an @import url(...) for a font family to src/app.css
|
|
80
84
|
feat <feature> Add a feature scaffold from the registry [--local]
|
|
81
85
|
|
|
82
86
|
Examples:
|
|
@@ -94,6 +98,7 @@ Examples:
|
|
|
94
98
|
bun x bosia@latest add shop/cart → src/lib/components/shop/cart/
|
|
95
99
|
bun x bosia@latest add block cards/feature-editorial
|
|
96
100
|
bun x bosia@latest add theme editorial
|
|
101
|
+
bun x bosia@latest add font "Fredoka" "https://fonts.googleapis.com/css2?family=Fredoka:wght@400;700&display=swap"
|
|
97
102
|
bun x bosia@latest feat login
|
|
98
103
|
`);
|
|
99
104
|
break;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
// ─── Types ────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export type AppHtmlSegments = {
|
|
7
|
+
headOpen: string;
|
|
8
|
+
headClose: string;
|
|
9
|
+
tail: string;
|
|
10
|
+
hasCustomFavicon: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// ─── Cached Singleton ─────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
let cachedSegments: AppHtmlSegments | undefined;
|
|
16
|
+
|
|
17
|
+
// ─── Parse & Validate ─────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function loadAppHtmlTemplate(cwd: string = process.cwd()): AppHtmlSegments {
|
|
20
|
+
const templatePath = join(cwd, "src", "app.html");
|
|
21
|
+
|
|
22
|
+
if (!existsSync(templatePath)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`src/app.html is required but not found. Create src/app.html with %bosia.head% and %bosia.body% placeholders.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let template = readFileSync(templatePath, "utf-8");
|
|
29
|
+
|
|
30
|
+
// Replace static placeholders at parse time
|
|
31
|
+
template = replaceStaticPlaceholders(template);
|
|
32
|
+
|
|
33
|
+
// Validate required placeholders
|
|
34
|
+
const missingPlaceholders: string[] = [];
|
|
35
|
+
if (!template.includes("%bosia.head%")) missingPlaceholders.push("%bosia.head%");
|
|
36
|
+
if (!template.includes("%bosia.body%")) missingPlaceholders.push("%bosia.body%");
|
|
37
|
+
|
|
38
|
+
if (missingPlaceholders.length > 0) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`src/app.html is missing required placeholder(s): ${missingPlaceholders.join(", ")}. ` +
|
|
41
|
+
`Both %bosia.head% and %bosia.body% must be present.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Split into segments
|
|
46
|
+
const [headOpen, rest1] = template.split("%bosia.head%", 2);
|
|
47
|
+
const [headClose, tail] = rest1.split("%bosia.body%", 2);
|
|
48
|
+
|
|
49
|
+
// Check for custom favicon
|
|
50
|
+
const hasCustomFavicon = headOpen.includes('rel="icon"');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
headOpen,
|
|
54
|
+
headClose,
|
|
55
|
+
tail,
|
|
56
|
+
hasCustomFavicon,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function replaceStaticPlaceholders(template: string): string {
|
|
61
|
+
// Replace %bosia.assets% with BOSIA_ASSETS_BASE env var
|
|
62
|
+
const assetsBase = process.env.BOSIA_ASSETS_BASE || "";
|
|
63
|
+
template = template.replaceAll("%bosia.assets%", assetsBase);
|
|
64
|
+
|
|
65
|
+
// Replace %bosia.env.PUBLIC_FOO% with process.env.PUBLIC_FOO
|
|
66
|
+
template = template.replace(/%bosia\.env\.(PUBLIC_[A-Z0-9_]+)%/g, (_match, key: string) => {
|
|
67
|
+
return process.env[key] ?? "";
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return template;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Cached Getter ────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export function getAppHtmlSegments(cwd: string = process.cwd()): AppHtmlSegments {
|
|
76
|
+
if (cachedSegments !== undefined) {
|
|
77
|
+
return cachedSegments;
|
|
78
|
+
}
|
|
79
|
+
cachedSegments = loadAppHtmlTemplate(cwd);
|
|
80
|
+
return cachedSegments;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Cache Invalidation ───────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export function invalidateAppHtmlCache(): void {
|
|
86
|
+
cachedSegments = undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Runtime Interpolation ────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export function interpolateSegment(
|
|
92
|
+
segment: string,
|
|
93
|
+
vars: { nonce?: string; lang?: string },
|
|
94
|
+
): string {
|
|
95
|
+
let result = segment;
|
|
96
|
+
|
|
97
|
+
if (vars.lang) {
|
|
98
|
+
result = result.replaceAll("%bosia.lang%", vars.lang);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (vars.nonce) {
|
|
102
|
+
result = result.replaceAll("%bosia.nonce%", vars.nonce);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
package/src/core/build.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { generateEnvModules } from "./envCodegen.ts";
|
|
|
12
12
|
import { BOSIA_NODE_PATH, OUT_DIR, resolveBosiaBin } from "./paths.ts";
|
|
13
13
|
import { loadPlugins } from "./config.ts";
|
|
14
14
|
import type { BuildContext } from "./types/plugin.ts";
|
|
15
|
+
import { loadAppHtmlTemplate } from "./appHtml.ts";
|
|
15
16
|
|
|
16
17
|
// Resolved from this file's location inside the bosia package
|
|
17
18
|
const CORE_DIR = import.meta.dir;
|
|
@@ -79,6 +80,20 @@ if (manifest.apis.length > 0) {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// 1b. Load & validate src/app.html template (required)
|
|
84
|
+
let appHtml: any;
|
|
85
|
+
try {
|
|
86
|
+
appHtml = loadAppHtmlTemplate(process.cwd());
|
|
87
|
+
console.log(
|
|
88
|
+
"📄 Loaded src/app.html (favicon override: " +
|
|
89
|
+
(appHtml.hasCustomFavicon ? "yes" : "no") +
|
|
90
|
+
")",
|
|
91
|
+
);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(`❌ src/app.html validation failed:\n${(err as Error).message}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
for (const p of userPlugins) {
|
|
83
98
|
if (p.build?.postScan) {
|
|
84
99
|
await p.build.postScan(manifest, buildCtx);
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { appState, clearDirty } from "./appState.svelte.ts";
|
|
7
7
|
import { captureSnapshot, liveContext, shouldRerun, type CacheEntry } from "./loaderCache.ts";
|
|
8
8
|
import { pickErrorPage } from "../errorMatch.ts";
|
|
9
|
+
import { fireAfterNavigate, type Navigation } from "./navListeners.ts";
|
|
10
|
+
import { drainNavResolvers } from "./navigation.ts";
|
|
9
11
|
|
|
10
12
|
let {
|
|
11
13
|
ssrMode = false,
|
|
@@ -129,6 +131,18 @@
|
|
|
129
131
|
.catch(() => null)
|
|
130
132
|
: Promise.resolve(null);
|
|
131
133
|
|
|
134
|
+
const settle = (target: { url: URL; params: Record<string, string> } | null) => {
|
|
135
|
+
const nav: Navigation = {
|
|
136
|
+
from: null,
|
|
137
|
+
to: target,
|
|
138
|
+
type: router.lastNavType,
|
|
139
|
+
willUnload: false,
|
|
140
|
+
cancel: () => {},
|
|
141
|
+
};
|
|
142
|
+
fireAfterNavigate(nav);
|
|
143
|
+
drainNavResolvers();
|
|
144
|
+
};
|
|
145
|
+
|
|
132
146
|
Promise.all([
|
|
133
147
|
match.route.page(),
|
|
134
148
|
Promise.all(match.route.layouts.map((l: any) => l())),
|
|
@@ -184,7 +198,9 @@
|
|
|
184
198
|
appState.errorComponent = errMod.default;
|
|
185
199
|
appState.errorProps = { error: { status: errStatus, message: errMessage } };
|
|
186
200
|
appState.errorDepth = K;
|
|
187
|
-
if (router.isPush) window.scrollTo(0, 0);
|
|
201
|
+
if (router.isPush && !router.suppressScroll) window.scrollTo(0, 0);
|
|
202
|
+
router.suppressScroll = false;
|
|
203
|
+
settle({ url, params: match.params });
|
|
188
204
|
} catch {
|
|
189
205
|
window.location.href = path;
|
|
190
206
|
}
|
|
@@ -272,8 +288,10 @@
|
|
|
272
288
|
appState.errorProps = null;
|
|
273
289
|
appState.errorDepth = null;
|
|
274
290
|
|
|
275
|
-
// Scroll to top on forward navigation (not on popstate/back-forward)
|
|
276
|
-
|
|
291
|
+
// Scroll to top on forward navigation (not on popstate/back-forward).
|
|
292
|
+
// goto({ noScroll: true }) flips `router.suppressScroll` for one nav.
|
|
293
|
+
if (router.isPush && !router.suppressScroll) window.scrollTo(0, 0);
|
|
294
|
+
router.suppressScroll = false;
|
|
277
295
|
|
|
278
296
|
// Update document title and meta description from server metadata
|
|
279
297
|
if (result?.metadata) {
|
|
@@ -290,6 +308,8 @@
|
|
|
290
308
|
meta.content = result.metadata.description;
|
|
291
309
|
}
|
|
292
310
|
}
|
|
311
|
+
|
|
312
|
+
settle({ url, params: match.params });
|
|
293
313
|
});
|
|
294
314
|
|
|
295
315
|
return () => {
|
|
@@ -38,6 +38,10 @@ class AppState {
|
|
|
38
38
|
// Bumped by `invalidate*()` to wake the App.svelte nav effect, so calling
|
|
39
39
|
// `invalidate("k")` re-runs the loader pipeline without changing the URL.
|
|
40
40
|
invalidationTick = $state(0);
|
|
41
|
+
// Resolvers waiting for the next navigation to settle. Drained by
|
|
42
|
+
// App.svelte after each nav effect finishes (success or error). Used by
|
|
43
|
+
// `goto()` to return a Promise that mirrors SvelteKit's contract.
|
|
44
|
+
navResolvers: Array<() => void> = [];
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
export const appState = new AppState();
|
|
@@ -130,7 +130,11 @@ async function main() {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
main()
|
|
133
|
+
main().catch((err) => {
|
|
134
|
+
// Without this, a hydration failure leaves "Loading..." stuck on screen
|
|
135
|
+
// with no console signal. Surface it loudly instead.
|
|
136
|
+
console.error("[bosia] hydration failed", err);
|
|
137
|
+
});
|
|
134
138
|
|
|
135
139
|
// ─── Hot Reload (dev only) ────────────────────────────────
|
|
136
140
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ─── Navigation lifecycle registry ────────────────────────
|
|
2
|
+
// Shared listener storage for `beforeNavigate` / `afterNavigate`. Kept in a
|
|
3
|
+
// dedicated module so `router.svelte.ts` can fire events without importing
|
|
4
|
+
// `navigation.ts` (which transitively imports the router — would deadlock
|
|
5
|
+
// the ESM evaluation cycle).
|
|
6
|
+
|
|
7
|
+
export interface NavigationTarget {
|
|
8
|
+
url: URL;
|
|
9
|
+
params: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Navigation {
|
|
13
|
+
from: NavigationTarget | null;
|
|
14
|
+
to: NavigationTarget | null;
|
|
15
|
+
type: "link" | "goto" | "popstate" | "form" | "enter";
|
|
16
|
+
willUnload: boolean;
|
|
17
|
+
cancel: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type BeforeNavigateCallback = (nav: Navigation) => void | Promise<void>;
|
|
21
|
+
export type AfterNavigateCallback = (nav: Navigation) => void;
|
|
22
|
+
|
|
23
|
+
export const beforeListeners = new Set<BeforeNavigateCallback>();
|
|
24
|
+
export const afterListeners = new Set<AfterNavigateCallback>();
|
|
25
|
+
|
|
26
|
+
/** Returns true when navigation should proceed, false when a listener cancelled it. */
|
|
27
|
+
export function fireBeforeNavigate(nav: Navigation): boolean {
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
nav.cancel = () => {
|
|
30
|
+
cancelled = true;
|
|
31
|
+
};
|
|
32
|
+
for (const fn of beforeListeners) {
|
|
33
|
+
try {
|
|
34
|
+
fn(nav);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn("[bosia] beforeNavigate listener threw", err);
|
|
37
|
+
}
|
|
38
|
+
if (cancelled) return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function fireAfterNavigate(nav: Navigation): void {
|
|
44
|
+
for (const fn of afterListeners) {
|
|
45
|
+
try {
|
|
46
|
+
fn(nav);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn("[bosia] afterNavigate listener threw", err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -10,10 +10,47 @@
|
|
|
10
10
|
// await invalidate((url) => url.pathname.startsWith("/api/"));
|
|
11
11
|
// await invalidateAll();
|
|
12
12
|
|
|
13
|
+
import { onDestroy } from "svelte";
|
|
13
14
|
import { appState } from "./appState.svelte.ts";
|
|
15
|
+
import { router } from "./router.svelte.ts";
|
|
16
|
+
import {
|
|
17
|
+
afterListeners,
|
|
18
|
+
beforeListeners,
|
|
19
|
+
type AfterNavigateCallback,
|
|
20
|
+
type BeforeNavigateCallback,
|
|
21
|
+
} from "./navListeners.ts";
|
|
14
22
|
|
|
15
23
|
type InvalidateTarget = string | URL | ((url: URL) => boolean);
|
|
16
24
|
|
|
25
|
+
export type { Navigation, NavigationTarget } from "./navListeners.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register a callback that runs before each client-side navigation. The
|
|
29
|
+
* callback may call `nav.cancel()` to block the navigation. Auto-unregisters
|
|
30
|
+
* when the calling Svelte component is destroyed.
|
|
31
|
+
*/
|
|
32
|
+
export function beforeNavigate(fn: BeforeNavigateCallback): void {
|
|
33
|
+
beforeListeners.add(fn);
|
|
34
|
+
try {
|
|
35
|
+
onDestroy(() => beforeListeners.delete(fn));
|
|
36
|
+
} catch {
|
|
37
|
+
// Not inside a component — caller is responsible for lifetime.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register a callback that runs after each client-side navigation settles.
|
|
43
|
+
* Auto-unregisters when the calling Svelte component is destroyed.
|
|
44
|
+
*/
|
|
45
|
+
export function afterNavigate(fn: AfterNavigateCallback): void {
|
|
46
|
+
afterListeners.add(fn);
|
|
47
|
+
try {
|
|
48
|
+
onDestroy(() => afterListeners.delete(fn));
|
|
49
|
+
} catch {
|
|
50
|
+
// Not inside a component — caller is responsible for lifetime.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
17
54
|
function bumpTick() {
|
|
18
55
|
appState.invalidationTick = appState.invalidationTick + 1;
|
|
19
56
|
}
|
|
@@ -57,3 +94,65 @@ export function invalidateAll(): Promise<void> {
|
|
|
57
94
|
bumpTick();
|
|
58
95
|
return Promise.resolve();
|
|
59
96
|
}
|
|
97
|
+
|
|
98
|
+
// ─── goto ─────────────────────────────────────────────────
|
|
99
|
+
// Programmatic SPA navigation. Counterpart to SvelteKit's `goto()`.
|
|
100
|
+
//
|
|
101
|
+
// Usage:
|
|
102
|
+
// import { goto } from "bosia/client";
|
|
103
|
+
// await goto("/dashboard");
|
|
104
|
+
// await goto("/login", { replaceState: true, invalidateAll: true });
|
|
105
|
+
|
|
106
|
+
export interface GotoOptions {
|
|
107
|
+
/** Use `history.replaceState` instead of `history.pushState`. */
|
|
108
|
+
replaceState?: boolean;
|
|
109
|
+
/** Mark every loader dirty so the next nav re-runs all of them. */
|
|
110
|
+
invalidateAll?: boolean;
|
|
111
|
+
/** Skip the default scroll-to-top after navigation. */
|
|
112
|
+
noScroll?: boolean;
|
|
113
|
+
/** Reserved — not yet honored by the framework. */
|
|
114
|
+
keepFocus?: boolean;
|
|
115
|
+
/** Reserved — not yet honored (no shallow routing). */
|
|
116
|
+
state?: Record<string, unknown>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Navigate to `url` via the client router. Returns a Promise that resolves
|
|
121
|
+
* after the navigation has settled (loaders ran, components mounted) or
|
|
122
|
+
* immediately if the URL matches the current route.
|
|
123
|
+
*/
|
|
124
|
+
export function goto(url: string, opts: GotoOptions = {}): Promise<void> {
|
|
125
|
+
if (typeof window === "undefined") return Promise.resolve();
|
|
126
|
+
|
|
127
|
+
if (opts.invalidateAll) {
|
|
128
|
+
appState.dirty.all = true;
|
|
129
|
+
}
|
|
130
|
+
if (opts.noScroll) {
|
|
131
|
+
router.suppressScroll = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const beforePath = router.currentRoute;
|
|
135
|
+
|
|
136
|
+
return new Promise<void>((resolve) => {
|
|
137
|
+
appState.navResolvers.push(resolve);
|
|
138
|
+
router.navigate(url, { replace: opts.replaceState, source: "goto" });
|
|
139
|
+
// `navigate()` short-circuits when `currentRoute === path` — the nav
|
|
140
|
+
// effect won't fire, so nothing will drain the resolver. Resolve now.
|
|
141
|
+
if (router.currentRoute === beforePath) {
|
|
142
|
+
drainNavResolvers();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Internal — App.svelte calls this after each nav effect settles. */
|
|
148
|
+
export function drainNavResolvers(): void {
|
|
149
|
+
const queue = appState.navResolvers;
|
|
150
|
+
appState.navResolvers = [];
|
|
151
|
+
for (const fn of queue) {
|
|
152
|
+
try {
|
|
153
|
+
fn();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.warn("[bosia] navResolver threw", err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import { findMatch, canonicalPathname } from "../matcher.ts";
|
|
6
6
|
import { clientRoutes } from "bosia:routes";
|
|
7
|
+
import { fireBeforeNavigate, type Navigation } from "./navListeners.ts";
|
|
8
|
+
|
|
9
|
+
export type NavType = "link" | "goto" | "popstate" | "form" | "enter";
|
|
10
|
+
|
|
11
|
+
function buildTarget(path: string): { url: URL; params: Record<string, string> } | null {
|
|
12
|
+
if (typeof window === "undefined") return null;
|
|
13
|
+
const pathname = path.split("?")[0].split("#")[0];
|
|
14
|
+
const match = findMatch(clientRoutes, pathname);
|
|
15
|
+
const url = new URL(path, window.location.origin);
|
|
16
|
+
return { url, params: match?.params ?? {} };
|
|
17
|
+
}
|
|
7
18
|
|
|
8
19
|
export const router = new (class Router {
|
|
9
20
|
currentRoute = $state(
|
|
@@ -14,8 +25,12 @@ export const router = new (class Router {
|
|
|
14
25
|
params = $state<Record<string, string>>({});
|
|
15
26
|
/** True when navigation was triggered by a link click / navigate() call, false on popstate (back/forward). */
|
|
16
27
|
isPush = $state(true);
|
|
28
|
+
/** Source of the most recent navigation — feeds the Navigation object passed to lifecycle hooks. */
|
|
29
|
+
lastNavType: NavType = "enter";
|
|
30
|
+
/** Set by `goto({ noScroll: true })`; consumed once by App.svelte after the next nav settles. */
|
|
31
|
+
suppressScroll = false;
|
|
17
32
|
|
|
18
|
-
navigate(path: string) {
|
|
33
|
+
navigate(path: string, opts: { replace?: boolean; source?: NavType } = {}) {
|
|
19
34
|
if (this.currentRoute === path) return;
|
|
20
35
|
// Unknown route — let the server handle it (renders +error.svelte with 404)
|
|
21
36
|
const queryHash = path.slice(path.split("?")[0].split("#")[0].length);
|
|
@@ -31,10 +46,28 @@ export const router = new (class Router {
|
|
|
31
46
|
(match.route as any).trailingSlash ?? "never",
|
|
32
47
|
);
|
|
33
48
|
const finalPath = canonical !== null ? canonical + queryHash : path;
|
|
49
|
+
|
|
50
|
+
const navType: NavType = opts.source ?? "link";
|
|
51
|
+
const fromTarget = buildTarget(this.currentRoute);
|
|
52
|
+
const toTarget = buildTarget(finalPath);
|
|
53
|
+
const nav: Navigation = {
|
|
54
|
+
from: fromTarget,
|
|
55
|
+
to: toTarget,
|
|
56
|
+
type: navType,
|
|
57
|
+
willUnload: false,
|
|
58
|
+
cancel: () => {},
|
|
59
|
+
};
|
|
60
|
+
if (!fireBeforeNavigate(nav)) return;
|
|
61
|
+
|
|
62
|
+
this.lastNavType = navType;
|
|
34
63
|
this.isPush = true;
|
|
35
64
|
this.currentRoute = finalPath;
|
|
36
65
|
if (typeof history !== "undefined") {
|
|
37
|
-
|
|
66
|
+
if (opts.replace) {
|
|
67
|
+
history.replaceState({}, "", finalPath);
|
|
68
|
+
} else {
|
|
69
|
+
history.pushState({}, "", finalPath);
|
|
70
|
+
}
|
|
38
71
|
}
|
|
39
72
|
}
|
|
40
73
|
|
|
@@ -57,14 +90,45 @@ export const router = new (class Router {
|
|
|
57
90
|
if (anchor.protocol !== "https:" && anchor.protocol !== "http:") return;
|
|
58
91
|
|
|
59
92
|
e.preventDefault();
|
|
60
|
-
this.navigate(anchor.pathname + anchor.search + anchor.hash);
|
|
93
|
+
this.navigate(anchor.pathname + anchor.search + anchor.hash, { source: "link" });
|
|
61
94
|
});
|
|
62
95
|
|
|
63
96
|
// Browser back/forward
|
|
64
97
|
window.addEventListener("popstate", () => {
|
|
65
|
-
|
|
66
|
-
this.currentRoute =
|
|
98
|
+
const finalPath =
|
|
67
99
|
window.location.pathname + window.location.search + window.location.hash;
|
|
100
|
+
// Fire beforeNavigate listeners; popstate can't be reliably cancelled
|
|
101
|
+
// (browser history already advanced), so we surface the event for
|
|
102
|
+
// observation only — `cancel()` is a no-op for this source.
|
|
103
|
+
const fromTarget = buildTarget(this.currentRoute);
|
|
104
|
+
const toTarget = buildTarget(finalPath);
|
|
105
|
+
const nav: Navigation = {
|
|
106
|
+
from: fromTarget,
|
|
107
|
+
to: toTarget,
|
|
108
|
+
type: "popstate",
|
|
109
|
+
willUnload: false,
|
|
110
|
+
cancel: () => {},
|
|
111
|
+
};
|
|
112
|
+
fireBeforeNavigate(nav);
|
|
113
|
+
|
|
114
|
+
this.lastNavType = "popstate";
|
|
115
|
+
this.isPush = false;
|
|
116
|
+
this.currentRoute = finalPath;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Full-page unload — fire beforeNavigate with willUnload=true so
|
|
120
|
+
// listeners can warn-on-leave (return value ignored; cancellation here
|
|
121
|
+
// requires `beforeunload`, not in scope).
|
|
122
|
+
window.addEventListener("beforeunload", () => {
|
|
123
|
+
const fromTarget = buildTarget(this.currentRoute);
|
|
124
|
+
const nav: Navigation = {
|
|
125
|
+
from: fromTarget,
|
|
126
|
+
to: null,
|
|
127
|
+
type: "link",
|
|
128
|
+
willUnload: true,
|
|
129
|
+
cancel: () => {},
|
|
130
|
+
};
|
|
131
|
+
fireBeforeNavigate(nav);
|
|
68
132
|
});
|
|
69
133
|
}
|
|
70
134
|
})();
|
package/src/core/html.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { existsSync, readFileSync } from "fs";
|
|
|
2
2
|
import { getDeclaredEnvKeys } from "./env.ts";
|
|
3
3
|
import { nonceAttr } from "./csp.ts";
|
|
4
4
|
import { OUT_DIR } from "./paths.ts";
|
|
5
|
+
import type { AppHtmlSegments } from "./appHtml.ts";
|
|
6
|
+
import { interpolateSegment } from "./appHtml.ts";
|
|
5
7
|
|
|
6
8
|
// ─── Dist Manifest ───────────────────────────────────────
|
|
7
9
|
// Maps hashed filenames → script/link tags.
|
|
@@ -98,6 +100,7 @@ export function buildHtml(
|
|
|
98
100
|
pageDeps: any = null,
|
|
99
101
|
layoutDeps: any[] | null = null,
|
|
100
102
|
bodyEndExtras?: string[],
|
|
103
|
+
segments?: AppHtmlSegments,
|
|
101
104
|
): string {
|
|
102
105
|
const cssLinks = (distManifest.css ?? [])
|
|
103
106
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
@@ -138,6 +141,31 @@ export function buildHtml(
|
|
|
138
141
|
|
|
139
142
|
const bodyEnd = bodyEndExtras?.length ? "\n " + bodyEndExtras.join("\n ") : "";
|
|
140
143
|
|
|
144
|
+
if (segments) {
|
|
145
|
+
const safeKey = safeLang(lang);
|
|
146
|
+
const headOpenInterpolated = interpolateSegment(segments.headOpen, {
|
|
147
|
+
lang: safeKey,
|
|
148
|
+
nonce,
|
|
149
|
+
});
|
|
150
|
+
const headCloseInterpolated = interpolateSegment(segments.headClose, { nonce });
|
|
151
|
+
const tailInterpolated = interpolateSegment(segments.tail, { nonce });
|
|
152
|
+
const faviconLine = segments.hasCustomFavicon
|
|
153
|
+
? ""
|
|
154
|
+
: ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n`;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
headOpenInterpolated +
|
|
158
|
+
`\n ${faviconLine}${cssLinks}\n` +
|
|
159
|
+
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
160
|
+
` <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
|
|
161
|
+
` ${fallbackTitle}${head}` +
|
|
162
|
+
headCloseInterpolated +
|
|
163
|
+
`\n${SPINNER}` +
|
|
164
|
+
`\n <div id="app">${body}</div>${scripts}${bodyEnd}` +
|
|
165
|
+
tailInterpolated
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
return `<!DOCTYPE html>
|
|
142
170
|
<html lang="${safeLang(lang)}">
|
|
143
171
|
<head>
|
|
@@ -161,12 +189,31 @@ export function buildHtml(
|
|
|
161
189
|
import type { Metadata } from "./hooks.ts";
|
|
162
190
|
|
|
163
191
|
/** Chunk 1: everything from <!DOCTYPE> through CSS/modulepreload links (head still open) */
|
|
164
|
-
export function buildHtmlShellOpen(
|
|
192
|
+
export function buildHtmlShellOpen(
|
|
193
|
+
lang?: string,
|
|
194
|
+
nonce?: string,
|
|
195
|
+
segments?: AppHtmlSegments,
|
|
196
|
+
): string {
|
|
165
197
|
const key = safeLang(lang);
|
|
166
198
|
const n = nonceAttr(nonce);
|
|
167
199
|
const cssLinks = (distManifest.css ?? [])
|
|
168
200
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
169
201
|
.join("\n ");
|
|
202
|
+
|
|
203
|
+
if (segments) {
|
|
204
|
+
const headOpenInterpolated = interpolateSegment(segments.headOpen, { lang: key, nonce });
|
|
205
|
+
const faviconLine = segments.hasCustomFavicon
|
|
206
|
+
? ""
|
|
207
|
+
: ` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n`;
|
|
208
|
+
return (
|
|
209
|
+
headOpenInterpolated +
|
|
210
|
+
`\n ${faviconLine}${cssLinks}\n` +
|
|
211
|
+
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
212
|
+
` <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
|
|
213
|
+
` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
170
217
|
return (
|
|
171
218
|
`<!DOCTYPE html>\n<html lang="${key}">\n<head>\n` +
|
|
172
219
|
` <meta charset="UTF-8">\n` +
|
|
@@ -188,7 +235,11 @@ const SPINNER =
|
|
|
188
235
|
`@keyframes __bs__{to{transform:rotate(360deg)}}</style><i></i></div>`;
|
|
189
236
|
|
|
190
237
|
/** Chunk 2: metadata tags + close </head> + open <body> + spinner */
|
|
191
|
-
export function buildMetadataChunk(
|
|
238
|
+
export function buildMetadataChunk(
|
|
239
|
+
metadata: Metadata | null,
|
|
240
|
+
headExtras?: string[],
|
|
241
|
+
segments?: AppHtmlSegments,
|
|
242
|
+
): string {
|
|
192
243
|
let out = "\n";
|
|
193
244
|
if (metadata) {
|
|
194
245
|
if (metadata.title) out += ` <title>${escapeHtml(metadata.title)}</title>\n`;
|
|
@@ -218,7 +269,14 @@ export function buildMetadataChunk(metadata: Metadata | null, headExtras?: strin
|
|
|
218
269
|
if (fragment) out += ` ${fragment}\n`;
|
|
219
270
|
}
|
|
220
271
|
}
|
|
221
|
-
|
|
272
|
+
|
|
273
|
+
if (segments) {
|
|
274
|
+
const headCloseInterpolated = interpolateSegment(segments.headClose, {});
|
|
275
|
+
out += headCloseInterpolated + `\n${SPINNER}`;
|
|
276
|
+
} else {
|
|
277
|
+
out += `</head>\n<body>\n${SPINNER}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
222
280
|
return out;
|
|
223
281
|
}
|
|
224
282
|
|
|
@@ -246,6 +304,7 @@ export function buildHtmlTail(
|
|
|
246
304
|
nonce?: string,
|
|
247
305
|
pageDeps: any = null,
|
|
248
306
|
layoutDeps: any[] | null = null,
|
|
307
|
+
segments?: AppHtmlSegments,
|
|
249
308
|
): string {
|
|
250
309
|
const n = nonceAttr(nonce);
|
|
251
310
|
let out = `<script${n}>document.getElementById('__bs__').remove()</script>`;
|
|
@@ -279,7 +338,14 @@ export function buildHtmlTail(
|
|
|
279
338
|
if (fragment) out += `\n${fragment}`;
|
|
280
339
|
}
|
|
281
340
|
}
|
|
282
|
-
|
|
341
|
+
|
|
342
|
+
if (segments) {
|
|
343
|
+
const tailInterpolated = interpolateSegment(segments.tail, { nonce });
|
|
344
|
+
out += `\n${tailInterpolated}`;
|
|
345
|
+
} else {
|
|
346
|
+
out += `\n</body>\n</html>`;
|
|
347
|
+
}
|
|
348
|
+
|
|
283
349
|
return out;
|
|
284
350
|
}
|
|
285
351
|
|
|
@@ -159,12 +159,14 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
let js = dev ? fixBindShadow(result.js.code) : result.js.code;
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// `+page
|
|
162
|
+
// Inject component <style> blocks via runtime JS, not CSS chunks.
|
|
163
|
+
// Bun's `splitting: true` names CSS chunks after the importing JS
|
|
164
|
+
// chunk's `[name]`, not the virtual module's uid — so when several
|
|
165
|
+
// routes (e.g. multiple `+page.svelte`) transitively import the same
|
|
166
|
+
// styled component, each route emits its own CSS sidecar named
|
|
167
|
+
// `+page-<contentHash>.css`. Identical content → identical hash →
|
|
168
|
+
// "Multiple files share the same output path". Runtime injection
|
|
169
|
+
// avoids CSS chunking entirely.
|
|
168
170
|
if (result.css?.code?.trim() && generate !== "server") {
|
|
169
171
|
const safeBase = basename(args.path).replace(/\./g, "_");
|
|
170
172
|
const uid = `${safeBase}-${fnv(args.path)}-style.css`;
|
|
@@ -184,7 +186,10 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
184
186
|
build.onLoad({ filter: /.*/, namespace: VIRTUAL_NS }, (args) => {
|
|
185
187
|
const css = virtualCss.get(args.path) ?? "";
|
|
186
188
|
virtualCss.delete(args.path);
|
|
187
|
-
|
|
189
|
+
const contents = css
|
|
190
|
+
? `(()=>{const s=document.createElement('style');s.textContent=${JSON.stringify(css)};document.head.appendChild(s);})();`
|
|
191
|
+
: "";
|
|
192
|
+
return { contents, loader: "js" };
|
|
188
193
|
});
|
|
189
194
|
},
|
|
190
195
|
};
|
package/src/core/renderer.ts
CHANGED
|
@@ -20,6 +20,8 @@ import type { Metadata } from "./hooks.ts";
|
|
|
20
20
|
import { loadPlugins } from "./config.ts";
|
|
21
21
|
import { reportDevErrorFromCatch } from "./devErrorReport.ts";
|
|
22
22
|
import type { BosiaPlugin, RenderContext } from "./types/plugin.ts";
|
|
23
|
+
import { getAppHtmlSegments } from "./appHtml.ts";
|
|
24
|
+
import type { AppHtmlSegments } from "./appHtml.ts";
|
|
23
25
|
|
|
24
26
|
// Plugins are loaded once per process at module init via top-level await elsewhere
|
|
25
27
|
// (server.ts), but renderer is also reachable from build/prerender contexts where
|
|
@@ -99,6 +101,11 @@ if (INTERNAL_HOSTS.size > 0) {
|
|
|
99
101
|
console.log(`🍪 Internal hosts (cookies forwarded): ${[...INTERNAL_HOSTS].join(", ")}`);
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
// ─── App HTML Template ───────────────────────────────────
|
|
105
|
+
// Required; loaded once per process; cached singleton with invalidation for HMR
|
|
106
|
+
|
|
107
|
+
const appHtmlSegments: AppHtmlSegments = getAppHtmlSegments();
|
|
108
|
+
|
|
102
109
|
// ─── Session-Aware Fetch ─────────────────────────────────
|
|
103
110
|
// Passed to load() functions so they can call internal APIs with the
|
|
104
111
|
// current user's cookies automatically forwarded. Cookie is attached
|
|
@@ -607,8 +614,8 @@ export async function renderSSRStream(
|
|
|
607
614
|
);
|
|
608
615
|
}
|
|
609
616
|
const html =
|
|
610
|
-
buildHtmlShellOpen(metadata?.lang, nonce) +
|
|
611
|
-
buildMetadataChunk(metadata, headExtras) +
|
|
617
|
+
buildHtmlShellOpen(metadata?.lang, nonce, appHtmlSegments) +
|
|
618
|
+
buildMetadataChunk(metadata, headExtras, appHtmlSegments) +
|
|
612
619
|
buildHtmlTail(
|
|
613
620
|
"",
|
|
614
621
|
"",
|
|
@@ -621,6 +628,7 @@ export async function renderSSRStream(
|
|
|
621
628
|
nonce,
|
|
622
629
|
data.pageDeps,
|
|
623
630
|
data.layoutDeps,
|
|
631
|
+
appHtmlSegments,
|
|
624
632
|
);
|
|
625
633
|
return new Response(html, {
|
|
626
634
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
@@ -659,8 +667,8 @@ export async function renderSSRStream(
|
|
|
659
667
|
|
|
660
668
|
// Pre-compute all chunks; pull-based stream gives Bun native backpressure.
|
|
661
669
|
const chunks: Uint8Array[] = [
|
|
662
|
-
enc.encode(buildHtmlShellOpen(metadata?.lang, nonce)),
|
|
663
|
-
enc.encode(buildMetadataChunk(metadata, headExtras)),
|
|
670
|
+
enc.encode(buildHtmlShellOpen(metadata?.lang, nonce, appHtmlSegments)),
|
|
671
|
+
enc.encode(buildMetadataChunk(metadata, headExtras, appHtmlSegments)),
|
|
664
672
|
enc.encode(
|
|
665
673
|
buildHtmlTail(
|
|
666
674
|
body,
|
|
@@ -674,6 +682,7 @@ export async function renderSSRStream(
|
|
|
674
682
|
nonce,
|
|
675
683
|
data.pageDeps,
|
|
676
684
|
data.layoutDeps,
|
|
685
|
+
appHtmlSegments,
|
|
677
686
|
),
|
|
678
687
|
),
|
|
679
688
|
];
|
|
@@ -781,6 +790,8 @@ export async function renderPageWithFormData(
|
|
|
781
790
|
nonce,
|
|
782
791
|
data.pageDeps,
|
|
783
792
|
data.layoutDeps,
|
|
793
|
+
undefined,
|
|
794
|
+
appHtmlSegments,
|
|
784
795
|
);
|
|
785
796
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
786
797
|
}
|
|
@@ -808,6 +819,8 @@ export async function renderPageWithFormData(
|
|
|
808
819
|
nonce,
|
|
809
820
|
data.pageDeps,
|
|
810
821
|
data.layoutDeps,
|
|
822
|
+
undefined,
|
|
823
|
+
appHtmlSegments,
|
|
811
824
|
);
|
|
812
825
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
813
826
|
}
|
|
@@ -887,6 +900,7 @@ export async function renderErrorPage(
|
|
|
887
900
|
null,
|
|
888
901
|
null,
|
|
889
902
|
bodyEndExtras,
|
|
903
|
+
appHtmlSegments,
|
|
890
904
|
);
|
|
891
905
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
892
906
|
} catch (err) {
|
|
@@ -924,6 +938,7 @@ export async function renderErrorPage(
|
|
|
924
938
|
null,
|
|
925
939
|
null,
|
|
926
940
|
bodyEndExtras,
|
|
941
|
+
appHtmlSegments,
|
|
927
942
|
);
|
|
928
943
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
929
944
|
} catch (err) {
|
package/src/lib/client.ts
CHANGED
|
@@ -10,4 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
export { enhance } from "../core/client/enhance.ts";
|
|
12
12
|
export type { SubmitFunction, ActionResult } from "../core/client/enhance.ts";
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
afterNavigate,
|
|
15
|
+
beforeNavigate,
|
|
16
|
+
goto,
|
|
17
|
+
invalidate,
|
|
18
|
+
invalidateAll,
|
|
19
|
+
} from "../core/client/navigation.ts";
|
|
20
|
+
export type { GotoOptions, Navigation, NavigationTarget } from "../core/client/navigation.ts";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Top block -->
|
|
3
|
+
<rect fill="#f0f0f0" x="50" y="50" width="28" height="28" rx="6"/>
|
|
4
|
+
<rect fill="#f0f0f0" x="86" y="50" width="60" height="28" rx="6"/>
|
|
5
|
+
|
|
6
|
+
<!-- Middle block -->
|
|
7
|
+
<rect fill="#f0f0f0" x="86" y="86" width="72" height="28" rx="6"/>
|
|
8
|
+
|
|
9
|
+
<!-- Bottom block -->
|
|
10
|
+
<rect fill="#f0f0f0" x="86" y="122" width="60" height="28" rx="6"/>
|
|
11
|
+
|
|
12
|
+
<!-- Connector bar on left -->
|
|
13
|
+
<rect fill="#f0f0f0" x="50" y="50" width="28" height="100" rx="6"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Top block -->
|
|
3
|
+
<rect fill="#1a1a1a" x="50" y="50" width="28" height="28" rx="6"/>
|
|
4
|
+
<rect fill="#1a1a1a" x="86" y="50" width="60" height="28" rx="6"/>
|
|
5
|
+
|
|
6
|
+
<!-- Middle block -->
|
|
7
|
+
<rect fill="#1a1a1a" x="86" y="86" width="72" height="28" rx="6"/>
|
|
8
|
+
|
|
9
|
+
<!-- Bottom block -->
|
|
10
|
+
<rect fill="#1a1a1a" x="86" y="122" width="60" height="28" rx="6"/>
|
|
11
|
+
|
|
12
|
+
<!-- Connector bar on left -->
|
|
13
|
+
<rect fill="#1a1a1a" x="50" y="50" width="28" height="100" rx="6"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Top block -->
|
|
3
|
+
<rect fill="#f0f0f0" x="50" y="50" width="28" height="28" rx="6"/>
|
|
4
|
+
<rect fill="#f0f0f0" x="86" y="50" width="60" height="28" rx="6"/>
|
|
5
|
+
|
|
6
|
+
<!-- Middle block -->
|
|
7
|
+
<rect fill="#f0f0f0" x="86" y="86" width="72" height="28" rx="6"/>
|
|
8
|
+
|
|
9
|
+
<!-- Bottom block -->
|
|
10
|
+
<rect fill="#f0f0f0" x="86" y="122" width="60" height="28" rx="6"/>
|
|
11
|
+
|
|
12
|
+
<!-- Connector bar on left -->
|
|
13
|
+
<rect fill="#f0f0f0" x="50" y="50" width="28" height="100" rx="6"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Top block -->
|
|
3
|
+
<rect fill="#1a1a1a" x="50" y="50" width="28" height="28" rx="6"/>
|
|
4
|
+
<rect fill="#1a1a1a" x="86" y="50" width="60" height="28" rx="6"/>
|
|
5
|
+
|
|
6
|
+
<!-- Middle block -->
|
|
7
|
+
<rect fill="#1a1a1a" x="86" y="86" width="72" height="28" rx="6"/>
|
|
8
|
+
|
|
9
|
+
<!-- Bottom block -->
|
|
10
|
+
<rect fill="#1a1a1a" x="86" y="122" width="60" height="28" rx="6"/>
|
|
11
|
+
|
|
12
|
+
<!-- Connector bar on left -->
|
|
13
|
+
<rect fill="#1a1a1a" x="50" y="50" width="28" height="100" rx="6"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Top block -->
|
|
3
|
+
<rect fill="#f0f0f0" x="50" y="50" width="28" height="28" rx="6"/>
|
|
4
|
+
<rect fill="#f0f0f0" x="86" y="50" width="60" height="28" rx="6"/>
|
|
5
|
+
|
|
6
|
+
<!-- Middle block -->
|
|
7
|
+
<rect fill="#f0f0f0" x="86" y="86" width="72" height="28" rx="6"/>
|
|
8
|
+
|
|
9
|
+
<!-- Bottom block -->
|
|
10
|
+
<rect fill="#f0f0f0" x="86" y="122" width="60" height="28" rx="6"/>
|
|
11
|
+
|
|
12
|
+
<!-- Connector bar on left -->
|
|
13
|
+
<rect fill="#f0f0f0" x="50" y="50" width="28" height="100" rx="6"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Top block -->
|
|
3
|
+
<rect fill="#1a1a1a" x="50" y="50" width="28" height="28" rx="6"/>
|
|
4
|
+
<rect fill="#1a1a1a" x="86" y="50" width="60" height="28" rx="6"/>
|
|
5
|
+
|
|
6
|
+
<!-- Middle block -->
|
|
7
|
+
<rect fill="#1a1a1a" x="86" y="86" width="72" height="28" rx="6"/>
|
|
8
|
+
|
|
9
|
+
<!-- Bottom block -->
|
|
10
|
+
<rect fill="#1a1a1a" x="86" y="122" width="60" height="28" rx="6"/>
|
|
11
|
+
|
|
12
|
+
<!-- Connector bar on left -->
|
|
13
|
+
<rect fill="#1a1a1a" x="50" y="50" width="28" height="100" rx="6"/>
|
|
14
|
+
</svg>
|