toiljs 0.0.10 → 0.0.11

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.
Files changed (39) hide show
  1. package/README.md +313 -1
  2. package/assets/logo.svg +37 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/create.js +2 -2
  5. package/build/compiler/.tsbuildinfo +1 -1
  6. package/build/compiler/generate.js +9 -3
  7. package/build/compiler/vite.js +7 -0
  8. package/examples/basic/client/components/Header.tsx +4 -1
  9. package/examples/basic/client/layout.tsx +4 -1
  10. package/examples/basic/client/public/index.html +1 -1
  11. package/examples/basic/client/routes/(legal)/privacy.tsx +19 -0
  12. package/examples/basic/client/routes/(legal)/terms.tsx +16 -0
  13. package/examples/basic/client/routes/about.tsx +8 -5
  14. package/examples/basic/client/routes/blog/[id].tsx +7 -1
  15. package/examples/basic/client/routes/features/actions.tsx +67 -0
  16. package/examples/basic/client/routes/features/error/error.tsx +16 -0
  17. package/examples/basic/client/routes/features/error/index.tsx +27 -0
  18. package/examples/basic/client/routes/features/head.tsx +38 -0
  19. package/examples/basic/client/routes/features/index.tsx +75 -0
  20. package/examples/basic/client/routes/features/realtime.tsx +32 -0
  21. package/examples/basic/client/routes/features/script.tsx +31 -0
  22. package/examples/basic/client/routes/features/seo.tsx +39 -0
  23. package/examples/basic/client/routes/features/template/b.tsx +14 -0
  24. package/examples/basic/client/routes/features/template/index.tsx +20 -0
  25. package/examples/basic/client/routes/features/template/template.tsx +18 -0
  26. package/examples/basic/client/routes/files/[[...slug]].tsx +21 -0
  27. package/examples/basic/client/routes/gallery/@modal/(.)photo/[id].tsx +23 -0
  28. package/examples/basic/client/routes/gallery/index.tsx +42 -0
  29. package/examples/basic/client/routes/gallery/layout.tsx +13 -0
  30. package/examples/basic/client/routes/gallery/photo/[id].tsx +18 -0
  31. package/examples/basic/client/routes/index.tsx +11 -2
  32. package/examples/basic/client/routes/loader-demo/index.tsx +6 -4
  33. package/examples/basic/client/toil.tsx +2 -4
  34. package/package.json +3 -2
  35. package/src/cli/create.ts +2 -2
  36. package/src/compiler/generate.ts +12 -3
  37. package/src/compiler/vite.ts +15 -0
  38. package/test/dom/route-head.test.tsx +34 -0
  39. package/test/slot-layouts.test.ts +69 -0
@@ -0,0 +1,67 @@
1
+ // Mutations: a `loader` reads, an action writes, then revalidation refetches the loader so the UI
2
+ // reflects the new state with no manual refetch. Here a module-level counter stands in for a server.
3
+ let serverCount = 0;
4
+ async function wait(ms: number): Promise<void> {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+
8
+ export const metadata: Toil.Metadata = {
9
+ title: 'Actions and forms',
10
+ description: 'useAction and <Form> mutations with pending state and revalidation.',
11
+ };
12
+
13
+ export const loader = async () => {
14
+ await wait(150);
15
+ return { count: serverCount };
16
+ };
17
+
18
+ export default function ActionsDemo() {
19
+ const { count } = Toil.useLoaderData(loader);
20
+
21
+ // useAction: run a mutation, track pending/error, then revalidate this route's loader on success.
22
+ const increment = Toil.useAction(
23
+ async (by: number) => {
24
+ await wait(400);
25
+ serverCount += by;
26
+ return serverCount;
27
+ },
28
+ { revalidate: true },
29
+ );
30
+
31
+ return (
32
+ <main>
33
+ <h1>Actions and forms</h1>
34
+ <p>
35
+ Server count (from the loader): <strong>{count}</strong>
36
+ </p>
37
+
38
+ <p>
39
+ <button type="button" disabled={increment.pending} onClick={() => void increment.run(1)}>
40
+ {increment.pending ? 'Saving' : 'Increment via useAction'}
41
+ </button>
42
+ {increment.error ? <span style={{ color: 'crimson' }}> failed</span> : null}
43
+ </p>
44
+
45
+ {/* The declarative form: submits to an action, revalidates on success, exposes pending. */}
46
+ <Toil.Form
47
+ action={async (form) => {
48
+ await wait(400);
49
+ serverCount += Number(form.get('by') || 0);
50
+ }}
51
+ revalidate>
52
+ {({ pending }) => (
53
+ <>
54
+ <input name="by" type="number" defaultValue={5} disabled={pending} />
55
+ <button type="submit" disabled={pending}>
56
+ {pending ? 'Saving' : 'Add via <Form>'}
57
+ </button>
58
+ </>
59
+ )}
60
+ </Toil.Form>
61
+
62
+ <p>
63
+ <Toil.Link href="/features">Back to features</Toil.Link>
64
+ </p>
65
+ </main>
66
+ );
67
+ }
@@ -0,0 +1,16 @@
1
+ // The error boundary for this segment. It receives the thrown error and a `reset` to retry the
2
+ // render. Place an `error.tsx` next to any route to contain failures there.
3
+ export default function FeatureError({ error, reset }: Toil.RouteErrorProps) {
4
+ return (
5
+ <main>
6
+ <h1>Something broke</h1>
7
+ <p style={{ color: 'crimson' }}>{error instanceof Error ? error.message : String(error)}</p>
8
+ <p>
9
+ <button type="button" onClick={reset}>
10
+ Try again
11
+ </button>{' '}
12
+ <Toil.Link href="/features">Back to features</Toil.Link>
13
+ </p>
14
+ </main>
15
+ );
16
+ }
@@ -0,0 +1,27 @@
1
+ import { useState } from 'react';
2
+
3
+ export const metadata: Toil.Metadata = { title: 'Error boundary' };
4
+
5
+ // When a route throws during render, the nearest `error.tsx` catches it and renders instead of a
6
+ // blank screen. Click the button to flip into a throwing render and watch error.tsx take over.
7
+ export default function ErrorDemo() {
8
+ const [boom, setBoom] = useState(false);
9
+ if (boom) throw new Error('Kaboom from features/error, caught by error.tsx');
10
+ return (
11
+ <main>
12
+ <h1>Error boundary</h1>
13
+ <p>
14
+ A thrown render is caught by <code>error.tsx</code> in this folder, scoped to this
15
+ segment so the rest of the app keeps working.
16
+ </p>
17
+ <p>
18
+ <button type="button" onClick={() => setBoom(true)}>
19
+ Throw an error
20
+ </button>
21
+ </p>
22
+ <p>
23
+ <Toil.Link href="/features">Back to features</Toil.Link>
24
+ </p>
25
+ </main>
26
+ );
27
+ }
@@ -0,0 +1,38 @@
1
+ import { useState } from 'react';
2
+
3
+ // The imperative head API, for when the title or tags depend on component state rather than a static
4
+ // export. `useTitle` / `useHead` apply for the component's lifetime and revert on unmount; `<Toil.Head>`
5
+ // is the declarative form. They compose with (and can override) the route `metadata`.
6
+ export default function HeadDemo() {
7
+ const [count, setCount] = useState(0);
8
+
9
+ // Live title: the tab updates every render as `count` changes.
10
+ Toil.useTitle(`Clicked ${count} times`);
11
+
12
+ // Add a meta tag and a canonical link for this page only.
13
+ Toil.useHead({
14
+ meta: [{ name: 'description', content: `Imperative head demo, clicked ${count} times.` }],
15
+ link: [{ rel: 'canonical', href: 'https://toil.example/features/head' }],
16
+ });
17
+
18
+ return (
19
+ <main>
20
+ <h1>Imperative head</h1>
21
+ <p>
22
+ The tab title is driven by component state via <code>Toil.useTitle</code>. Click the
23
+ button and watch it change, then leave the page and the title reverts.
24
+ </p>
25
+ <p>
26
+ <button type="button" onClick={() => setCount((c) => c + 1)}>
27
+ Clicked {count} times
28
+ </button>
29
+ </p>
30
+ {/* Declarative equivalent, the same merge rules apply. */}
31
+ <Toil.Head meta={[{ property: 'og:title', content: `Clicked ${count} times` }]} />
32
+ <p>
33
+ <Toil.Link href="/features/seo">Compare with route metadata</Toil.Link>{' '}
34
+ <Toil.Link href="/features">Back to features</Toil.Link>
35
+ </p>
36
+ </main>
37
+ );
38
+ }
@@ -0,0 +1,75 @@
1
+ // The feature hub: one place that links to a live demo of every ToilJS capability. Its own tab
2
+ // title comes from the `metadata` export below (rendered as "Features | ToilJS" via the layout
3
+ // template) and is baked into build/client/features/index.html at build time.
4
+ export const metadata: Toil.Metadata = {
5
+ title: 'Features',
6
+ description: 'Live demos of every ToilJS feature: routing, data, head/SEO, components, realtime, and binary IO.',
7
+ openGraph: { title: 'Every ToilJS feature, demoed', type: 'website' },
8
+ };
9
+
10
+ const groups: { heading: string; items: { href: Toil.Href; label: string; note: string }[] }[] = [
11
+ {
12
+ heading: 'Routing',
13
+ items: [
14
+ { href: '/blog/42', label: 'Dynamic route', note: 'blog/[id].tsx, /blog/42' },
15
+ { href: '/docs/getting/started', label: 'Catch-all', note: 'docs/[...slug].tsx' },
16
+ { href: '/files', label: 'Optional catch-all', note: 'files/[[...slug]].tsx, matches /files and /files/a/b' },
17
+ { href: '/privacy', label: 'Route group', note: '(legal)/privacy.tsx, no URL segment' },
18
+ { href: '/gallery', label: 'Parallel + intercepting', note: '@modal/(.)photo/[id], a real modal route' },
19
+ { href: '/features/template', label: 'Templates', note: 'template.tsx remounts on every navigation' },
20
+ { href: '/features/error', label: 'Error boundary', note: 'error.tsx catches a thrown route' },
21
+ ],
22
+ },
23
+ {
24
+ heading: 'Data',
25
+ items: [
26
+ { href: '/loader-demo', label: 'Loader + revalidate', note: 'data before render, cached, refetchable' },
27
+ { href: '/features/actions', label: 'Actions + Form', note: 'useAction / <Form>, pending state, revalidate' },
28
+ ],
29
+ },
30
+ {
31
+ heading: 'Head and SEO',
32
+ items: [
33
+ { href: '/features/seo', label: 'Route metadata', note: 'export const metadata, title override' },
34
+ { href: '/features/head', label: 'Imperative head', note: 'useTitle / useHead / <Head>' },
35
+ ],
36
+ },
37
+ {
38
+ heading: 'Components and runtime',
39
+ items: [
40
+ { href: '/features/script', label: 'Script', note: 'Toil.Script with a load strategy' },
41
+ { href: '/features/realtime', label: 'WebSocket channel', note: 'Toil.useChannel against /_toil' },
42
+ { href: '/io', label: 'Binary IO', note: 'BinaryWriter / BinaryReader / FastSet, no import' },
43
+ ],
44
+ },
45
+ ];
46
+
47
+ export default function Features() {
48
+ return (
49
+ <main>
50
+ <h1>Every feature, live</h1>
51
+ <p>
52
+ Each link is a working demo served from <code>client/routes/</code>. Watch the tab
53
+ title change as you navigate, that is the per-route <code>metadata</code> at work.
54
+ </p>
55
+ {groups.map((g) => (
56
+ <section key={g.heading} style={{ marginTop: 24 }}>
57
+ <h2 style={{ fontSize: '1rem', opacity: 0.7 }}>{g.heading}</h2>
58
+ <ul style={{ display: 'grid', gap: 8, listStyle: 'none', padding: 0 }}>
59
+ {g.items.map((it) => (
60
+ <li key={it.href}>
61
+ <Toil.Link href={it.href} style={{ fontWeight: 600 }}>
62
+ {it.label}
63
+ </Toil.Link>
64
+ <span style={{ opacity: 0.6 }}> , {it.note}</span>
65
+ </li>
66
+ ))}
67
+ </ul>
68
+ </section>
69
+ ))}
70
+ <p style={{ marginTop: 24 }}>
71
+ <Toil.Link href="/">Back home</Toil.Link>
72
+ </p>
73
+ </main>
74
+ );
75
+ }
@@ -0,0 +1,32 @@
1
+ export const metadata: Toil.Metadata = {
2
+ title: 'Realtime',
3
+ description: 'A typed WebSocket channel to the server with connect, reconnect, and message decoding.',
4
+ };
5
+
6
+ // Toil.useChannel opens a WebSocket to the server (default path /_toil), tracks `connected`, collects
7
+ // `messages`, and exposes `send`. It reconnects automatically. With no server socket running the demo
8
+ // simply shows "disconnected", the API is the same once the server handles the channel.
9
+ export default function RealtimeDemo() {
10
+ const chat = Toil.useChannel({ path: '/_toil' });
11
+ return (
12
+ <main>
13
+ <h1>Realtime</h1>
14
+ <p>
15
+ Connection: <strong>{chat.connected ? 'connected' : 'disconnected'}</strong>, messages
16
+ received: <strong>{chat.messages.length}</strong>.
17
+ </p>
18
+ <p>
19
+ <button type="button" onClick={() => chat.send('ping')}>
20
+ Send ping
21
+ </button>
22
+ </p>
23
+ <p style={{ opacity: 0.6 }}>
24
+ <code>const chat = Toil.useChannel({'{'} path: '/_toil' {'}'})</code>, connect, reconnect,
25
+ and decoding are handled for you.
26
+ </p>
27
+ <p>
28
+ <Toil.Link href="/features">Back to features</Toil.Link>
29
+ </p>
30
+ </main>
31
+ );
32
+ }
@@ -0,0 +1,31 @@
1
+ import { useState } from 'react';
2
+
3
+ export const metadata: Toil.Metadata = {
4
+ title: 'Script',
5
+ description: 'Load third-party scripts with a strategy, deduplicated so they never run twice.',
6
+ };
7
+
8
+ // Toil.Script injects an external or inline script once (deduped by src/id), with a load strategy.
9
+ // `onReady` fires when it has loaded. Here we load a tiny public script and report when it is ready.
10
+ export default function ScriptDemo() {
11
+ const [ready, setReady] = useState(false);
12
+ return (
13
+ <main>
14
+ <h1>Script</h1>
15
+ <p>
16
+ <code>Toil.Script</code> loads external scripts with a <code>strategy</code>{' '}
17
+ (<code>afterInteractive</code>, <code>lazyOnload</code>, <code>beforeInteractive</code>)
18
+ and dedupes them, so the same script never runs twice across navigations.
19
+ </p>
20
+ <Toil.Script
21
+ src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js"
22
+ strategy="afterInteractive"
23
+ onReady={() => setReady(true)}
24
+ />
25
+ <p>Status: {ready ? 'script ready' : 'loading'}</p>
26
+ <p>
27
+ <Toil.Link href="/features">Back to features</Toil.Link>
28
+ </p>
29
+ </main>
30
+ );
31
+ }
@@ -0,0 +1,39 @@
1
+ // Route metadata: the declarative way to set this page's <title>, description, OpenGraph, and more.
2
+ // The router applies it before paint and the build bakes it into static HTML for crawlers.
3
+ //
4
+ // This route sets its OWN `titleTemplate: '%s'`, which overrides the layout's "%s | ToilJS" template,
5
+ // so the tab reads exactly "useReducer | React Hooks" with no site suffix. Drop the titleTemplate
6
+ // line and the same title renders as "useReducer | React Hooks | ToilJS".
7
+ export const metadata: Toil.Metadata = {
8
+ title: 'useReducer | React Hooks',
9
+ titleTemplate: '%s',
10
+ description: 'Manage complex state transitions with a reducer function using the useReducer hook.',
11
+ keywords: ['react', 'hooks', 'useReducer', 'state'],
12
+ canonical: 'https://toil.example/features/seo',
13
+ openGraph: {
14
+ title: 'useReducer | React Hooks',
15
+ description: 'Manage complex state transitions with a reducer.',
16
+ type: 'website',
17
+ },
18
+ };
19
+
20
+ export default function SeoDemo() {
21
+ return (
22
+ <main>
23
+ <h1>Route metadata</h1>
24
+ <p>
25
+ The browser tab now reads <strong>useReducer | React Hooks</strong>, set entirely by
26
+ the <code>metadata</code> export in <code>client/routes/features/seo.tsx</code>, with
27
+ no <code>useEffect</code> and no title suffix.
28
+ </p>
29
+ <p>
30
+ It also emitted <code>&lt;meta name="description"&gt;</code>, keywords, a canonical
31
+ link, and the <code>og:*</code> tags, all from that one object.
32
+ </p>
33
+ <p>
34
+ <Toil.Link href="/features/head">Prefer the imperative API?</Toil.Link>{' '}
35
+ <Toil.Link href="/features">Back to features</Toil.Link>
36
+ </p>
37
+ </main>
38
+ );
39
+ }
@@ -0,0 +1,14 @@
1
+ export const metadata: Toil.Metadata = { title: 'Templates, sibling' };
2
+
3
+ export default function TemplateSibling() {
4
+ return (
5
+ <main>
6
+ <h1>Sibling page</h1>
7
+ <p>Same template, fresh mount. Navigate back and forth to see the counter increment.</p>
8
+ <p>
9
+ <Toil.Link href="/features/template">First page</Toil.Link>{' '}
10
+ <Toil.Link href="/features">Back to features</Toil.Link>
11
+ </p>
12
+ </main>
13
+ );
14
+ }
@@ -0,0 +1,20 @@
1
+ export const metadata: Toil.Metadata = { title: 'Templates' };
2
+
3
+ export default function TemplateDemo() {
4
+ return (
5
+ <main>
6
+ <h1>Templates</h1>
7
+ <p>
8
+ The line above is rendered by <code>template.tsx</code>. Bounce between these two links
9
+ and watch the mount number climb, the template remounts on every navigation.
10
+ </p>
11
+ <p>
12
+ <Toil.Link href="/features/template">This page</Toil.Link>{' '}
13
+ <Toil.Link href="/features/template/b">Sibling page</Toil.Link>
14
+ </p>
15
+ <p>
16
+ <Toil.Link href="/features">Back to features</Toil.Link>
17
+ </p>
18
+ </main>
19
+ );
20
+ }
@@ -0,0 +1,18 @@
1
+ import { useState, type ReactNode } from 'react';
2
+
3
+ // A template wraps a segment like a layout, but RE-MOUNTS on every navigation within it (a layout
4
+ // persists). This counter increments each time the template mounts, so navigating between the two
5
+ // child links below bumps it, proving the remount. Swap this file for `layout.tsx` and the number
6
+ // would hold steady instead.
7
+ let mounts = 0;
8
+ export default function PlaygroundTemplate({ children }: { children?: ReactNode }) {
9
+ const [mountId] = useState(() => ++mounts);
10
+ return (
11
+ <div>
12
+ <p style={{ opacity: 0.6 }}>
13
+ template mount #{mountId} (it increments on every navigation here)
14
+ </p>
15
+ {children}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,21 @@
1
+ export const metadata: Toil.Metadata = { title: 'Optional catch-all' };
2
+
3
+ // Optional catch-all: `[[...slug]]` matches the bare `/files` AND any depth below it (`/files/a/b`).
4
+ // `slug` is undefined at the base and a path string when segments are present.
5
+ export default function Files() {
6
+ const { slug } = Toil.useParams();
7
+ return (
8
+ <main>
9
+ <h1>Optional catch-all</h1>
10
+ <p>
11
+ <code>files/[[...slug]].tsx</code> matched. Current slug:{' '}
12
+ <code>{slug ?? '(none, this is the base /files)'}</code>
13
+ </p>
14
+ <p>
15
+ <Toil.Link href="/files">/files</Toil.Link>{' '}
16
+ <Toil.Link href="/files/images/logo">/files/images/logo</Toil.Link>{' '}
17
+ <Toil.Link href="/features">Back to features</Toil.Link>
18
+ </p>
19
+ </main>
20
+ );
21
+ }
@@ -0,0 +1,23 @@
1
+ // Intercepting route: `(.)photo/[id]` inside the `@modal` slot catches a soft navigation to
2
+ // /gallery/photo/:id and renders here instead, as an overlay, while the gallery stays mounted behind
3
+ // it. On a hard reload the interception does not apply and the full photo/[id].tsx page renders.
4
+ export default function PhotoModal() {
5
+ const { id } = Toil.useParams();
6
+ return (
7
+ <div
8
+ style={{
9
+ position: 'fixed',
10
+ inset: 0,
11
+ background: 'rgba(0,0,0,0.6)',
12
+ display: 'grid',
13
+ placeItems: 'center',
14
+ zIndex: 50,
15
+ }}>
16
+ <div style={{ background: 'var(--bg, #0b0f14)', padding: 24, borderRadius: 12, minWidth: 240 }}>
17
+ <h2>Photo {id}</h2>
18
+ <p>This is the intercepted modal view (soft navigation).</p>
19
+ <Toil.Link href="/gallery">Close</Toil.Link>
20
+ </div>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,42 @@
1
+ export const metadata: Toil.Metadata = {
2
+ title: 'Gallery',
3
+ description: 'Parallel routes and intercepting routes: a photo opens as a modal on soft nav.',
4
+ };
5
+
6
+ const photos = [1, 2, 3, 4];
7
+
8
+ // Clicking a photo soft-navigates to /gallery/photo/:id. The intercepting route @modal/(.)photo/[id]
9
+ // catches that on soft nav and shows it as a modal over this grid. A hard reload of the same URL
10
+ // renders the full page (photo/[id].tsx) instead, deep links still work.
11
+ export default function Gallery() {
12
+ return (
13
+ <main>
14
+ <h1>Gallery</h1>
15
+ <p>
16
+ Click a photo, it opens as a modal (intercepting route). Reload that URL and you get
17
+ the full page. Same URL, two presentations.
18
+ </p>
19
+ <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
20
+ {photos.map((id) => (
21
+ <Toil.Link
22
+ key={id}
23
+ href={`/gallery/photo/${id}`}
24
+ style={{
25
+ width: 88,
26
+ height: 88,
27
+ display: 'grid',
28
+ placeItems: 'center',
29
+ border: '1px solid currentColor',
30
+ borderRadius: 8,
31
+ fontWeight: 700,
32
+ }}>
33
+ {id}
34
+ </Toil.Link>
35
+ ))}
36
+ </div>
37
+ <p style={{ marginTop: 16 }}>
38
+ <Toil.Link href="/features">Back to features</Toil.Link>
39
+ </p>
40
+ </main>
41
+ );
42
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+
3
+ // This layout renders the normal page content AND a parallel `@modal` slot. The slot stays empty
4
+ // until an intercepting route fills it (see @modal/(.)photo/[id].tsx), at which point a modal appears
5
+ // over the gallery without leaving the page.
6
+ export default function GalleryLayout({ children }: { children?: ReactNode }) {
7
+ return (
8
+ <div>
9
+ {children}
10
+ <Toil.Slot name="modal" />
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,18 @@
1
+ export const generateMetadata: Toil.GenerateMetadata = ({ params }) => ({
2
+ title: `Photo ${params.id}`,
3
+ });
4
+
5
+ // The full page for a photo, shown on a hard load or deep link to /gallery/photo/:id.
6
+ export default function PhotoPage() {
7
+ const { id } = Toil.useParams();
8
+ return (
9
+ <main>
10
+ <h1>Photo {id}</h1>
11
+ <p>
12
+ Full page at <code>gallery/photo/[id].tsx</code>. Reached directly (reload or deep
13
+ link), not intercepted.
14
+ </p>
15
+ <Toil.Link href="/gallery">Back to gallery</Toil.Link>
16
+ </main>
17
+ );
18
+ }
@@ -1,3 +1,12 @@
1
+ // The home page sets its own title with an absolute template (`%s`), so the tab reads exactly this
2
+ // rather than being suffixed by the layout's "%s | ToilJS".
3
+ export const metadata: Toil.Metadata = {
4
+ title: 'ToilJS, the modern React framework',
5
+ titleTemplate: '%s',
6
+ description: 'File-based routing, instant HMR, build-time SEO, and a WebAssembly backend. Zero config.',
7
+ openGraph: { title: 'ToilJS', type: 'website' },
8
+ };
9
+
1
10
  const GitHubIcon = () => (
2
11
  <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
3
12
  <path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.604-3.369-1.341-3.369-1.341-.454-1.154-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0 1 12 6.836a9.59 9.59 0 0 1 2.504.337c1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.202 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.579.688.481C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z" />
@@ -42,9 +51,9 @@ export default function Home() {
42
51
  return (
43
52
  <section className="hero">
44
53
  <div className="hero-logo">
45
- <img src="images/logo.svg" className="hero-logo-glow" alt="" aria-hidden="true" width={96} height={96} />
54
+ <img src="/images/logo.svg" className="hero-logo-glow" alt="" aria-hidden="true" width={96} height={96} />
46
55
  <Toil.Image
47
- src="images/logo.svg"
56
+ src="/images/logo.svg"
48
57
  className="hero-logo-img"
49
58
  alt="ToilJS"
50
59
  width={96}
@@ -25,7 +25,7 @@ export default function LoaderDemo() {
25
25
  <h1>Loader demo</h1>
26
26
  <p>
27
27
  Data loaded before render (no <code>useEffect</code>): <code>{data.loadedAt}</code>
28
- {data.q !== null ? ` · q=${data.q}` : ''}
28
+ {data.q !== null ? `, q=${data.q}` : ''}
29
29
  </p>
30
30
  <p>
31
31
  <button type="button" onClick={() => { router.revalidate(); }}>
@@ -33,13 +33,15 @@ export default function LoaderDemo() {
33
33
  </button>
34
34
  </p>
35
35
  {/* The write half: an action runs on submit, then revalidates this route's loader so
36
- `loadedAt` above updates, read write revalidate, no manual refetch. */}
37
- <Toil.Form action={async (form) => { await wait(500); console.log('saved', form.get('note')); }}>
36
+ `loadedAt` above updates, read, write, revalidate, no manual refetch. */}
37
+ <Toil.Form
38
+ action={async (form) => { await wait(500); console.log('saved', form.get('note')); }}
39
+ revalidate>
38
40
  {({ pending }) => (
39
41
  <>
40
42
  <input name="note" placeholder="Leave a note" disabled={pending} />
41
43
  <button type="submit" disabled={pending}>
42
- {pending ? 'Saving' : 'Save & revalidate'}
44
+ {pending ? 'Saving' : 'Save & revalidate'}
43
45
  </button>
44
46
  </>
45
47
  )}
@@ -1,7 +1,5 @@
1
- import { routes, layout, notFound, globalError } from 'toiljs/routes';
1
+ import { routes, layout, notFound, globalError, slots } from 'toiljs/routes';
2
2
 
3
3
  import './styles/main.css';
4
4
 
5
-
6
-
7
- Toil.mount(routes, layout, notFound, globalError);
5
+ Toil.mount(routes, layout, notFound, globalError, slots);
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.10",
4
+ "version": "0.0.11",
5
5
  "author": "Dacely",
6
- "description": "todo",
6
+ "description": "Full-stack TypeScript framework: a file-based React app with a toilscript-compiled WebAssembly server.",
7
7
  "engines": {
8
8
  "node": ">=24.0.0"
9
9
  },
@@ -136,6 +136,7 @@
136
136
  "eslint": "^10.4.1",
137
137
  "jsdom": "^29.1.1",
138
138
  "micromatch": "^4.0.8",
139
+ "playwright": "^1.60.0",
139
140
  "prettier": "^3.8.3",
140
141
  "react": "^19.2.6",
141
142
  "react-dom": "^19.2.6",
package/src/cli/create.ts CHANGED
@@ -269,10 +269,10 @@ function minimalClient(name: string, features: StyleFeatures): Record<string, st
269
269
  ) + '\n',
270
270
  'client/public/images/.gitkeep': '# Place images and other static assets here; served at /images/*.\n',
271
271
  'client/toil.tsx':
272
- "import { routes, layout, notFound, globalError } from 'toiljs/routes';\n\n" +
272
+ "import { routes, layout, notFound, globalError, slots } from 'toiljs/routes';\n\n" +
273
273
  styleImportLines(features).join('\n') +
274
274
  '\n\n' +
275
- 'Toil.mount(routes, layout, notFound, globalError);\n',
275
+ 'Toil.mount(routes, layout, notFound, globalError, slots);\n',
276
276
  [`client/${styleEntry(features.preprocessor)}`]: DEFAULT_STYLE_CONTENT,
277
277
  'client/components/.gitkeep': '# Place shared React components here.\n',
278
278
  'client/layout.tsx': `import { type ReactNode } from 'react';