Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions osmium/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@solid-primitives/event-listener": "^2.4.6",
"@solid-primitives/marker": "^0.2.2",
"@solid-primitives/platform": "^0.2.1",
"@solid-primitives/storage": "^4.4.0",
"@solidjs/router": "1.0.0",
"@tailwindcss/typography": "^0.5.19",
"solid-heroicons": "^3.2.4",
Expand Down
65 changes: 51 additions & 14 deletions osmium/src/mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import {
For,
type JSX,
Match,
type ParentProps,
Switch,
children,
createMemo,
createSignal,
splitProps,
} from "solid-js";
import { isServer } from "solid-js/web";
import {
cookieStorage,
makePersisted,
messageSync,
} from "@solid-primitives/storage";

import { clientOnly } from "@solidjs/start";
import { Callout } from "./ui/callout";
Expand Down Expand Up @@ -44,25 +51,55 @@ export const DirectiveContainer = (
>
<Match when={props.type === "tab"}>{_children}</Match>
<Match when={props.type === "tab-group"}>
<Tabs>
<TabList>
<For each={tabNames()}>
{(title) => <Tab value={title}>{title}</Tab>}
</For>
</TabList>
<For each={tabNames()}>
{(title, idx) => (
<TabPanel value={title} forceMount={true}>
{_children[idx()]}
</TabPanel>
)}
</For>
</Tabs>
<TabGroup
syncKey={props.title}
tabNames={tabNames()}
panels={_children}
/>
</Match>
</Switch>
);
};

const TabGroup = (props: {
syncKey?: string;
tabNames: string[];
panels: JSX.Element[];
}) => {
const tabs = (
value?: () => string | undefined,
onChange?: (value: string) => void
) => (
<Tabs value={value?.()} onChange={onChange}>
<TabList>
<For each={props.tabNames}>
{(title) => <Tab value={title}>{title}</Tab>}
</For>
</TabList>
<For each={props.tabNames}>
{(title, idx) => (
<TabPanel value={title} forceMount={true}>
{props.panels[idx()]}
</TabPanel>
)}
</For>
</Tabs>
);

if (!props.syncKey) return tabs();

// Groups sharing a sync key select together across the page and tabs,
// and the choice persists between visits.
const [openTab, setOpenTab] = makePersisted(createSignal(props.tabNames[0]), {
name: `tab-group:${props.syncKey}`,
sync: messageSync(new BroadcastChannel("tab-group")),
storage: cookieStorage.withOptions({
expires: new Date(Date.now() + 3e10),
}),
});
return tabs(openTab, setOpenTab);
};

export const strong = (props: ParentProps) => (
<b class="font-semibold">{props.children}</b>
);
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 101 additions & 0 deletions src/routes/(3)building-apps/(3)server-functions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,31 @@ Leave writes on the default `POST` transport:
The mutation examples assume authentication middleware has populated typed `userId` and `isAdmin` fields on `event.locals`.
See [Sessions and authentication](/building-apps/sessions-and-auth) for the request-scoped pattern.

::::tab-group[validation-library]

:::tab[Valibot]

```ts
import { getRequestEvent, redirect } from "@solidjs/web";
import * as v from "valibot";

const UserName = v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(100));

export async function updateCurrentUser(form: FormData) {
"use server";
const event = getRequestEvent();
const userId = event?.locals.userId;
if (!userId) throw redirect("/sign-in");

const name = v.parse(UserName, form.get("name"));
await database.users.update(userId, { name });
}
```

:::

:::tab[Zod]

```ts
import { getRequestEvent, redirect } from "@solidjs/web";
import { z } from "zod";
Expand All @@ -104,6 +129,10 @@ export async function updateCurrentUser(form: FormData) {
}
```

:::

::::

A server function is a transport primitive and does not require a router or data cache.

## The request event
Expand Down Expand Up @@ -149,6 +178,38 @@ The client reference resolves to the decoded value.

Use `respond` when a value also needs status, headers, or revalidation metadata:

::::tab-group[validation-library]

:::tab[Valibot]

```ts
import { getRequestEvent, respond } from "@solidjs/web";
import * as v from "valibot";

const CreateUser = v.object({
name: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(100)),
email: v.pipe(v.string(), v.email()),
});

export async function createUser(input: unknown) {
"use server";
const event = getRequestEvent();
if (!event?.locals.isAdmin) {
return respond({ error: "Forbidden" }, { status: 403 });
}

const user = await database.users.create(v.parse(CreateUser, input));
return respond(user, {
status: 201,
headers: { "x-created-user": user.id },
});
}
```

:::

:::tab[Zod]

```ts
import { getRequestEvent, respond } from "@solidjs/web";
import { z } from "zod";
Expand All @@ -173,6 +234,10 @@ export async function createUser(input: unknown) {
}
```

:::

::::

Scripted callers receive the carried value, while the transport forwards the response metadata.
`redirect()` and `reload()` return standard `Response` objects carrying `Location` or revalidation headers.
Returning or throwing those responses preserves their control-flow metadata for an integration to interpret.
Expand All @@ -187,6 +252,38 @@ Use `respond()` for an intentional structured failure, or `markSafeError()` only
Solid Router can add caching, submissions, revalidation, forms, and single-flight data to server functions.
These wrappers are optional:

::::tab-group[validation-library]

:::tab[Valibot]

```ts
import { action, query } from "@solidjs/router";
import { getRequestEvent, redirect } from "@solidjs/web";
import * as v from "valibot";

const UserName = v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(100));

export const getUsers = query(async () => {
"use server";
return database.users.all();
}, "users");

export const renameCurrentUser = action(async (form: FormData) => {
"use server";
const event = getRequestEvent();
const userId = event?.locals.userId;
if (!userId) throw redirect("/sign-in");

await database.users.update(userId, {
name: v.parse(UserName, form.get("name")),
});
});
```

:::

:::tab[Zod]

```ts
import { action, query } from "@solidjs/router";
import { getRequestEvent, redirect } from "@solidjs/web";
Expand All @@ -211,6 +308,10 @@ export const renameCurrentUser = action(async (form: FormData) => {
});
```

:::

::::

Place the `"use server"` directive inside the callback passed to `query()` or `action()`.

`query()` caches reads by its name and arguments and declares a wrapped server function as an HTTP `GET`.
Expand Down
28 changes: 28 additions & 0 deletions src/routes/(3)building-apps/(4)sessions-and-auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,30 @@ Authentication identifies the caller.
Authorization decides whether that caller may perform an operation.
Both decisions belong in server code.

::::tab-group[validation-library]

:::tab[Valibot]

```ts
import { redirect } from "@solidjs/web";
import * as v from "valibot";

const UserName = v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(100));

export async function renameCurrentUser(form: FormData) {
"use server";
const session = await getSession();
if (!session?.userId) throw redirect("/sign-in");

const name = v.parse(UserName, form.get("name"));
await database.users.rename(session.userId, name);
}
```

:::

:::tab[Zod]

```ts
import { redirect } from "@solidjs/web";
import { z } from "zod";
Expand All @@ -134,6 +158,10 @@ export async function renameCurrentUser(form: FormData) {
}
```

:::

::::

Hiding a control in the browser does not authorize the corresponding server function or API route.
Check the session or other credentials again at every protected server entry point, then authorize access to the specific record.
Do not accept a user ID from the browser when the operation should target the signed-in user.
Expand Down
38 changes: 37 additions & 1 deletion src/routes/(3)building-apps/(5)environment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,39 @@ This environment layer belongs to start mode and is not enabled by `ssr: true` a

Add `env.ts` or `env.js` at the Vite project root.
Default-export optional `server` and `client` maps whose values implement Standard Schema.
The official `solid-v2/fullstack` template uses Zod:
The official `solid-v2/fullstack` template uses Valibot, but any Standard Schema library works:

::::tab-group[validation-library]

:::tab[Valibot]

```ts
import * as v from "valibot";

const signingKeys = v.pipe(
v.unknown(),
v.transform((value) =>
typeof value === "string"
? value.split(",").map((key) => key.trim())
: value
),
v.array(v.pipe(v.string(), v.minLength(32))),
v.minLength(1)
);

export default {
server: {
SESSION_SECRET: signingKeys,
},
client: {
VITE_APP_NAME: v.optional(v.pipe(v.string(), v.minLength(1)), "Solid App"),
},
};
```

:::

:::tab[Zod]

```ts
import { z } from "zod";
Expand All @@ -35,6 +67,10 @@ export default {
};
```

:::

::::

The `SESSION_SECRET` input is a comma-separated list.
The schema validates every signing key after splitting the input, so a short or empty rotation key fails validation.

Expand Down
36 changes: 36 additions & 0 deletions src/routes/(6)migration/(1)from-solid-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,38 @@ Keep secrets in server-only modules.

Start mode also supports a typed Standard Schema file at `env.ts` or `env.js` in the project root:

::::tab-group[validation-library]

:::tab[Valibot]

```ts title="env.ts"
import * as v from "valibot";

const signingKeys = v.pipe(
v.unknown(),
v.transform((value) =>
typeof value === "string"
? value.split(",").map((key) => key.trim())
: value
),
v.array(v.pipe(v.string(), v.minLength(32))),
v.minLength(1)
);

export default {
server: {
SESSION_SECRET: signingKeys,
},
client: {
VITE_APP_NAME: v.pipe(v.string(), v.minLength(1)),
},
};
```

:::

:::tab[Zod]

```ts title="env.ts"
import { z } from "zod";

Expand All @@ -360,6 +392,10 @@ export default {
};
```

:::

::::

Import validated values from `virtual:env/server` in server-only modules and from `virtual:env/client` in shared or client code.
The plugin generates `solid-env.d.ts` next to the schema.
Do not copy a SolidStart `@solidjs/start/env` type reference.
Expand Down
Loading