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
47 changes: 47 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
FROM archlinux:latest

# Sync package database and install base tooling
# docker CLI (no daemon) lets `docker compose` talk to the host Podman socket
RUN pacman -Syu --noconfirm \
&& pacman -S --noconfirm --needed \
base-devel \
bash \
ca-certificates \
curl \
docker \
docker-compose \
git \
github-cli \
make \
openssh \
procps-ng \
sudo \
unzip \
&& pacman -Scc --noconfirm

# Create a non-root user matching the host UID/GID (1000/1000 on SteamOS)
ARG USERNAME=dev
ARG USER_UID=1000
ARG USER_GID=1000
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m -s /bin/bash $USERNAME \
&& echo "$USERNAME ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME

USER $USERNAME
WORKDIR /home/$USERNAME

# Install mise (runtime version manager)
RUN curl https://mise.run | sh
ENV PATH="/home/$USERNAME/.local/bin:$PATH"

# Node 20 matches the repo's CI workflows; pnpm 10.14.0 matches the
# "packageManager" field in package.json (activated via corepack).
RUN mise use --global node@20 \
&& mise exec -- corepack enable \
&& mise exec -- corepack prepare pnpm@10.14.0 --activate

RUN echo 'eval "$(mise activate bash)"' >> ~/.bashrc \
&& echo 'eval "$(mise activate bash)"' >> ~/.profile

ENV PATH="/home/$USERNAME/.local/share/mise/shims:$PATH"
87 changes: 87 additions & 0 deletions .devcontainer/linux-podman/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
// ─── To reuse this in another project ────────────────────────────────────────
// 1. Copy .devcontainer/ into the new repo
// 2. Update "name" and "postCreateCommand"
// 3. Everything else (Dockerfile, mounts, runArgs) is project-agnostic
// ─────────────────────────────────────────────────────────────────────────────
"name": "yar",
"build": {
"dockerfile": "../Dockerfile",
"args": {
"USERNAME": "dev",
"USER_UID": "1000",
"USER_GID": "1000"
}
},

// Podman-specific flags only — no volume paths here (those are in "mounts")
// --userns=keep-id: rootless Podman maps your UID into the container so file ownership matches
// --network=host: shares host network stack (localhost = host)
"runArgs": [
"--userns=keep-id",
"--network=host"
],

// ${localEnv:HOME} → /home/<you> on any Linux machine
// ${localEnv:XDG_RUNTIME_DIR} → /run/user/<uid>, where the Podman socket lives
"mounts": [
// Podman socket → docker CLI inside the container talks to host Podman (no daemon needed).
// Mounted at the SAME path as on the host so docker-out-of-docker path forwarding works.
{
"source": "${localEnv:XDG_RUNTIME_DIR}/podman/podman.sock",
"target": "${localEnv:XDG_RUNTIME_DIR}/podman/podman.sock",
"type": "bind"
},
// pnpm content-addressable store → packages survive image rebuilds
{
"source": "${localEnv:HOME}/.local/share/pnpm",
"target": "/home/dev/.local/share/pnpm",
"type": "bind"
},
// SSH keys (read-only) → git push/pull over SSH works inside the container
{
"source": "${localEnv:HOME}/.ssh",
"target": "/home/dev/.ssh",
"type": "bind",
"readonly": true
},
// gh CLI auth token → survives container rebuilds without re-authenticating
{
"source": "${localEnv:HOME}/.config/gh",
"target": "/home/dev/.config/gh",
"type": "bind"
}
],

"containerEnv": {
// Point at the socket's real host path (mounted 1:1 above) so any path the
// CLI forwards to sibling containers resolves identically on the host.
"DOCKER_HOST": "unix://${localEnv:XDG_RUNTIME_DIR}/podman/podman.sock"
},

// Mount the workspace at its REAL host path (not /workspaces/...) so any
// project-relative paths forwarded to sibling containers resolve on the HOST.
"workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind,consistency=cached",
"workspaceFolder": "${localWorkspaceFolder}",

// reshim: regenerates mise shims to point to this container's mise binary
// store-dir: pins pnpm store so it never falls back to a project-local .pnpm-store/
"postCreateCommand": "mise reshim && pnpm config set store-dir /home/dev/.local/share/pnpm/store && pnpm install",

// NOTE: No "forwardPorts" here on purpose. This container runs with
// --network=host (see runArgs), so it already shares the host's network
// namespace and any dev server is directly reachable on the host.

"customizations": {
"vscode": {
"extensions": [
"biomejs.biome"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"terminal.integrated.defaultProfile.linux": "bash"
}
}
}
}
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A simple, type-safe router for React that works with any framework.

## Why use this?

- ✨ **Super Simple** - Only 2 functions: `createRoute` and `createRouter`
- ✨ **Super Simple** - A tiny API: `createRoute` and `createRouter` (plus optional `defineRoute`/`defineRoutes` sugar)
- 🔒 **Type Safe** - TypeScript knows your route params automatically
- 🎯 **Flexible** - Works with any React framework
- ✅ **Validated** - Built-in query parameter validation
Expand Down Expand Up @@ -103,6 +103,44 @@ const staticRoutes = Object.values(router.routes)
.filter(route => route.meta?.isStatic);
```

## Declarative Routes (less boilerplate)

If you don't need custom closures per route, `defineRoute` and `defineRoutes` remove the handler wiring entirely. Components receive the route context (`params`, `query`) as props, and `loader`/`meta`/`extra` receive it as their first argument:

```tsx
import { defineRoute, defineRoutes, createRouter } from "@btst/yar";

// Single route
const postRoute = defineRoute("/blog/:slug", {
page: BlogPostPage, // rendered with { params, query } as props
loading: Spinner,
error: ErrorPage,
loader: (ctx, signal?: AbortSignal) =>
fetch(`/api/posts/${ctx.params.slug}`, { signal }).then((r) => r.json()),
meta: (ctx, post) => [{ name: "title", content: post?.title ?? ctx.params.slug }],
extra: (ctx) => ({ breadcrumbs: ["Home", "Blog", ctx.params.slug] }),
});

// Many routes at once, with optional page overrides
const routes = defineRoutes(
{
home: defineRoute("/", { page: HomePage }, undefined, { isStatic: true }),
post: defineRoute("/blog/:slug", {
page: BlogPostPage,
loader: (ctx) => fetchPost(ctx.params.slug),
meta: (ctx) => [{ name: "title", content: ctx.params.slug }],
}),
},
{ pages: { post: CustomPostPage } } // swap a page component per key
);

const router = createRouter(routes);
```

Overridden pages still receive the route context (`params`, `query`) as props. `defineRoutes` also accepts plain `createRoute` routes; overriding those swaps the `PageComponent` as-is.

`defineRoute` returns a regular route, so it composes freely with `createRoute` routes in the same `createRouter` call. Use `createRoute` when you need full control over the handler closure; use `defineRoute`/`defineRoutes` for the common declarative case.

## What You Need to Know

### `createRoute(path, handler, options?, routeMeta?)`
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/yar",
"version": "1.2.0",
"version": "1.3.0",
"packageManager": "pnpm@10.14.0",
"description": "Pluggable router for modern react frameworks",
"type": "module",
Expand Down Expand Up @@ -44,13 +44,15 @@
"@biomejs/biome": "2.2.4",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"react": "^19.1.1",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"peerDependencies": {
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9"
"@types/react-dom": "^19.1.9",
"react": "^18.0.0 || ^19.0.0"
},
"exports": {
".": {
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

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

Loading
Loading