@sigil-dev/grimoire
v0.9.2
Published
Full stack web framework using Sigil
Readme
@sigil-dev/grimoire
Full-stack server framework for Sigil.
Heavily inspired by SvelteKit. Most of its documentation applies here
Quick start
bun i -g @sigil-dev/cli
sigil createRoute files
Place files in /src/routes.
| File | Purpose |
|---|---|
| +page.tsx | index.tsx | routeName.tsx | Page |
| +page.server.ts | Server data passed to page / form actions |
| +layout.tsx | Page layout |
| +layout.server.ts | Layout-level load |
| +server.ts | API route (raw Request → Response) |
| +error.tsx | Error boundary page |
| hooks.index.ts | Global request middleware (project root) |
/routes/about/index.tsx, /routes/about/+page.tsx and /routes/about.tsx all work and all do the same thing.
For path parameters use square brackets. src/routes/posts/[slug]/+page.tsx → /posts/:slug.
Loading data
// src/routes/posts/[slug]/+page.server.ts
import { error } from "@sigil-dev/grimoire";
import type { TypedLoadContext } from "@sigil-dev/grimoire";
export async function load({ params }: TypedLoadContext<{ slug: string }>) {
const post = await db.posts.findBySlug(params.slug);
if (!post) throw error(404, "Post not found");
return { post }; // → page receives { data: { post }, params }
}The return value is passed as the data prop to the matching +page.tsx component.
Form actions
Export HTTP method handlers alongside load:
// src/routes/login/+page.server.ts
import { fail, redirect } from "@sigil-dev/grimoire";
export async function POST({ request }) {
const data = await request.formData();
const user = await auth.login(data.get("email"), data.get("password"));
if (!user) return fail(400, { error: "Invalid credentials" });
throw redirect(303, "/dashboard");
}Use enhance with the use prop on the client for fetch-based submission without a full page reload:
This is jank and will likely be reworked.
// src/routes/login/+page.tsx
import { enhance } from "@sigil-dev/grimoire/client";
export default function LoginPage() {
const errors = $state({});
return (
<form use={[enhance, { onFail: (data) => errors.set(data) }]}>
<input name="email" />
<input name="password" type="password" />
<button>Log in</button>
</form>
);
}Error handling
error(status, message) can be thrown from any load function or action. Grimoire renders the closest +error.tsx if one exists, otherwise falls back to a plain-text response.
// src/routes/+error.tsx
export default function ErrorPage({ status, message }: { status: number; message: string }) {
return (
<html>
<body>
<h1>{status}</h1>
<p>{message}</p>
</body>
</html>
);
}API routes
// src/routes/api/items/+server.ts
export async function GET({ url }) {
const items = await db.items.list();
return Response.json(items);
}
export async function POST({ request }) {
const body = await request.json();
const item = await db.items.create(body);
return Response.json(item, { status: 201 });
}Layouts
// src/routes/+layout.tsx
import { Head } from "@sigil-dev/grimoire";
export default function Layout({ children }) {
return (
<html>
<Head><title>My App</title></Head>
<body>{children}</body>
</html>
);
}Hooks (middleware)
// hooks.index.ts (project root)
import type { Handle } from "@sigil-dev/grimoire/hooks";
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await getUser(event.request);
return resolve(event);
};Configuration
import { defineConfig } from "@sigil-dev/grimoire";
export default defineConfig({
port: 3000,
host: "localhost",
routes: "src/routes", // default
plugins: [],
});Helpers
| Import | Description |
|---|---|
| error(status, message) | Throw an HTTP error from load or an action |
| fail(status, data) | Return validation errors from an action |
| redirect(status, location) | Throw a redirect from load or an action |
| sequence(...handles) | Compose multiple Handle middleware functions |
Security headers
import { securityHeaders } from "@sigil-dev/grimoire/headers";
// use as a Handle in hooks.index.ts, or compose with sequence()securityHeaders() returns a Handle that sets CSP, HSTS, X-Frame-Options, and related headers on every response.
