bosia 1.1.4 → 1.1.6
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/ambient.d.ts +3 -0
- package/src/cli/add.ts +15 -12
- package/src/cli/block.ts +18 -16
- package/src/cli/feat.ts +31 -11
- package/src/cli/page.ts +18 -16
- package/src/cli/registry.ts +117 -20
- package/src/cli/theme.ts +7 -2
- package/src/core/backend.ts +8 -1
- package/src/core/build.ts +7 -2
- package/src/core/cache.ts +31 -6
- package/src/core/client/App.svelte +15 -26
- package/src/core/client/page.svelte.ts +15 -0
- package/src/core/client/prefetch.ts +55 -60
- package/src/core/dev.ts +11 -1
- package/src/core/etag.ts +13 -0
- package/src/core/hooks.ts +17 -0
- package/src/core/html.ts +109 -14
- package/src/core/plugins/inspector/bun-plugin.ts +31 -18
- package/src/core/renderer.ts +111 -79
- package/src/core/routeFile.ts +36 -20
- package/src/core/scanner.ts +2 -0
- package/src/core/server.ts +312 -103
- package/src/core/staticManifest.ts +60 -15
- package/src/core/svelteCompiler.ts +96 -27
- package/src/core/types.ts +4 -0
- package/templates/shop/.env.example +3 -0
- package/templates/shop/src/hooks.server.ts +12 -2
- package/templates/shop/src/routes/(private)/+layout.server.ts +2 -6
- package/templates/store/.env.example +3 -0
- package/templates/store/src/hooks.server.ts +12 -2
- package/templates/store/src/routes/(private)/+layout.server.ts +2 -6
|
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, statSync } from "fs";
|
|
|
2
2
|
import { basename, join, resolve as resolvePath } from "path";
|
|
3
3
|
import { PRECOMPRESSED, pickEncoding } from "./html.ts";
|
|
4
4
|
import { AssetCache, assetCache } from "./assetCache.ts";
|
|
5
|
+
import { matchesEtag } from "./etag.ts";
|
|
5
6
|
|
|
6
7
|
/** `br`/`gz`: absolute paths of build-time precompressed siblings (precompress.ts). */
|
|
7
8
|
export type StaticEntry = { absPath: string; cacheControl?: string; br?: string; gz?: string };
|
|
@@ -11,10 +12,63 @@ const HASHED_BASENAME = /\-[a-z0-9]{8,}\.[a-z]+$/;
|
|
|
11
12
|
const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
12
13
|
const DEFAULT_CACHE = "no-cache";
|
|
13
14
|
|
|
14
|
-
// Files/dirs at OUT_DIR root that
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// Files/dirs at OUT_DIR root that must never be served — server code, build
|
|
16
|
+
// metadata, or re-merges already covered by the per-root walks. `server/` and
|
|
17
|
+
// `worker/` are server bundles; `hooks.server.js` and `bosia.config.js` are the
|
|
18
|
+
// user's own server code bundled for the runtime (with their dev `.map`s).
|
|
19
|
+
const OUT_DIR_SKIP_DIRS = new Set(["client", "static", "prerendered", "server", "worker"]);
|
|
20
|
+
const OUT_DIR_SKIP_FILES = new Set([
|
|
21
|
+
"manifest.json",
|
|
22
|
+
"route-manifest.json",
|
|
23
|
+
"app-html.json",
|
|
24
|
+
"svelte-maps.json",
|
|
25
|
+
"hooks.server.js",
|
|
26
|
+
"hooks.server.js.map",
|
|
27
|
+
"bosia.config.js",
|
|
28
|
+
"bosia.config.js.map",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether `rel` (a path relative to OUT_DIR, `/`-separated, no leading slash)
|
|
33
|
+
* is build output the server must not hand to a browser. Shared by the prod
|
|
34
|
+
* manifest and the dev per-request fallthrough so both refuse the same files.
|
|
35
|
+
*/
|
|
36
|
+
export function isPrivateOutPath(rel: string): boolean {
|
|
37
|
+
const slash = rel.indexOf("/");
|
|
38
|
+
return slash === -1 ? OUT_DIR_SKIP_FILES.has(rel) : OUT_DIR_SKIP_DIRS.has(rel.slice(0, slash));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Dev-mode lookup of a user file at the OUT_DIR root (the prod manifest covers
|
|
43
|
+
* this in production). `urlPath` is the decoded request path. Each segment must
|
|
44
|
+
* equal, exactly, a name `readdir` returns, and the joined real names must not
|
|
45
|
+
* be private. Matching the filesystem's own names instead of comparing request
|
|
46
|
+
* strings means no other spelling of a private file can get through: a
|
|
47
|
+
* different case, a trailing dot or space (Windows), an NTFS `::$DATA` stream,
|
|
48
|
+
* an 8.3 short name, or a Unicode case-folding twin like `ſerver` (macOS).
|
|
49
|
+
* Returns the absolute path, or null.
|
|
50
|
+
*/
|
|
51
|
+
export function findOutDirFile(outDir: string, urlPath: string): string | null {
|
|
52
|
+
const segments = urlPath.split("/").filter(Boolean);
|
|
53
|
+
if (segments.length === 0) return null;
|
|
54
|
+
if (isPrivateOutPath(segments.join("/"))) return null;
|
|
55
|
+
let dir = resolvePath(outDir);
|
|
56
|
+
for (let i = 0; i < segments.length; i++) {
|
|
57
|
+
let names: string[];
|
|
58
|
+
try {
|
|
59
|
+
names = readdirSync(dir);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (!names.includes(segments[i]!)) return null;
|
|
64
|
+
dir = join(dir, segments[i]!);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return statSync(dir).isFile() ? dir : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
18
72
|
|
|
19
73
|
const RESERVED_PREFIX = "/__bosia/";
|
|
20
74
|
|
|
@@ -94,13 +148,13 @@ export function buildStaticManifest(outDir: string): StaticManifest {
|
|
|
94
148
|
}
|
|
95
149
|
for (const ent of rootEntries) {
|
|
96
150
|
if (ent.isDirectory()) {
|
|
97
|
-
if (
|
|
151
|
+
if (isPrivateOutPath(`${ent.name}/`)) continue;
|
|
98
152
|
const sub = join(outAbs, ent.name);
|
|
99
153
|
for (const { abs, rel } of walk(sub, ent.name)) {
|
|
100
154
|
addOnce(manifest, `/${rel}`, { absPath: abs });
|
|
101
155
|
}
|
|
102
156
|
} else if (ent.isFile()) {
|
|
103
|
-
if (
|
|
157
|
+
if (isPrivateOutPath(ent.name)) continue;
|
|
104
158
|
addOnce(manifest, `/${ent.name}`, { absPath: join(outAbs, ent.name) });
|
|
105
159
|
}
|
|
106
160
|
}
|
|
@@ -215,15 +269,6 @@ function etagFor(path: string, encoding: string | undefined): string | null {
|
|
|
215
269
|
return tag;
|
|
216
270
|
}
|
|
217
271
|
|
|
218
|
-
function matchesEtag(header: string | null, etag: string): boolean {
|
|
219
|
-
if (!header) return false;
|
|
220
|
-
for (const raw of header.split(",")) {
|
|
221
|
-
const t = raw.trim();
|
|
222
|
-
if (t === "*" || t === etag || (t.startsWith("W/") && t.slice(2) === etag)) return true;
|
|
223
|
-
}
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
272
|
export function lookupStatic(manifest: StaticManifest, urlPath: string): StaticEntry | null {
|
|
228
273
|
const raw = urlPath.split("?")[0];
|
|
229
274
|
// Manifest keys are raw filenames; URLs arrive percent-encoded.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { compile, compileModule, parse, VERSION as SVELTE_VERSION } from "svelte/compiler";
|
|
2
4
|
import type { BunPlugin } from "bun";
|
|
3
5
|
|
|
4
6
|
import { auditSvelteSource } from "./svelteAudit.ts";
|
|
@@ -95,6 +97,56 @@ export function rebaseSvelteMarkup(source: string): string {
|
|
|
95
97
|
);
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
// ─── Dev compile cache ───────────────────────────────────
|
|
101
|
+
// Every dev rebuild is a fresh `bun run build.ts`, so each one used to compile
|
|
102
|
+
// every component twice (client + server) from scratch — most of the bundle
|
|
103
|
+
// step. `bosia dev` points BOSIA_SVELTE_CACHE_DIR at a directory it clears on
|
|
104
|
+
// start; a component whose source and compile inputs are unchanged reuses the
|
|
105
|
+
// stored output. Production builds never set it.
|
|
106
|
+
//
|
|
107
|
+
// The import audit still runs on every build: it reads other files' exports,
|
|
108
|
+
// so its verdict can change while this file doesn't. A hit re-parses the
|
|
109
|
+
// source for it (~10x cheaper than a compile) and replays stored warnings.
|
|
110
|
+
|
|
111
|
+
// Bump when the cached shape or anything feeding `compile()` below changes.
|
|
112
|
+
const COMPILE_CACHE_VERSION = 1;
|
|
113
|
+
|
|
114
|
+
type CompileWarning = { code: string; message: string; start?: { line: number; column: number } };
|
|
115
|
+
type CachedCompile = { code: string; map: unknown; css: string | null; warnings: CompileWarning[] };
|
|
116
|
+
|
|
117
|
+
let madeDir: string | null = null;
|
|
118
|
+
function compileCacheDir(): string | null {
|
|
119
|
+
const dir = process.env.BOSIA_SVELTE_CACHE_DIR || null;
|
|
120
|
+
if (dir && dir !== madeDir) {
|
|
121
|
+
mkdirSync(dir, { recursive: true });
|
|
122
|
+
madeDir = dir;
|
|
123
|
+
}
|
|
124
|
+
return dir;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* `build()`'s result, from the dev compile cache when `key` was seen before in
|
|
129
|
+
* this `bosia dev` session. Without a cache dir (every non-dev build) it just
|
|
130
|
+
* calls `build()`. `key` must cover every input of the compile; the Svelte
|
|
131
|
+
* version and COMPILE_CACHE_VERSION are added here. The value must be JSON.
|
|
132
|
+
*/
|
|
133
|
+
export async function cachedCompile<T>(
|
|
134
|
+
key: unknown[],
|
|
135
|
+
build: () => T,
|
|
136
|
+
): Promise<{ value: T; hit: boolean }> {
|
|
137
|
+
const dir = compileCacheDir();
|
|
138
|
+
if (!dir) return { value: build(), hit: false };
|
|
139
|
+
const hash = Bun.hash(JSON.stringify([COMPILE_CACHE_VERSION, SVELTE_VERSION, ...key]));
|
|
140
|
+
const path = join(dir, `${hash.toString(36)}.json`);
|
|
141
|
+
try {
|
|
142
|
+
return { value: (await Bun.file(path).json()) as T, hit: true };
|
|
143
|
+
} catch {
|
|
144
|
+
const value = build();
|
|
145
|
+
await Bun.write(path, JSON.stringify(value));
|
|
146
|
+
return { value, hit: false };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
98
150
|
export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
|
|
99
151
|
const generate = target === "browser" ? "client" : "server";
|
|
100
152
|
const dev = process.env.NODE_ENV !== "production";
|
|
@@ -109,41 +161,59 @@ export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
|
|
|
109
161
|
|
|
110
162
|
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
|
|
111
163
|
const source = await Bun.file(args.path).text();
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
164
|
+
const rebased = rebaseSvelteMarkup(source);
|
|
165
|
+
// Set on a fresh compile; a cache hit re-parses for the audit.
|
|
166
|
+
let ast: unknown = undefined;
|
|
167
|
+
const { value: compiled } = await cachedCompile<CachedCompile>(
|
|
168
|
+
["bosia", args.path, generate, dev, rebased],
|
|
169
|
+
() => {
|
|
170
|
+
const result = compile(rebased, {
|
|
171
|
+
generate,
|
|
172
|
+
// External on both targets. The browser used to get "injected",
|
|
173
|
+
// which put every scoped rule inside the JS bundle — so an
|
|
174
|
+
// SSR'd page painted before its own layout CSS existed and
|
|
175
|
+
// snapped into place at hydration. `collectComponentCss` below
|
|
176
|
+
// gathers the rules into one stylesheet the head can link.
|
|
177
|
+
css: "external",
|
|
178
|
+
dev,
|
|
179
|
+
hmr: false,
|
|
180
|
+
cssHash: ({ css }) => `svelte-${svelteHash(css)}`,
|
|
181
|
+
filename: args.path,
|
|
182
|
+
// Modern AST shape (Svelte 5.x) — `fragment`, `instance`, `module`
|
|
183
|
+
// rather than the legacy `html`. The audit walker assumes modern.
|
|
184
|
+
modernAst: true,
|
|
185
|
+
});
|
|
186
|
+
ast = (result as unknown as { ast?: unknown }).ast;
|
|
187
|
+
return {
|
|
188
|
+
code: result.js.code,
|
|
189
|
+
map: typeof result.js.map === "string" ? JSON.parse(result.js.map) : result.js.map,
|
|
190
|
+
css: result.css?.code ?? null,
|
|
191
|
+
warnings: (result.warnings ?? []).map((w) => ({
|
|
192
|
+
code: w.code,
|
|
193
|
+
message: w.message,
|
|
194
|
+
start: w.start ? { line: w.start.line, column: w.start.column } : undefined,
|
|
195
|
+
})),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
);
|
|
128
199
|
// Browser only: both plugin instances share module state and the
|
|
129
200
|
// client and server builds run concurrently, so collecting from
|
|
130
201
|
// each would emit every rule twice.
|
|
131
|
-
if (target === "browser" &&
|
|
132
|
-
collectComponentCss(args.path,
|
|
202
|
+
if (target === "browser" && compiled.css) {
|
|
203
|
+
collectComponentCss(args.path, compiled.css);
|
|
133
204
|
}
|
|
134
205
|
const existing = auditInflight.get(args.path);
|
|
135
206
|
if (existing) {
|
|
136
207
|
await existing;
|
|
137
208
|
} else {
|
|
209
|
+
const warnings = compiled.warnings;
|
|
138
210
|
const promise = (async () => {
|
|
139
211
|
const strict = await getStrictImportsOption();
|
|
140
212
|
const failure = await auditSvelteSource({
|
|
141
213
|
source,
|
|
142
214
|
filename: args.path,
|
|
143
|
-
ast:
|
|
144
|
-
warnings:
|
|
145
|
-
typeof auditSvelteSource
|
|
146
|
-
>[0]["warnings"],
|
|
215
|
+
ast: ast ?? parse(rebased, { modern: true }),
|
|
216
|
+
warnings: warnings as unknown as Parameters<typeof auditSvelteSource>[0]["warnings"],
|
|
147
217
|
cwd: process.cwd(),
|
|
148
218
|
exportCache: auditExportCache,
|
|
149
219
|
strict,
|
|
@@ -157,11 +227,10 @@ export function makeBosiaSvelteCompiler(target: "browser" | "bun"): BunPlugin {
|
|
|
157
227
|
// resolver — browser-side stack frames are what we need to translate.
|
|
158
228
|
// Server (Bun) compile output has different line numbers and would
|
|
159
229
|
// clobber the client entry under the same cache key.
|
|
160
|
-
if (dev && target === "browser" &&
|
|
161
|
-
|
|
162
|
-
svelteMapCache.set(args.path, m);
|
|
230
|
+
if (dev && target === "browser" && compiled.map) {
|
|
231
|
+
svelteMapCache.set(args.path, compiled.map);
|
|
163
232
|
}
|
|
164
|
-
const contents = dev ? fixBindShadow(
|
|
233
|
+
const contents = dev ? fixBindShadow(compiled.code) : compiled.code;
|
|
165
234
|
return { contents, loader: "ts" };
|
|
166
235
|
});
|
|
167
236
|
|
package/src/core/types.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type TrailingSlash = "never" | "always" | "ignore";
|
|
|
7
7
|
export interface PageRoute {
|
|
8
8
|
/** URL pattern, e.g. "/" or "/blog/[slug]" or "/[...rest]" */
|
|
9
9
|
pattern: string;
|
|
10
|
+
/** Folder path under src/routes with groups kept, e.g. "/(private)/blog/[slug]" */
|
|
11
|
+
id: string;
|
|
10
12
|
/** Path to +page.svelte, relative to src/routes/ */
|
|
11
13
|
page: string;
|
|
12
14
|
/** Chain of +layout.svelte paths root → leaf, relative to src/routes/ */
|
|
@@ -42,6 +44,8 @@ export interface PageRoute {
|
|
|
42
44
|
export interface ApiRoute {
|
|
43
45
|
/** URL pattern, e.g. "/api/hello" or "/api/users/[id]" */
|
|
44
46
|
pattern: string;
|
|
47
|
+
/** Folder path under src/routes with groups kept, e.g. "/api/(admin)/users" */
|
|
48
|
+
id: string;
|
|
45
49
|
/** Path to +server.ts, relative to src/routes/ */
|
|
46
50
|
server: string;
|
|
47
51
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
DATABASE_URL=sqlite://./data/app.db
|
|
2
2
|
|
|
3
3
|
SESSION_SECRET=change-me-in-production
|
|
4
|
+
# The response cache tells users apart by these cookies. Keep bosia_session
|
|
5
|
+
# (the sign-in cookie) here, or one user's page can be served to another.
|
|
6
|
+
CACHE_KEYS=session,sid,auth,token,jwt,Authorization,bosia_session
|
|
4
7
|
|
|
5
8
|
STORAGE_DRIVER=s3
|
|
6
9
|
S3_BUCKET=
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sequence } from "bosia";
|
|
1
|
+
import { redirect, sequence } from "bosia";
|
|
2
2
|
import type { Handle } from "bosia";
|
|
3
3
|
import { db } from "./features/drizzle";
|
|
4
4
|
import { authHandle } from "./features/auth";
|
|
@@ -8,6 +8,16 @@ const dbHandle: Handle = async ({ event, resolve }) => {
|
|
|
8
8
|
return resolve(event);
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// Every page under (private) needs a signed-in user. Checked here, by route id,
|
|
12
|
+
// before metadata() or any loader runs — on page loads, client navigations and forms.
|
|
13
|
+
const guardHandle: Handle = async ({ event, resolve }) => {
|
|
14
|
+
if (event.route.id?.startsWith("/(private)") && !event.locals.user) {
|
|
15
|
+
const next = encodeURIComponent(event.url.pathname + event.url.search);
|
|
16
|
+
throw redirect(303, `/login?next=${next}`);
|
|
17
|
+
}
|
|
18
|
+
return resolve(event);
|
|
19
|
+
};
|
|
20
|
+
|
|
11
21
|
const loggingHandle: Handle = async ({ event, resolve }) => {
|
|
12
22
|
const start = Date.now();
|
|
13
23
|
event.locals.requestTime = start;
|
|
@@ -18,4 +28,4 @@ const loggingHandle: Handle = async ({ event, resolve }) => {
|
|
|
18
28
|
return res;
|
|
19
29
|
};
|
|
20
30
|
|
|
21
|
-
export const handle = sequence(dbHandle, authHandle, loggingHandle);
|
|
31
|
+
export const handle = sequence(dbHandle, authHandle, guardHandle, loggingHandle);
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { redirect } from "bosia";
|
|
2
1
|
import type { LoadEvent } from "bosia";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const next = encodeURIComponent(url.pathname + url.search);
|
|
7
|
-
throw redirect(303, `/login?next=${next}`);
|
|
8
|
-
}
|
|
3
|
+
// Sign-in is checked in hooks.server.ts, before this loader runs.
|
|
4
|
+
export async function load({ locals }: LoadEvent) {
|
|
9
5
|
return { user: locals.user };
|
|
10
6
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/store
|
|
2
2
|
|
|
3
3
|
SESSION_SECRET=change-me-in-production
|
|
4
|
+
# The response cache tells users apart by these cookies. Keep bosia_session
|
|
5
|
+
# (the sign-in cookie) here, or one user's page can be served to another.
|
|
6
|
+
CACHE_KEYS=session,sid,auth,token,jwt,Authorization,bosia_session
|
|
4
7
|
|
|
5
8
|
STORAGE_DRIVER=s3
|
|
6
9
|
S3_BUCKET=uploads
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sequence } from "bosia";
|
|
1
|
+
import { redirect, sequence } from "bosia";
|
|
2
2
|
import type { Handle } from "bosia";
|
|
3
3
|
import { db } from "./features/drizzle";
|
|
4
4
|
import { authHandle } from "./features/auth";
|
|
@@ -8,6 +8,16 @@ const dbHandle: Handle = async ({ event, resolve }) => {
|
|
|
8
8
|
return resolve(event);
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// Every page under (private) needs a signed-in user. Checked here, by route id,
|
|
12
|
+
// before metadata() or any loader runs — on page loads, client navigations and forms.
|
|
13
|
+
const guardHandle: Handle = async ({ event, resolve }) => {
|
|
14
|
+
if (event.route.id?.startsWith("/(private)") && !event.locals.user) {
|
|
15
|
+
const next = encodeURIComponent(event.url.pathname + event.url.search);
|
|
16
|
+
throw redirect(303, `/login?next=${next}`);
|
|
17
|
+
}
|
|
18
|
+
return resolve(event);
|
|
19
|
+
};
|
|
20
|
+
|
|
11
21
|
const loggingHandle: Handle = async ({ event, resolve }) => {
|
|
12
22
|
const start = Date.now();
|
|
13
23
|
event.locals.requestTime = start;
|
|
@@ -18,4 +28,4 @@ const loggingHandle: Handle = async ({ event, resolve }) => {
|
|
|
18
28
|
return res;
|
|
19
29
|
};
|
|
20
30
|
|
|
21
|
-
export const handle = sequence(dbHandle, authHandle, loggingHandle);
|
|
31
|
+
export const handle = sequence(dbHandle, authHandle, guardHandle, loggingHandle);
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { redirect } from "bosia";
|
|
2
1
|
import type { LoadEvent } from "bosia";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const next = encodeURIComponent(url.pathname + url.search);
|
|
7
|
-
throw redirect(303, `/login?next=${next}`);
|
|
8
|
-
}
|
|
3
|
+
// Sign-in is checked in hooks.server.ts, before this loader runs.
|
|
4
|
+
export async function load({ locals }: LoadEvent) {
|
|
9
5
|
return { user: locals.user };
|
|
10
6
|
}
|