create-fate 1.0.0-rc.0 → 1.0.1

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 (43) hide show
  1. package/README.md +2 -0
  2. package/bin/create-fate.mjs +80 -1
  3. package/package.json +2 -2
  4. package/templates/fate/drizzle/AGENTS.md +1 -1
  5. package/templates/fate/drizzle/README.md +45 -10
  6. package/templates/fate/drizzle/_gitignore +2 -0
  7. package/templates/fate/drizzle/client/package.json +2 -2
  8. package/templates/fate/drizzle/client/pages/layout.tsx +31 -29
  9. package/templates/fate/drizzle/client/src/ui/Header.tsx +13 -11
  10. package/templates/fate/drizzle/client/src/ui/PostCard.tsx +6 -3
  11. package/templates/fate/drizzle/client/vite.config.ts +9 -7
  12. package/templates/fate/drizzle/package.json +1 -1
  13. package/templates/fate/drizzle/server/src/app.tsx +4 -0
  14. package/templates/fate/drizzle/server/src/drizzle/seedData.ts +2 -2
  15. package/templates/fate/drizzle/server/src/router.ts +1 -0
  16. package/templates/fate/drizzle/server/src/trpc/init.ts +32 -0
  17. package/templates/fate/drizzle/server/src/trpc/routers/comment.ts +7 -1
  18. package/templates/fate/drizzle/server/src/trpc/routers/post.ts +5 -1
  19. package/templates/fate/drizzle/server/vite.config.ts +1 -1
  20. package/templates/fate/http/AGENTS.md +1 -1
  21. package/templates/fate/http/README.md +45 -10
  22. package/templates/fate/http/_gitignore +2 -0
  23. package/templates/fate/http/client/package.json +2 -2
  24. package/templates/fate/http/client/pages/layout.tsx +21 -29
  25. package/templates/fate/http/client/src/ui/Header.tsx +13 -11
  26. package/templates/fate/http/client/vite.config.ts +9 -7
  27. package/templates/fate/http/package.json +1 -1
  28. package/templates/fate/http/server/src/drizzle/seedData.ts +2 -2
  29. package/templates/fate/prisma/AGENTS.md +1 -1
  30. package/templates/fate/prisma/README.md +59 -12
  31. package/templates/fate/prisma/_gitignore +2 -0
  32. package/templates/fate/prisma/client/package.json +2 -2
  33. package/templates/fate/prisma/client/pages/layout.tsx +21 -29
  34. package/templates/fate/prisma/client/src/ui/Header.tsx +13 -11
  35. package/templates/fate/prisma/client/vite.config.ts +9 -7
  36. package/templates/fate/prisma/package.json +1 -1
  37. package/templates/fate/prisma/server/src/prisma/seedData.ts +2 -2
  38. package/templates/fate/void/README.md +42 -5
  39. package/templates/fate/void/db/migrations/20260508120500_seed_void_demo.sql +3 -3
  40. package/templates/fate/void/package.json +5 -2
  41. package/templates/fate/void/seedData.ts +3 -3
  42. package/templates/fate/void/src/ui/Header.tsx +11 -11
  43. package/templates/fate/void/vite.config.ts +10 -3
package/README.md CHANGED
@@ -6,6 +6,8 @@ Create a new fate app:
6
6
  vp create fate my-app
7
7
  ```
8
8
 
9
+ The generated app is installed and the fate client is generated during creation.
10
+
9
11
  Choose between these templates:
10
12
 
11
13
  - `void`: Void pages router with Drizzle.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
  import process from 'node:process';
@@ -37,6 +38,7 @@ Create a new fate app.
37
38
 
38
39
  Options:
39
40
  --template, -t Template variant to create
41
+ --no-setup Skip dependency installation and fate client generation
40
42
  --help, -h Show this help message
41
43
  `);
42
44
  };
@@ -49,6 +51,7 @@ const normalizePackageName = (name) =>
49
51
  .replaceAll(/^-+|-+$/g, '') || 'my-app';
50
52
 
51
53
  const parseArgs = (args) => {
54
+ let setup = true;
52
55
  let targetDir;
53
56
  let variant;
54
57
 
@@ -58,6 +61,11 @@ const parseArgs = (args) => {
58
61
  return { help: true };
59
62
  }
60
63
 
64
+ if (arg === '--no-setup') {
65
+ setup = false;
66
+ continue;
67
+ }
68
+
61
69
  if (arg === '--template' || arg === '-t' || arg === '--variant') {
62
70
  variant = args[++index];
63
71
  continue;
@@ -81,7 +89,7 @@ const parseArgs = (args) => {
81
89
  throw new Error(`Unexpected argument: ${arg}`);
82
90
  }
83
91
 
84
- return { targetDir, variant };
92
+ return { setup, targetDir, variant };
85
93
  };
86
94
 
87
95
  const validateTargetDir = (value) => {
@@ -267,6 +275,70 @@ const restoreTemplateFileNames = (dir) => {
267
275
  }
268
276
  };
269
277
 
278
+ const copyExampleEnvFiles = (dir) => {
279
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
280
+ const entryPath = path.join(dir, entry.name);
281
+ if (entry.isDirectory()) {
282
+ copyExampleEnvFiles(entryPath);
283
+ continue;
284
+ }
285
+
286
+ if (!entry.name.endsWith('.env.example')) {
287
+ continue;
288
+ }
289
+
290
+ const envPath = path.join(dir, entry.name.slice(0, -'.example'.length));
291
+ if (!fs.existsSync(envPath)) {
292
+ fs.copyFileSync(entryPath, envPath);
293
+ }
294
+ }
295
+ };
296
+
297
+ const runCommand = (command, args, cwd) => {
298
+ const result = spawnSync(command, args, {
299
+ cwd,
300
+ shell: process.platform === 'win32',
301
+ stdio: 'inherit',
302
+ });
303
+
304
+ if (result.error) {
305
+ throw result.error;
306
+ }
307
+
308
+ if (result.status !== 0) {
309
+ throw new Error(`Command failed: ${[command, ...args].join(' ')}`);
310
+ }
311
+ };
312
+
313
+ const setupProject = (targetPath, selectedVariant) => {
314
+ runCommand('vp', ['install'], targetPath);
315
+
316
+ switch (selectedVariant) {
317
+ case 'prisma':
318
+ runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
319
+ runCommand('vp', ['run', 'fate:generate'], targetPath);
320
+ break;
321
+ case 'void':
322
+ runCommand('vp', ['run', 'prepare:void'], targetPath);
323
+ runCommand('vp', ['run', 'fate:generate'], targetPath);
324
+ break;
325
+ default:
326
+ runCommand('vp', ['run', 'fate:generate'], targetPath);
327
+ break;
328
+ }
329
+ };
330
+
331
+ const printReadme = (targetPath) => {
332
+ const readmePath = path.join(targetPath, 'README.md');
333
+ if (!fs.existsSync(readmePath)) {
334
+ return;
335
+ }
336
+
337
+ process.stdout.write(`\nNext steps are in README.md. Follow these instructions:\n\n`);
338
+ process.stdout.write(fs.readFileSync(readmePath, 'utf8'));
339
+ process.stdout.write('\n');
340
+ };
341
+
270
342
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
271
343
 
272
344
  const resolveTemplateRoot = (template) => {
@@ -310,6 +382,7 @@ const main = async () => {
310
382
  fs.mkdirSync(targetPath, { recursive: true });
311
383
  fs.cpSync(templateRoot, targetPath, { recursive: true });
312
384
  restoreTemplateFileNames(targetPath);
385
+ copyExampleEnvFiles(targetPath);
313
386
  updatePackageJsonFiles(
314
387
  targetPath,
315
388
  targetPath,
@@ -317,12 +390,18 @@ const main = async () => {
317
390
  fateDependencyVersions,
318
391
  );
319
392
 
393
+ if (options.setup) {
394
+ setupProject(targetPath, selectedVariant);
395
+ }
396
+
320
397
  const message = `Created ${variants[selectedVariant].label} fate app in ${targetDir}`;
321
398
  if (interactive) {
322
399
  outro(message);
323
400
  } else {
324
401
  process.stdout.write(`${message}\n`);
325
402
  }
403
+
404
+ printReadme(targetPath);
326
405
  };
327
406
 
328
407
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fate",
3
- "version": "1.0.0-rc.0",
3
+ "version": "1.0.1",
4
4
  "description": "Create a new fate app.",
5
5
  "homepage": "https://github.com/nkzw-tech/fate",
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "type": "module",
24
24
  "dependencies": {
25
- "@clack/prompts": "^0.11.0"
25
+ "@clack/prompts": "^1.4.0"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=24.0.0"
@@ -26,7 +26,7 @@ This guidance is for agents working in projects bootstrapped from the fate templ
26
26
  - **Do not duplicate types:** Keep generated tRPC types and views imported from the server package. Avoid redefining entity shapes on the client. This ensures view selections stay type-safe end-to-end.
27
27
  - **`useRequest` only at the root:** When adding routes or layouts, create a dedicated `useRequest` call per screen root that pulls together all child views; do not scatter `useRequest` across leaf components unless it's necessary to issue requests due to user input.
28
28
  - **React Actions:** Prefer React Action-friendly UI primitives (buttons/forms that accept an `action` prop). If unavailable, wrap action calls with `startTransition` in `onClick` handlers.
29
- - **Generated Client** The fate client is generated via `pnpm fate:generate`. It should not be manually edited. Instead make the proper schema changes on the server and run `pnpm fate:generate`.
29
+ - **fate client support** fate's local client support files are maintained by the Vite/fate tooling. Do not manually edit `.fate` files; make the proper schema changes on the server and run `pnpm fate:generate` when working outside Vite dev.
30
30
  - **Library Versions:** This repository uses the most recent releases of React, fate, Vite, tRPC, and more. You might not know about the new releases yet, please don't get confused. The versions you see are real, and there are no feature or version mismatches. You can search the internet for more information about them.
31
31
 
32
32
  Full documentation can be found on the filesystem at `./client/node_modules/react-fate/README.md` or online at [fate.technology](https://fate.technology/).
@@ -57,10 +57,25 @@ Next to [_fate_](https://fate.technology), it comes with the following technolog
57
57
 
58
58
  You'll need Node.js 24+ and [Vite+](https://viteplus.dev/guide/).
59
59
 
60
- - Run `vp install`.
61
- - Copy `server/.env.example` to `server/.env`.
62
- - Set up a Postgres database locally or run `docker-compose up -d` to start Postgres in a Docker container.
63
- - Postgres setup:
60
+ Install dependencies:
61
+
62
+ ```bash
63
+ vp install
64
+ ```
65
+
66
+ Review `server/.env`, which is copied from `server/.env.example` when the app is created. The default local values expect:
67
+
68
+ - Postgres at `postgresql://fate:echo@localhost:5432/fate`.
69
+ - The server at `http://localhost:9000`.
70
+ - The client at `http://localhost:5173`.
71
+
72
+ Start Postgres with Docker:
73
+
74
+ ```bash
75
+ docker-compose up -d
76
+ ```
77
+
78
+ Alternatively, create the database manually:
64
79
 
65
80
  ```SQL
66
81
  CREATE ROLE fate WITH LOGIN PASSWORD 'echo';
@@ -68,10 +83,30 @@ CREATE DATABASE fate;
68
83
  ALTER DATABASE fate OWNER TO fate;
69
84
  ```
70
85
 
71
- Then, at the root of the project, run:
86
+ Then set up the schema, seed data, translations, and fate client support:
87
+
88
+ ```bash
89
+ vp run dev:setup
90
+ ```
91
+
92
+ Start the app:
93
+
94
+ ```bash
95
+ vp run dev
96
+ ```
97
+
98
+ The client runs at `http://localhost:5173` and the server runs at `http://localhost:9000`. tRPC requests go to `/trpc`; fate live updates use the SSE endpoint under `/fate/live`.
99
+
100
+ ## Development
101
+
102
+ Common commands from the project root:
72
103
 
73
- - `vp run dev:setup` to create the database tables and seed initial data.
74
- - Run `vp run fate:generate` to regenerate the fate client code.
75
- - Run `vp test` to run all tests.
76
- - Run `vp run dev` to run the client and server.
77
- - Visit `http://localhost:5173` to see the app in action.
104
+ - `vp run dev` starts the client and server together.
105
+ - `vp run dev:client` starts only the client.
106
+ - `vp run dev:server` starts only the server.
107
+ - `vp run dev:setup` pushes the Drizzle schema, seeds the database, runs fbtee setup, and prepares fate client support.
108
+ - `vp run fate:generate` refreshes fate client support after changing server views, roots, or routers.
109
+ - `vp run drizzle` opens Drizzle Kit commands for the server package.
110
+ - `vp check --fix` formats, lints, and type-checks the workspace.
111
+ - `vp test` runs the test suite.
112
+ - `vp run build` builds the client and server.
@@ -12,6 +12,8 @@ client/source_strings.json
12
12
  client/src/translations/
13
13
  coverage/
14
14
  dist/
15
+ server/.env
16
+ server/.prod.env
15
17
  server/dist/
16
18
  server/src/prisma/prisma-client/
17
19
  tsconfig.tsbuildinfo
@@ -23,7 +23,7 @@
23
23
  "@nkzw/stack": "^2.3.2",
24
24
  "@radix-ui/react-slot": "^1.2.4",
25
25
  "@trpc/client": "^11.17.0",
26
- "@void/react": "^0.7.5",
26
+ "@void/react": "^0.7.6",
27
27
  "better-auth": "^1.6.9",
28
28
  "class-variance-authority": "^0.7.1",
29
29
  "clsx": "^2.1.1",
@@ -34,7 +34,7 @@
34
34
  "react-error-boundary": "^6.1.1",
35
35
  "react-fate": "latest",
36
36
  "tailwind-merge": "^3.5.0",
37
- "void": "^0.7.5"
37
+ "void": "^0.7.6"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@nkzw/babel-preset-fbtee": "^1.7.4",
@@ -53,42 +53,44 @@ export default function Layout({ children }: { children: ReactNode }) {
53
53
  url: `${env('SERVER_URL')}/trpc`,
54
54
  }),
55
55
  ],
56
+ liveUrl: `${env('SERVER_URL')}/fate`,
57
+ ...(userId
58
+ ? {
59
+ fetch: (input: RequestInfo | URL, init?: RequestInit) =>
60
+ fetch(input, {
61
+ ...init,
62
+ credentials: 'include',
63
+ }),
64
+ }
65
+ : null),
56
66
  }),
57
67
  [userId],
58
68
  );
59
69
 
60
- if (isPending) {
61
- return (
62
- <LocaleContext>
63
- <div className="min-h-screen bg-background text-foreground">
64
- <div className="min-h-screen bg-[radial-gradient(circle_at_20%_20%,rgba(59,130,246,0.08),transparent_35%),radial-gradient(circle_at_80%_0,rgba(99,102,241,0.08),transparent_28%)]">
65
- <Thinking />
66
- </div>
67
- </div>
68
- </LocaleContext>
69
- );
70
- }
71
-
72
70
  return (
73
71
  <LocaleContext>
74
- <FateClient client={fate} key={userId}>
75
- <div className="min-h-screen bg-background text-foreground">
76
- <div className="min-h-screen bg-[radial-gradient(circle_at_20%_20%,rgba(59,130,246,0.08),transparent_35%),radial-gradient(circle_at_80%_0,rgba(99,102,241,0.08),transparent_28%)]">
77
- <Header />
78
- <ErrorBoundary
79
- fallbackRender={({ error }) => (
80
- <Section>
81
- <Card>
82
- <Error error={error} />
83
- </Card>
84
- </Section>
85
- )}
86
- >
87
- <Suspense fallback={<Thinking />}>{children}</Suspense>
88
- </ErrorBoundary>
89
- </div>
72
+ <div className="min-h-screen bg-background text-foreground">
73
+ <div className="min-h-screen bg-[radial-gradient(circle_at_20%_20%,rgba(59,130,246,0.08),transparent_35%),radial-gradient(circle_at_80%_0,rgba(99,102,241,0.08),transparent_28%)]">
74
+ <Header />
75
+ {isPending ? (
76
+ <Thinking />
77
+ ) : (
78
+ <FateClient client={fate} key={userId}>
79
+ <ErrorBoundary
80
+ fallbackRender={({ error }) => (
81
+ <Section>
82
+ <Card>
83
+ <Error error={error} />
84
+ </Card>
85
+ </Section>
86
+ )}
87
+ >
88
+ <Suspense fallback={<Thinking />}>{children}</Suspense>
89
+ </ErrorBoundary>
90
+ </FateClient>
91
+ )}
90
92
  </div>
91
- </FateClient>
93
+ </div>
92
94
  </LocaleContext>
93
95
  );
94
96
  }
@@ -65,9 +65,9 @@ export default function Header() {
65
65
  </span>
66
66
  </Link>
67
67
  </Button>
68
- {session ? (
69
- <>
70
- <Button asChild size="sm" variant="ghost">
68
+ <div className="flex w-9 justify-end sm:w-24">
69
+ {session ? (
70
+ <Button asChild className="w-full" size="sm" variant="ghost">
71
71
  <Stack
72
72
  alignCenter
73
73
  as="a"
@@ -81,14 +81,16 @@ export default function Header() {
81
81
  </span>
82
82
  </Stack>
83
83
  </Button>
84
- </>
85
- ) : !isPending ? (
86
- <Button asChild size="sm" variant="ghost">
87
- <Link to="/login">
88
- <LogIn className="h-4 w-4" /> <fbt desc="Login button">Login</fbt>
89
- </Link>
90
- </Button>
91
- ) : null}
84
+ ) : !isPending ? (
85
+ <Button asChild className="w-full" size="sm" variant="ghost">
86
+ <Link to="/login">
87
+ <LogIn className="h-4 w-4" /> <fbt desc="Login button">Login</fbt>
88
+ </Link>
89
+ </Button>
90
+ ) : (
91
+ <div aria-hidden className="h-9 w-full" />
92
+ )}
93
+ </div>
92
94
  </Stack>
93
95
  </Stack>
94
96
  </header>
@@ -11,7 +11,7 @@ import {
11
11
  useState,
12
12
  } from 'react';
13
13
  import { ErrorBoundary } from 'react-error-boundary';
14
- import { useFateClient, useListView, useView, view, ViewRef } from 'react-fate';
14
+ import { useFateClient, useLiveListView, useLiveView, useView, view, ViewRef } from 'react-fate';
15
15
  import { Button } from '../ui/Button.tsx';
16
16
  import Card from '../ui/Card.tsx';
17
17
  import AuthClient from '../user/AuthClient.tsx';
@@ -25,6 +25,9 @@ const CommentConnectionView = {
25
25
  items: {
26
26
  node: CommentView,
27
27
  },
28
+ live: {
29
+ append: 'visible',
30
+ },
28
31
  };
29
32
 
30
33
  export const PostView = view<Post>()({
@@ -123,9 +126,9 @@ const CommentInput = ({
123
126
 
124
127
  export function PostCard({ detail, post: postRef }: { detail?: boolean; post: ViewRef<'Post'> }) {
125
128
  const fate = useFateClient();
126
- const post = useView(PostView, postRef);
129
+ const post = useLiveView(PostView, postRef);
127
130
  const author = useView(UserView, post.author);
128
- const [comments, loadNext] = useListView(CommentConnectionView, post.comments);
131
+ const [comments, loadNext] = useLiveListView(CommentConnectionView, post.comments);
129
132
 
130
133
  const [likeResult, likeAction, likeIsPending] = useActionState(fate.actions.post.like, null);
131
134
 
@@ -6,7 +6,7 @@ import { reactCompilerPreset } from '@vitejs/plugin-react';
6
6
  import { voidReact } from '@void/react/plugin';
7
7
  import dotenv from 'dotenv';
8
8
  import { fate } from 'react-fate/vite';
9
- import { defineConfig } from 'vite-plus';
9
+ import { defineConfig, lazyPlugins } from 'vite-plus';
10
10
  import { voidPlugin } from 'void';
11
11
 
12
12
  const root = process.cwd();
@@ -24,12 +24,14 @@ if (!process.env.VITE_SERVER_URL) {
24
24
  export default defineConfig({
25
25
  build: { outDir: join(root, '../dist/client') },
26
26
  plugins: [
27
- babel({
28
- presets: [fbteePreset, reactCompilerPreset()],
29
- }),
30
- tailwindcss(),
31
- voidPlugin(),
32
- voidReact(),
27
+ ...(lazyPlugins(() => [
28
+ babel({
29
+ presets: [fbteePreset, reactCompilerPreset()],
30
+ }),
31
+ tailwindcss(),
32
+ voidPlugin(),
33
+ voidReact(),
34
+ ]) ?? []),
33
35
  fate({
34
36
  module: '@app/server/src/router.ts',
35
37
  }),
@@ -14,7 +14,7 @@
14
14
  "dev": "npm-run-all --parallel dev:client dev:server",
15
15
  "dev:client": "cd client && vp dev",
16
16
  "dev:server": "cd server && vp run dev",
17
- "dev:setup": "vp run --filter '@app/client' --filter '@app/server' dev:setup",
17
+ "dev:setup": "vp run --filter '@app/client' --filter '@app/server' dev:setup && vp run fate:generate",
18
18
  "drizzle": "vp run --filter '@app/server' drizzle",
19
19
  "fate:generate": "vp run --filter '@app/client' fate:generate",
20
20
  "prepare": "vp config"
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env NODE_ENV=development node_modules/.bin/nodemon -q -I --exec node --no-warnings --experimental-specifier-resolution=node --loader ts-node/esm --env-file .env
2
2
  import { styleText } from 'node:util';
3
3
  import { trpcServer } from '@hono/trpc-server';
4
+ import { createHonoFateHandler } from '@nkzw/fate/server';
4
5
  import { Hono } from 'hono';
5
6
  import { cors } from 'hono/cors';
6
7
  import { connectDatabase } from './drizzle/db.ts';
@@ -8,6 +9,7 @@ import { auth } from './lib/auth.ts';
8
9
  import { clientOrigin, resolveCorsOrigin } from './lib/origins.ts';
9
10
  import { appRouter } from './router.ts';
10
11
  import { createContext } from './trpc/context.ts';
12
+ import { fateServer } from './trpc/init.ts';
11
13
 
12
14
  try {
13
15
  await connectDatabase();
@@ -33,6 +35,8 @@ app.use(
33
35
  }),
34
36
  );
35
37
 
38
+ app.all('/fate/*', createHonoFateHandler(fateServer));
39
+
36
40
  app.on(['POST', 'GET'], '/api/auth/*', ({ req }) => auth.handler(req.raw));
37
41
 
38
42
  app.all('/*', (context) => context.redirect(clientOrigin));
@@ -126,7 +126,7 @@ export const posts = [
126
126
  {
127
127
  authorEmail: 'alex@example.com',
128
128
  content:
129
- 'The launch post introduced fate with tRPC, but the repo now includes a native HTTP transport too. The generated client can point at a fate endpoint, send requests through the protocol package, and use GET and POST routes for live updates.',
129
+ 'The launch post introduced fate with tRPC, but the repo now includes a native HTTP transport too. The Vite plugin wires the client to a fate endpoint, sends requests through the protocol package, and uses GET and POST routes for live updates.',
130
130
  likes: 88,
131
131
  title: 'Native HTTP transport after the alpha launch',
132
132
  },
@@ -191,7 +191,7 @@ export const comments = [
191
191
  'The stable ref change sounds small, but it explains a lot of the rerender fixes in the history.',
192
192
  'I used the strict selection post to explain why overfetching is not just a network problem.',
193
193
  'The native protocol should make it easier to build examples outside of Hono and tRPC.',
194
- 'The Vite plugin note clarifies why the generated client is still present without making codegen feel mandatory.',
194
+ 'The Vite plugin note clarifies how fate connects the client APIs without making codegen part of the app workflow.',
195
195
  'The migration sequence matches how we would try this inside an existing dashboard.',
196
196
  'The comments list is long enough now to exercise load-more behavior without creating fake lorem ipsum.',
197
197
  'Seeing optimistic actions and live updates use the same normalized cache is the key idea.',
@@ -11,4 +11,5 @@ export const appRouter = router({
11
11
 
12
12
  export type AppRouter = typeof appRouter;
13
13
 
14
+ export { fateServer } from './trpc/init.ts';
14
15
  export * from './trpc/views.ts';
@@ -1,5 +1,7 @@
1
+ import { createFateServer, createLiveEventBus } from '@nkzw/fate/server';
1
2
  import { createDrizzleFate } from '@nkzw/fate/server/drizzle';
2
3
  import { initTRPC } from '@trpc/server';
4
+ import type { Context } from 'hono';
3
5
  import db from '../drizzle/db.ts';
4
6
  import schema from '../drizzle/schema.ts';
5
7
  import type { AppContext } from './context.ts';
@@ -10,6 +12,7 @@ const t = initTRPC.context<AppContext>().create();
10
12
  export const router = t.router;
11
13
  export const procedure = t.procedure;
12
14
  export const middleware = t.middleware;
15
+ export const live = createLiveEventBus();
13
16
 
14
17
  export const fate = createDrizzleFate<AppContext, typeof procedure>({
15
18
  db,
@@ -17,3 +20,32 @@ export const fate = createDrizzleFate<AppContext, typeof procedure>({
17
20
  schema,
18
21
  views: Root,
19
22
  });
23
+
24
+ export const fateServer = createFateServer<AppContext>({
25
+ context: async ({ adapterContext }) => {
26
+ const { createContext } = await import('./context.ts');
27
+ return createContext({ context: adapterContext as Context });
28
+ },
29
+ live,
30
+ queries: {
31
+ viewer: {
32
+ resolve: ({
33
+ ctx,
34
+ input,
35
+ }: {
36
+ ctx: AppContext;
37
+ input: { args?: Record<string, unknown>; select: Array<string> };
38
+ }) =>
39
+ ctx.sessionUser
40
+ ? fate.resolveById({
41
+ ctx,
42
+ id: ctx.sessionUser.id,
43
+ input,
44
+ view: Root.viewer,
45
+ })
46
+ : null,
47
+ },
48
+ },
49
+ roots: Root,
50
+ sources: fate,
51
+ });
@@ -9,7 +9,7 @@ import {
9
9
  findCommentPostId,
10
10
  postExists,
11
11
  } from '../../drizzle/queries.ts';
12
- import { fate, procedure, router } from '../init.ts';
12
+ import { fate, live, procedure, router } from '../init.ts';
13
13
  import type { CommentItem } from '../views.ts';
14
14
  import { commentDataView, postSummaryDataView } from '../views.ts';
15
15
 
@@ -68,6 +68,9 @@ export const commentRouter = router({
68
68
  });
69
69
  }
70
70
 
71
+ live.connection('Post.comments', { id: input.postId }).appendNode('Comment', commentId);
72
+ live.update('Post', input.postId, { changed: ['commentCount', 'comments'] });
73
+
71
74
  return result as CommentItem & { post?: { commentCount: number } };
72
75
  }),
73
76
  delete: procedure
@@ -102,6 +105,9 @@ export const commentRouter = router({
102
105
  view: postSummaryDataView,
103
106
  });
104
107
 
108
+ live.connection('Post.comments', { id: postId }).deleteEdge('Comment', input.id);
109
+ live.update('Post', postId, { changed: ['commentCount', 'comments'] });
110
+
105
111
  return {
106
112
  id: input.id,
107
113
  post,
@@ -2,7 +2,7 @@ import { connectionArgs } from '@nkzw/fate/server';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { z } from 'zod';
4
4
  import { likePostRecord, unlikePostRecord } from '../../drizzle/queries.ts';
5
- import { fate, procedure, router } from '../init.ts';
5
+ import { fate, live, procedure, router } from '../init.ts';
6
6
  import { Post, postDataView } from '../views.ts';
7
7
 
8
8
  export const postRouter = router({
@@ -58,6 +58,8 @@ export const postRouter = router({
58
58
  });
59
59
  }
60
60
 
61
+ live.update('Post', input.id, { changed: ['likes'] });
62
+
61
63
  return post as Post;
62
64
  }),
63
65
  unlike: procedure
@@ -90,6 +92,8 @@ export const postRouter = router({
90
92
  });
91
93
  }
92
94
 
95
+ live.update('Post', input.id, { changed: ['likes'] });
96
+
93
97
  return post as Post;
94
98
  }),
95
99
  });
@@ -3,6 +3,6 @@ import { defineConfig } from 'vite-plus';
3
3
  export default defineConfig({
4
4
  pack: {
5
5
  entry: ['./src/app.tsx'],
6
- outputOptions: { file: 'dist/index.js' },
6
+ outputOptions: { codeSplitting: false, file: 'dist/index.js' },
7
7
  },
8
8
  });
@@ -26,7 +26,7 @@ This guidance is for agents working in projects bootstrapped from the fate templ
26
26
  - **Do not duplicate types:** Keep generated fate types and views imported from the server package. Avoid redefining entity shapes on the client. This ensures view selections stay type-safe end-to-end.
27
27
  - **`useRequest` only at the root:** When adding routes or layouts, create a dedicated `useRequest` call per screen root that pulls together all child views; do not scatter `useRequest` across leaf components unless it's necessary to issue requests due to user input.
28
28
  - **React Actions:** Prefer React Action-friendly UI primitives (buttons/forms that accept an `action` prop). If unavailable, wrap action calls with `startTransition` in `onClick` handlers.
29
- - **Generated Client** The fate client is generated via `pnpm fate:generate`. It should not be manually edited. Instead make the proper schema changes on the server and run `pnpm fate:generate`.
29
+ - **fate client support** fate's local client support files are maintained by the Vite/fate tooling. Do not manually edit `.fate` files; make the proper schema changes on the server and run `pnpm fate:generate` when working outside Vite dev.
30
30
  - **Library Versions:** This repository uses the most recent releases of React, fate, Vite, and more. You might not know about the new releases yet, please don't get confused. The versions you see are real, and there are no feature or version mismatches. You can search the internet for more information about them.
31
31
 
32
32
  Full documentation can be found on the filesystem at `./client/node_modules/react-fate/README.md` or online at [fate.technology](https://fate.technology/).