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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
48 changes: 48 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: E2E

on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
e2e:
name: E2E (shard ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: pnpm
- run: pnpm install
- name: Install Playwright (chromium)
run: pnpm exec playwright install --with-deps chromium
- name: Run examples smoke tests
run: pnpm exec playwright test --shard=${{ matrix.shard }}/4
timeout-minutes: 30
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.shard }}
path: |
playwright-report/
test-results/
retention-days: 14
if-no-files-found: ignore
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
*~
*.swp
node_modules
dist
*.tsbuildinfo
pages.gen.ts
.cache
.wrangler
test-results
playwright-report
.env.local
.vercel
.netlify
.DS_Store
105 changes: 104 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,105 @@
# waku-examples
The collection of Waku examples

A collection of [Waku](https://waku.gg) examples, grouped by the **Waku API** they
use, since that is the choice that most shapes how an app is written:

- [`fs-router/`](#fs-router) — file-system routing (the default for most apps)
- [`create-pages/`](#create-pages) — programmatic routing with `createPages`
- [`define-router/`](#define-router) — low-level routing with `unstable_defineRouter`
- [`minimal-api/`](#minimal-api) — the low-level minimal RSC primitives

If you are instead looking for a particular **topic** (styling, forms, state,
deployment, …), jump to the [topic index](#by-topic).

## By API

### fs-router

File-system routing under `src/pages`. Most examples run in managed mode (no
server entry); those marked _explicit_ provide a `src/waku.server.tsx` that calls
`fsRouter` directly, usually to customize the adapter.

| Example | What it shows |
| --- | --- |
| [`basic`](fs-router/basic) | Basic app (Tailwind CSS, React Compiler) |
| [`basic-js`](fs-router/basic-js) | Basic app in JavaScript |
| [`data-fetching`](fs-router/data-fetching) | Async server components fetching data |
| [`css-modules`](fs-router/css-modules) | Scoped styling with CSS Modules |
| [`stylex`](fs-router/stylex) | Styling with StyleX (zero-runtime CSS-in-JS) |
| [`vanilla-extract`](fs-router/vanilla-extract) | Styling with Vanilla Extract (compile-time CSS-in-JS) |
| [`form`](fs-router/form) | Server actions and form handling |
| [`jotai`](fs-router/jotai) | Global state with Jotai (`waku-jotai`) |
| [`features`](fs-router/features) | Feature tour — layouts, slices, segments, API routes _(explicit)_ |
| [`tanstack-router`](fs-router/tanstack-router) | Replace the client router with TanStack Router _(explicit)_ |
| [`no-ssr`](fs-router/no-ssr) | Disable SSR for client-only rendering _(explicit)_ |
| [`cloudflare`](fs-router/cloudflare) | Deploy to Cloudflare Workers _(explicit)_ |

### create-pages

Programmatic routing via `createPages` from `waku/router/server`.

| Example | What it shows |
| --- | --- |
| [`basic`](create-pages/basic) | Pages, layouts, slices, and API routes defined in code |
| [`react-tweet`](create-pages/react-tweet) | Embed a third-party component (`react-tweet`) with SSR |
| [`weave-render`](create-pages/weave-render) | Weave async server components with client components |
| [`view-transitions`](create-pages/view-transitions) | Animate navigation with the View Transitions API |

### define-router

The lower-level `unstable_defineRouter` API, for building custom routing on top
of Waku's router.

| Example | What it shows |
| --- | --- |
| [`basic`](define-router/basic) | Manual route configuration with `unstable_defineRouter` |

### minimal-api

The low-level RSC primitives from `waku/minimal/*`. Intended for library
authors, custom runtimes, and learning how Waku works underneath.

| Example | What it shows |
| --- | --- |
| [`basic`](minimal-api/basic) | Minimal RSC + SSR with the minimal API (TypeScript) |
| [`basic-js`](minimal-api/basic-js) | Minimal RSC + SSR (JavaScript) |
| [`promise`](minimal-api/promise) | Stream RSC payloads and compose with `<Children>` |
| [`functions`](minimal-api/functions) | Server functions that return a value and re-render |
| [`nesting`](minimal-api/nesting) | Nested slots and `useRefetch` |
| [`form`](minimal-api/form) | Server actions with `formState` (progressive enhancement) |
| [`css`](minimal-api/css) | CSS Modules with the `classnames` helper |
| [`cookies`](minimal-api/cookies) | Read/write cookies and headers via Hono middleware |
| [`api`](minimal-api/api) | Custom API endpoints alongside RSC |
| [`path-alias`](minimal-api/path-alias) | TypeScript path aliases (`@/*`) |
| [`spa`](minimal-api/spa) | Single-page-app mode (client-only) with server functions |
| [`islands`](minimal-api/islands) | Islands architecture (selective hydration) |
| [`jotai`](minimal-api/jotai) | Jotai state with the minimal API (`waku-jotai`) |

## By topic

The same examples, grouped by what they teach.

| Topic | Examples |
| --- | --- |
| Getting started | [`fs-router/basic`](fs-router/basic), [`fs-router/basic-js`](fs-router/basic-js), [`minimal-api/basic`](minimal-api/basic), [`minimal-api/basic-js`](minimal-api/basic-js) |
| Routing | [`fs-router/features`](fs-router/features), [`fs-router/tanstack-router`](fs-router/tanstack-router), [`create-pages/basic`](create-pages/basic), [`define-router/basic`](define-router/basic) |
| Styling | [`fs-router/css-modules`](fs-router/css-modules), [`fs-router/stylex`](fs-router/stylex), [`fs-router/vanilla-extract`](fs-router/vanilla-extract), [`minimal-api/css`](minimal-api/css) |
| Data fetching | [`fs-router/data-fetching`](fs-router/data-fetching) |
| Mutations (forms, actions, API) | [`fs-router/form`](fs-router/form), [`minimal-api/form`](minimal-api/form), [`minimal-api/functions`](minimal-api/functions), [`minimal-api/api`](minimal-api/api) |
| State management | [`fs-router/jotai`](fs-router/jotai), [`minimal-api/jotai`](minimal-api/jotai) |
| Rendering & architecture | [`fs-router/no-ssr`](fs-router/no-ssr), [`create-pages/weave-render`](create-pages/weave-render), [`minimal-api/promise`](minimal-api/promise), [`minimal-api/nesting`](minimal-api/nesting), [`minimal-api/spa`](minimal-api/spa), [`minimal-api/islands`](minimal-api/islands) |
| Navigation & UX | [`create-pages/view-transitions`](create-pages/view-transitions) |
| Request handling | [`minimal-api/cookies`](minimal-api/cookies) |
| Integrations | [`create-pages/react-tweet`](create-pages/react-tweet), [`fs-router/tanstack-router`](fs-router/tanstack-router) |
| Deployment | [`fs-router/cloudflare`](fs-router/cloudflare) |
| Tooling & config | [`minimal-api/path-alias`](minimal-api/path-alias) |

## Running an example

Each example is a standalone Waku project.

```sh
cd <group>/<name>
pnpm install
pnpm dev
```
23 changes: 23 additions & 0 deletions create-pages/basic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "create-pages-basic",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "waku dev",
"build": "waku build",
"start": "waku start"
},
"dependencies": {
"react": "~19.2.6",
"react-dom": "~19.2.6",
"react-server-dom-webpack": "~19.2.6",
"waku": "1.0.0-beta.3"
},
"devDependencies": {
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"server-only": "^0.0.1",
"typescript": "^6.0.3"
}
}
1 change: 1 addition & 0 deletions create-pages/basic/private/hi.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello from a text file!
10 changes: 10 additions & 0 deletions create-pages/basic/src/components/BarPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Counter } from './Counter';

const Bar = () => (
<div>
<h2>Bar</h2>
<Counter />
</div>
);

export default Bar;
27 changes: 27 additions & 0 deletions create-pages/basic/src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { useState, useTransition } from 'react';
import { Link, useRouter } from 'waku/router/client';
import { jump } from './funcs';

export const Counter = () => {
const { path } = useRouter();
const [count, setCount] = useState(0);
const [isPending, startTransition] = useTransition();
return (
<div style={{ border: '3px blue dashed', margin: '1em', padding: '1em' }}>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<h3>This is a client component.</h3>
<span>path: {path}</span>
<p>
<Link to="/">Go to Home</Link>
</p>
<p>
<button onClick={() => startTransition(jump)}>
Jump to random page{isPending && '...'}
</button>
</p>
</div>
);
};
10 changes: 10 additions & 0 deletions create-pages/basic/src/components/DeeplyNestedLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react';

export const DeeplyNestedLayout = ({ children }: { children: ReactNode }) => {
return (
<div>
<h3>Deeply Nested Layout</h3>
{children}
</div>
);
};
7 changes: 7 additions & 0 deletions create-pages/basic/src/components/DynamicSlice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function DynamicSlice() {
return (
<h2 className="text-2xl font-bold">
Dynamic Slice Component {new Date().toLocaleString()}
</h2>
);
}
12 changes: 12 additions & 0 deletions create-pages/basic/src/components/FooPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'server-only';

import { Counter } from './Counter';

const Foo = () => (
<div>
<h2>Foo</h2>
<Counter />
</div>
);

export default Foo;
51 changes: 51 additions & 0 deletions create-pages/basic/src/components/HomeLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ReactNode } from 'react';
import { Link } from 'waku/router/client';
import { Pending } from './pending';
import '../styles.css';
import { RoutingHandler } from './RoutingHandler';

const HomeLayout = ({ children }: { children: ReactNode }) => (
<div>
<RoutingHandler />
<ul>
<li>
<Link to="/">
Home
<Pending />
</Link>
</li>
<li>
<Link to="/foo">
Foo
<Pending />
</Link>
</li>
<li>
<Link to="/bar" unstable_prefetchOnEnter>
Bar
</Link>
</li>
<li>
<Link to="/baz">Baz</Link>
</li>
<li>
<Link to="/nested/foo">Nested / Foo</Link>
</li>
<li>
<Link to="/nested/bar">Nested / Bar</Link>
</li>
<li>
<Link to="/nested/baz">Nested / Baz</Link>
</li>
<li>
<Link to="/nested/qux">Nested / Qux</Link>
</li>
<li>
<Link to="/slice-page">Slice Page</Link>
</li>
</ul>
{children}
</div>
);

export default HomeLayout;
8 changes: 8 additions & 0 deletions create-pages/basic/src/components/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const Home = () => (
<div>
<h2>Home</h2>
<p>This is the home page.</p>
</div>
);

export default Home;
9 changes: 9 additions & 0 deletions create-pages/basic/src/components/NestedBazPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Baz = () => (
<div>
<h2>Nested</h2>
<h3>Baz</h3>
<p>{new Date().toISOString()}</p>
</div>
);

export default Baz;
12 changes: 12 additions & 0 deletions create-pages/basic/src/components/NestedLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ReactNode } from 'react';

const NestedLayout = ({ children }: { children: ReactNode }) => {
return (
<div>
<h3>Nested Layout</h3>
{children}
</div>
);
};

export default NestedLayout;
8 changes: 8 additions & 0 deletions create-pages/basic/src/components/NestedQuxPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const Qux = () => (
<div>
<h2>Nested</h2>
<h3>Qux</h3>
</div>
);

export default Qux;
12 changes: 12 additions & 0 deletions create-pages/basic/src/components/Root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ReactNode } from 'react';

export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<head>
<title>Waku</title>
</head>
<body>{children}</body>
</html>
);
}
23 changes: 23 additions & 0 deletions create-pages/basic/src/components/RoutingHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { useEffect } from 'react';
import { useRouter } from 'waku/router/client';

export const RoutingHandler = () => {
const router = useRouter();
useEffect(() => {
const onStart = () => {
console.log('Route change started');
};
const onComplete = () => {
console.log('Route change completed');
};
router.unstable_events.on('start', onStart);
router.unstable_events.on('complete', onComplete);
return () => {
router.unstable_events.off('start', onStart);
router.unstable_events.off('complete', onComplete);
};
});
return null;
};
Loading
Loading