@timber-js/app 0.2.0-alpha.153 → 0.2.0-alpha.155

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 (52) hide show
  1. package/agent-skill.md +344 -0
  2. package/dist/cli.d.ts +9 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +41 -4
  5. package/dist/cli.js.map +1 -1
  6. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  7. package/dist/server/internal.js +6 -2
  8. package/dist/server/internal.js.map +1 -1
  9. package/dist/server/pipeline-metadata.d.ts.map +1 -1
  10. package/dist/server/pipeline-phases.d.ts.map +1 -1
  11. package/dist/server/sitemap-handler.d.ts.map +1 -1
  12. package/docs/api/30-api-server.mdx +233 -0
  13. package/docs/api/31-api-client.mdx +161 -0
  14. package/docs/api/32-api-cache.mdx +193 -0
  15. package/docs/api/33-api-search-params.mdx +141 -0
  16. package/docs/api/34-api-config.mdx +189 -0
  17. package/docs/api/35-api-typescript.mdx +170 -0
  18. package/docs/api/36-cli.mdx +143 -0
  19. package/docs/learn/00-introduction.mdx +81 -0
  20. package/docs/learn/01-quick-start.mdx +94 -0
  21. package/docs/learn/02-pages-and-layouts.mdx +138 -0
  22. package/docs/learn/03-fetching-data.mdx +211 -0
  23. package/docs/learn/03b-access-control.mdx +130 -0
  24. package/docs/learn/04-loading-states.mdx +67 -0
  25. package/docs/learn/04b-the-flush-point.mdx +115 -0
  26. package/docs/learn/05-typed-params.mdx +275 -0
  27. package/docs/learn/06-forms-and-actions.mdx +150 -0
  28. package/docs/learn/08-streaming.mdx +146 -0
  29. package/docs/learn/09-caching.mdx +121 -0
  30. package/docs/learn/10-middleware.mdx +156 -0
  31. package/docs/learn/11-error-handling.mdx +147 -0
  32. package/docs/learn/12-client-navigation.mdx +168 -0
  33. package/docs/learn/13-configuration.mdx +117 -0
  34. package/docs/learn/14-deploying.mdx +206 -0
  35. package/docs/more/01-advanced-routing.mdx +139 -0
  36. package/docs/more/02-advanced-forms.mdx +137 -0
  37. package/docs/more/03-coming-from-nextjs.mdx +186 -0
  38. package/docs/more/04-metadata-and-fonts.mdx +193 -0
  39. package/docs/more/04b-mdx.mdx +209 -0
  40. package/docs/more/05-content-collections.mdx +90 -0
  41. package/docs/more/06-instrumentation.mdx +188 -0
  42. package/docs/more/07-security.mdx +123 -0
  43. package/docs/more/40-why-timber.mdx +50 -0
  44. package/docs/more/41-timber-vs-nextjs.mdx +81 -0
  45. package/docs/more/42-timber-vs-others.mdx +68 -0
  46. package/llms.txt +55 -0
  47. package/package.json +6 -2
  48. package/src/cli.ts +62 -3
  49. package/src/client/browser-entry/action-dispatch.ts +14 -0
  50. package/src/server/pipeline-metadata.ts +1 -0
  51. package/src/server/pipeline-phases.ts +4 -1
  52. package/src/server/sitemap-handler.ts +12 -3
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: 'timber.js vs Next.js'
3
+ description: 'An honest comparison of timber.js and Next.js — where they differ and why.'
4
+ slug: 'timber-vs-nextjs'
5
+ ---
6
+
7
+ # timber.js vs Next.js
8
+
9
+ Next.js is the most widely used React framework. It's mature, well-documented, and backed by Vercel. timber.js would not exist without it — the app directory routing model, server components, and many API patterns are directly inspired by Next.js.
10
+
11
+ That said, we made different design decisions in a few areas. Here's an honest comparison.
12
+
13
+ ## HTTP Semantics
14
+
15
+ | Behavior | Next.js | timber.js |
16
+ | ------------------ | ------------------------------------------------ | ------------------------------------------------ |
17
+ | Status codes | Always 200 (streaming commits early) | Real status codes (flush held until shell ready) |
18
+ | 404 pages | 200 + `not-found.tsx` error boundary | HTTP 404 + `404.tsx` |
19
+ | Redirects in pages | Client-side via `redirect()` after 200 | HTTP 302/301 before any bytes sent |
20
+ | Works without JS | Partially (primary content yes, interactions no) | Fully (forms, navigation, status codes all work) |
21
+
22
+ This is the core philosophical difference. Next.js optimizes for earliest possible TTFB. timber.js optimizes for correct HTTP responses, at the cost of ~20ms additional server buffering.
23
+
24
+ ## Streaming
25
+
26
+ Both frameworks support React Suspense streaming. The difference is when it starts:
27
+
28
+ - **Next.js:** Streams immediately. `loading.tsx` renders while data loads. Status code is already committed.
29
+ - **timber.js:** Holds until the shell is ready (everything outside `<Suspense>`). Then streams Suspense content. No `loading.tsx` — `<Suspense>` is opt-in only.
30
+
31
+ ## Routing
32
+
33
+ Both use file-system routing in an `app/` directory. The models are similar:
34
+
35
+ - Layouts, pages, dynamic segments, catch-all routes — same patterns
36
+ - Parallel routes (slots) — both support `@sidebar`, `@modal` patterns
37
+ - Route groups — both support `(group)` directories
38
+
39
+ **Differences:**
40
+
41
+ - **Middleware:** Next.js has a single global `middleware.ts`. timber.js has per-segment `middleware.ts` files.
42
+ - **Authorization:** timber.js has `access.ts` files for per-segment access control with `deny()`. Next.js uses middleware or in-component checks.
43
+ - **Typed routes:** timber.js generates route types at build time — `<Link>` type-checks `href` and params.
44
+
45
+ ## Caching
46
+
47
+ | Behavior | Next.js | timber.js |
48
+ | ----------------- | ---------------------------------------------------- | ----------------------------------------------------- |
49
+ | `fetch()` caching | Cached by default (opt out with `cache: 'no-store'`) | Never cached (explicit is better) |
50
+ | Cache API | `unstable_cache` / `'use cache'` | `timber.cache()` with TTL, tags, staleWhileRevalidate |
51
+ | Revalidation | `revalidateTag()`, `revalidatePath()` | `revalidateTag()` |
52
+
53
+ timber.js takes the position that implicit caching causes more bugs than it prevents. Nothing is cached unless you wrap it in `timber.cache()`.
54
+
55
+ ## Build System
56
+
57
+ | | Next.js | timber.js |
58
+ | ---------------- | ------------------- | ------------------------------------- |
59
+ | Bundler | Turbopack / Webpack | Vite 7 (Rolldown) |
60
+ | Config | `next.config.js` | `vite.config.ts` + `timber.config.ts` |
61
+ | Plugin ecosystem | Next.js-specific | Full Vite plugin ecosystem |
62
+
63
+ ## Platform Support
64
+
65
+ Next.js is optimized for Vercel and works on other platforms through community adapters. timber.js ships a first-party Cloudflare Workers adapter and a Nitro adapter that covers Node.js, Vercel, Bun, Netlify, AWS Lambda, and more.
66
+
67
+ ## When to Use Next.js
68
+
69
+ - You need the largest ecosystem and community support
70
+ - You're deploying to Vercel
71
+ - You need features timber.js hasn't built yet (image optimization, ISR, etc.)
72
+ - Your team is already productive with Next.js
73
+
74
+ ## When to Use timber.js
75
+
76
+ - You care about correct HTTP status codes and headers
77
+ - You want pages that work fully without JavaScript
78
+ - You're deploying to Cloudflare Workers
79
+ - You prefer explicit caching over implicit
80
+ - You want per-segment middleware and authorization
81
+ - You want Vite's build speed and plugin ecosystem
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: 'timber.js vs Other Frameworks'
3
+ description: 'How timber.js compares to Vinext, Remix, and other React server frameworks.'
4
+ slug: 'timber-vs-others'
5
+ ---
6
+
7
+ # timber.js vs Other Frameworks
8
+
9
+ Beyond Next.js, there are several React frameworks worth comparing against. Each makes different trade-offs.
10
+
11
+ ## Vinext (Cloudflare)
12
+
13
+ Vinext is Cloudflare's implementation of Next.js on Vite. It aims for Next.js API compatibility running natively on Cloudflare Workers.
14
+
15
+ **Shared ground:** Both timber.js and Vinext target Cloudflare Workers, use Vite, and support React Server Components.
16
+
17
+ **Key differences:**
18
+
19
+ | | Vinext | timber.js |
20
+ | ------------ | -------------------------------------- | ---------------------------------------- |
21
+ | Goal | Next.js compatibility on Vite | Independent design, different trade-offs |
22
+ | Streaming | Streams immediately (Next.js behavior) | Holds until shell ready |
23
+ | Status codes | 200 for everything (Next.js behavior) | Real HTTP status codes |
24
+ | API surface | Next.js-compatible | Similar but divergent where we disagree |
25
+ | Caching | Next.js caching model | Explicit-only caching |
26
+
27
+ If you want Next.js on Cloudflare with minimal code changes, Vinext is the right choice. If you want a different rendering model with correct HTTP semantics, that's timber.js.
28
+
29
+ ## Remix / React Router
30
+
31
+ Remix (now React Router v7) pioneered many of the ideas timber.js agrees with: progressive enhancement, forms that work without JavaScript, and server-first rendering.
32
+
33
+ **Shared philosophy:**
34
+
35
+ - Forms should work without JS
36
+ - Server rendering is the default
37
+ - Progressive enhancement over client-side-first
38
+
39
+ **Key differences:**
40
+
41
+ | | Remix / React Router | timber.js |
42
+ | ------------- | ------------------------------------ | ---------------------------------------- |
43
+ | Routing model | Flat route config or file convention | Nested `app/` directory (Next.js-style) |
44
+ | Data loading | `loader` / `action` functions | Async server components + server actions |
45
+ | RSC support | Not yet (planned) | Built on RSC from day one |
46
+ | Streaming | Supports `defer()` for streaming | `<Suspense>` with explicit flush point |
47
+ | Build system | Vite (React Router v7) | Vite |
48
+
49
+ Remix's `loader`/`action` model is elegant and well-understood. timber.js bets on RSC as the data loading primitive — your components are your data layer.
50
+
51
+ ## TanStack Start
52
+
53
+ TanStack Start is a full-stack React framework from the TanStack Router team.
54
+
55
+ **Shared ground:** Both use Vite, both aim for good TypeScript support, both support server-side rendering.
56
+
57
+ **Key differences:**
58
+
59
+ | | TanStack Start | timber.js |
60
+ | -------------- | --------------------------------------- | ------------------------------------------ |
61
+ | Routing | TanStack Router (code-based, type-safe) | File-system routing with generated types |
62
+ | Data loading | TanStack Query integration | Async server components + `timber.cache()` |
63
+ | RSC support | Experimental | Core architecture |
64
+ | Platform focus | General-purpose | Cloudflare-first, works everywhere |
65
+
66
+ ## Summary
67
+
68
+ Every framework makes trade-offs. timber.js trades streaming speed for HTTP correctness and trades API surface size for explicitness. If those trade-offs align with how you think about web development, it's worth trying. If they don't, the frameworks above are all excellent choices.
package/llms.txt ADDED
@@ -0,0 +1,55 @@
1
+ # @timber-js/app
2
+
3
+ > Vite-native React Server Components framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript.
4
+
5
+ timber.js is a React framework built on Vite and React Server Components. It uses file-system routing (app/ directory), server components by default, and produces real HTTP status codes. It deploys to Cloudflare Workers, Node.js, Vercel, Netlify, and more via adapters.
6
+
7
+ It is inspired by, but _not_ next.js. Do not assume next.js features work in timber, though the routing is similar, much of the rest diverges.
8
+
9
+ ## Docs
10
+
11
+ Documentation ships with this package in the `docs/` directory. All files are MDX.
12
+
13
+ ### Learn (core concepts, read in order)
14
+
15
+ - [Introduction](docs/learn/00-introduction.mdx)
16
+ - [Quick Start](docs/learn/01-quick-start.mdx)
17
+ - [Pages and Layouts](docs/learn/02-pages-and-layouts.mdx)
18
+ - [Fetching Data](docs/learn/03-fetching-data.mdx)
19
+ - [Access Control](docs/learn/03b-access-control.mdx)
20
+ - [Loading States](docs/learn/04-loading-states.mdx)
21
+ - [The Flush Point](docs/learn/04b-the-flush-point.mdx)
22
+ - [Typed Params](docs/learn/05-typed-params.mdx)
23
+ - [Forms and Actions](docs/learn/06-forms-and-actions.mdx)
24
+ - [Streaming](docs/learn/08-streaming.mdx)
25
+ - [Caching](docs/learn/09-caching.mdx)
26
+ - [Middleware](docs/learn/10-middleware.mdx)
27
+ - [Error Handling](docs/learn/11-error-handling.mdx)
28
+ - [Client Navigation](docs/learn/12-client-navigation.mdx)
29
+ - [Configuration](docs/learn/13-configuration.mdx)
30
+ - [Deploying](docs/learn/14-deploying.mdx)
31
+
32
+ ### More (advanced topics)
33
+
34
+ - [Advanced Routing](docs/more/01-advanced-routing.mdx)
35
+ - [Advanced Forms](docs/more/02-advanced-forms.mdx)
36
+ - [Coming from Next.js](docs/more/03-coming-from-nextjs.mdx)
37
+ - [Metadata and Fonts](docs/more/04-metadata-and-fonts.mdx)
38
+ - [MDX](docs/more/04b-mdx.mdx)
39
+ - [Content Collections](docs/more/05-content-collections.mdx)
40
+ - [Instrumentation & Tracing](docs/more/06-instrumentation.mdx)
41
+ - [Security](docs/more/07-security.mdx)
42
+
43
+ ### API Reference
44
+
45
+ - [@timber-js/app/server](docs/api/30-api-server.mdx)
46
+ - [@timber-js/app/client](docs/api/31-api-client.mdx)
47
+ - [@timber-js/app/cache](docs/api/32-api-cache.mdx)
48
+ - [Search & Segment Params](docs/api/33-api-search-params.mdx)
49
+ - [timber.config.ts](docs/api/34-api-config.mdx)
50
+ - [TypeScript](docs/api/35-api-typescript.mdx)
51
+ - [CLI](docs/api/36-cli.mdx)
52
+
53
+ ## AI Agent Skill
54
+
55
+ A Claude Code skill is included at `agent-skill.md`. Load it for guidance on building timber.js applications.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.153",
3
+ "version": "0.2.0-alpha.155",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -26,6 +26,9 @@
26
26
  "dist",
27
27
  "src",
28
28
  "bin",
29
+ "docs",
30
+ "agent-skill.md",
31
+ "llms.txt",
29
32
  "README.md",
30
33
  "LICENSE"
31
34
  ],
@@ -159,7 +162,8 @@
159
162
  "node": ">=22.18.0"
160
163
  },
161
164
  "scripts": {
162
- "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
165
+ "copy-docs": "node scripts/copy-docs.js",
166
+ "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
163
167
  "typecheck": "tsgo --noEmit"
164
168
  }
165
169
  }
package/src/cli.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  // timber preview — Serve the production build
12
12
  // timber check — Validate types + routes without building
13
13
 
14
- const COMMANDS = ['dev', 'build', 'preview', 'check', 'schema'] as const;
14
+ const COMMANDS = ['dev', 'build', 'preview', 'check', 'schema', 'init'] as const;
15
15
  type Command = (typeof COMMANDS)[number];
16
16
 
17
17
  export interface ParsedArgs {
@@ -37,7 +37,7 @@ export interface CommandOptions {
37
37
  export function parseArgs(args: string[]): ParsedArgs {
38
38
  if (args.length === 0) {
39
39
  throw new Error(
40
- 'No command provided. Usage: timber <dev|build|preview|check|schema> [--config <path>]'
40
+ 'No command provided. Usage: timber <dev|build|preview|check|schema|init> [--config <path>]'
41
41
  );
42
42
  }
43
43
 
@@ -64,7 +64,7 @@ export function parseArgs(args: string[]): ParsedArgs {
64
64
  throw new Error(
65
65
  `Unknown option: ${arg}. The timber CLI only accepts --config <path>.${portHint}`
66
66
  );
67
- } else if (command === 'schema') {
67
+ } else if (command === 'schema' || command === 'init') {
68
68
  rest.push(arg);
69
69
  } else {
70
70
  throw new Error(`Unexpected argument: ${arg}. Usage: timber ${command} [--config <path>]`);
@@ -235,6 +235,62 @@ export async function runSchema(args: string[]): Promise<void> {
235
235
  }
236
236
  }
237
237
 
238
+ // ─── Init Subcommand ─────────────────────────────────────────────────────────
239
+
240
+ /**
241
+ * `timber init` — write AGENTS.md (and CLAUDE.md pointer) to the project root
242
+ * so AI coding agents can discover timber.js documentation and conventions.
243
+ *
244
+ * The content is read from the installed @timber-js/app package itself, so it
245
+ * stays in sync with the installed version.
246
+ */
247
+ export async function runInit(): Promise<void> {
248
+ const { readFileSync, writeFileSync, existsSync } = await import('node:fs');
249
+ const { resolve, dirname } = await import('node:path');
250
+ const { fileURLToPath } = await import('node:url');
251
+
252
+ const root = process.cwd();
253
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
254
+
255
+ const agentSkillPath = resolve(packageRoot, 'agent-skill.md');
256
+ if (!existsSync(agentSkillPath)) {
257
+ throw new Error(
258
+ 'agent-skill.md not found in @timber-js/app package. ' +
259
+ 'This may be a development install — try reinstalling.'
260
+ );
261
+ }
262
+
263
+ const agentSkill = readFileSync(agentSkillPath, 'utf-8');
264
+
265
+ const agentsPath = resolve(root, 'AGENTS.md');
266
+ const claudePath = resolve(root, 'CLAUDE.md');
267
+
268
+ const wrote: string[] = [];
269
+
270
+ if (existsSync(agentsPath)) {
271
+ console.log('[timber] AGENTS.md already exists — skipping (delete it first to regenerate)');
272
+ } else {
273
+ writeFileSync(agentsPath, agentSkill);
274
+ wrote.push('AGENTS.md');
275
+ }
276
+
277
+ if (existsSync(claudePath)) {
278
+ console.log('[timber] CLAUDE.md already exists — skipping (delete it first to regenerate)');
279
+ } else {
280
+ writeFileSync(claudePath, 'Read AGENTS.md for project context and conventions.\n');
281
+ wrote.push('CLAUDE.md');
282
+ }
283
+
284
+ if (wrote.length > 0) {
285
+ console.log(
286
+ `[timber] Created ${wrote.join(' and ')} — AI agents can now discover timber.js docs.`
287
+ );
288
+ console.log('[timber] Full documentation: node_modules/@timber-js/app/docs/');
289
+ } else {
290
+ console.log('[timber] Nothing to do — both files already exist.');
291
+ }
292
+ }
293
+
238
294
  // ─── Main Entry Point ────────────────────────────────────────────────────────
239
295
 
240
296
  async function main(): Promise<void> {
@@ -257,6 +313,9 @@ async function main(): Promise<void> {
257
313
  case 'schema':
258
314
  await runSchema(parsed.rest);
259
315
  break;
316
+ case 'init':
317
+ await runInit();
318
+ break;
260
319
  }
261
320
  }
262
321
 
@@ -66,6 +66,20 @@ export function setupServerActions(): void {
66
66
  // instead of returning HTTP 302. Perform a client-side SPA navigation.
67
67
  if (hasRedirect) {
68
68
  const wrapper = decoded as { _redirect: string; _status: number };
69
+
70
+ // External redirects (e.g., OAuth flows via redirectExternal()) must
71
+ // use a full page navigation — the RSC router can only fetch same-origin
72
+ // payloads. On browsers with the Navigation API, navigation.navigate()
73
+ // happens to perform a real cross-origin navigation, but browsers
74
+ // without it (iPad Safari) would attempt an RSC fetch that fails on CORS.
75
+ const isExternal =
76
+ wrapper._redirect.startsWith('https://') || wrapper._redirect.startsWith('http://');
77
+ if (isExternal) {
78
+ setHardNavigating(true);
79
+ window.location.href = wrapper._redirect;
80
+ return undefined;
81
+ }
82
+
69
83
  try {
70
84
  const router = getRouter();
71
85
  void router.navigate(wrapper._redirect);
@@ -41,6 +41,7 @@ export async function serveStaticMetadataFile(
41
41
  const headers: Record<string, string> = {
42
42
  'Content-Type': isText ? `${contentType}; charset=utf-8` : contentType,
43
43
  'Content-Length': String(body.byteLength),
44
+ 'Cache-Control': 'public, max-age=14400, must-revalidate',
44
45
  };
45
46
 
46
47
  return new Response(body, { status: 200, headers });
@@ -316,7 +316,10 @@ export async function handleRequest(
316
316
  }
317
317
  return new Response(body, {
318
318
  status: 200,
319
- headers: { 'Content-Type': `${contentType}; charset=utf-8` },
319
+ headers: {
320
+ 'Content-Type': `${contentType}; charset=utf-8`,
321
+ 'Cache-Control': 'public, max-age=14400, must-revalidate',
322
+ },
320
323
  });
321
324
  } catch (error) {
322
325
  // Control-flow signals from the metadata handler (e.g. a sitemap that
@@ -111,7 +111,10 @@ export function createAutoSitemapHandler(
111
111
  const xml = result.type === 'single' ? result.xml : result.indexXml;
112
112
  return new Response(xml, {
113
113
  status: 200,
114
- headers: { 'Content-Type': 'application/xml; charset=utf-8' },
114
+ headers: {
115
+ 'Content-Type': 'application/xml; charset=utf-8',
116
+ 'Cache-Control': 'public, max-age=14400, must-revalidate',
117
+ },
115
118
  });
116
119
  }
117
120
 
@@ -125,7 +128,10 @@ export function createAutoSitemapHandler(
125
128
  if (pageIndex === 0) {
126
129
  return new Response(result.xml, {
127
130
  status: 200,
128
- headers: { 'Content-Type': 'application/xml; charset=utf-8' },
131
+ headers: {
132
+ 'Content-Type': 'application/xml; charset=utf-8',
133
+ 'Cache-Control': 'public, max-age=14400, must-revalidate',
134
+ },
129
135
  });
130
136
  }
131
137
  return new Response(null, { status: 404 });
@@ -138,7 +144,10 @@ export function createAutoSitemapHandler(
138
144
  }
139
145
  return new Response(pageXml, {
140
146
  status: 200,
141
- headers: { 'Content-Type': 'application/xml; charset=utf-8' },
147
+ headers: {
148
+ 'Content-Type': 'application/xml; charset=utf-8',
149
+ 'Cache-Control': 'public, max-age=14400, must-revalidate',
150
+ },
142
151
  });
143
152
  }
144
153