create-fate 1.0.0-rc.0 → 1.0.0

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.
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.0",
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"
@@ -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 generated fate client:
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 regenerates the fate client.
108
+ - `vp run fate:generate` regenerates `client/.fate/client.generated.ts` 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
@@ -53,6 +53,16 @@ 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
  );
@@ -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));
@@ -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
  });
@@ -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 generated fate client:
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`. Native fate requests go to `/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 regenerates the fate client.
108
+ - `vp run fate:generate` regenerates `client/.fate/client.generated.ts` after changing server views, roots, or mutations.
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
@@ -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
  transport: 'native',
@@ -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"
@@ -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,12 +83,44 @@ 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
+ For local development, push the Prisma schema and seed the database:
87
+
88
+ ```bash
89
+ vp run prisma db push
90
+ vp run prisma db seed
91
+ ```
92
+
93
+ If you prefer migration files, use Prisma's migration workflow instead:
94
+
95
+ ```bash
96
+ vp run prisma migrate dev --name init
97
+ vp run prisma db seed
98
+ ```
99
+
100
+ Then generate local files:
101
+
102
+ ```bash
103
+ vp run dev:setup
104
+ ```
105
+
106
+ Start the app:
107
+
108
+ ```bash
109
+ vp run dev
110
+ ```
111
+
112
+ The client runs at `http://localhost:5173` and the server runs at `http://localhost:9000`. tRPC requests go to `/trpc`.
113
+
114
+ ## Development
115
+
116
+ Common commands from the project root:
72
117
 
73
- - `vp run prisma migrate dev` to create the database and run the migrations.
74
- - You might want to run `vp run prisma migrate reset` and `vp run prisma db seed` to seed the database with initial data.
75
- - Run `vp run dev:setup` to generate the Prisma client.
76
- - Run `vp run fate:generate` to regenerate the fate client code.
77
- - Run `vp test` to run all tests.
78
- - Run `vp run dev` to run the client and server.
79
- - Visit `http://localhost:5173` to see the app in action.
118
+ - `vp run dev` starts the client and server together.
119
+ - `vp run dev:client` starts only the client.
120
+ - `vp run dev:server` starts only the server.
121
+ - `vp run dev:setup` generates the Prisma client, runs fbtee setup, and regenerates the fate client.
122
+ - `vp run fate:generate` regenerates `client/.fate/client.generated.ts` after changing server views, roots, or routers.
123
+ - `vp run prisma` runs Prisma CLI commands in the server package.
124
+ - `vp check --fix` formats, lints, and type-checks the workspace.
125
+ - `vp test` runs the test suite.
126
+ - `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
@@ -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
  "fate:generate": "vp run --filter '@app/client' fate:generate",
19
19
  "prepare": "vp config",
20
20
  "prisma": "vp run --filter '@app/server' prisma"
@@ -6,13 +6,50 @@ Create a new fate app with Vite+:
6
6
  vp create fate my-app --template void
7
7
  ```
8
8
 
9
- This template uses the Void pages router with Drizzle, `void-fate`, `react-fate`, Better Auth, and Tailwind.
9
+ This template uses the Void pages router with Drizzle, `void-fate`, `react-fate`, Better Auth, Tailwind, and React Compiler.
10
+
11
+ ## Folder Structure
12
+
13
+ - `pages/` - Void page routes.
14
+ - `routes/` - API routes, including fate RPC and live SSE routes.
15
+ - `src/` - Shared application, fate, UI, and auth code.
16
+ - `db/` - Drizzle schema, migrations, queries, and seed data.
10
17
 
11
18
  ## Initial Setup
12
19
 
13
20
  You'll need Node.js 24+ and [Vite+](https://viteplus.dev/guide/).
14
21
 
15
- - Run `vp install`.
16
- - Run `vp run dev:setup` to prepare Void, seed the local database, and generate the fate client.
17
- - Run `vp test` to run all tests.
18
- - Run `vp run dev` to run the app.
22
+ Install dependencies:
23
+
24
+ ```bash
25
+ vp install
26
+ ```
27
+
28
+ Set up Void local files, seed the local database, and regenerate the fate client:
29
+
30
+ ```bash
31
+ vp run dev:setup
32
+ ```
33
+
34
+ Start the app:
35
+
36
+ ```bash
37
+ vp run dev
38
+ ```
39
+
40
+ The app runs at `http://localhost:6001`. Fate RPC requests go to `/fate`; live updates use the SSE route at `/fate-live`.
41
+
42
+ ## Development
43
+
44
+ Common commands from the project root:
45
+
46
+ - `vp run dev` starts the Void app.
47
+ - `vp run dev:setup` prepares Void, seeds the local database, and regenerates the fate client.
48
+ - `vp run prepare:void` regenerates Void local files.
49
+ - `vp run fate:generate` regenerates `.fate/client.generated.ts` after changing `src/fate/server.ts` or views.
50
+ - `vp run db:generate` generates Drizzle migration files from `db/schema.ts`.
51
+ - `vp run db:migrate` applies database migrations.
52
+ - `vp run db:seed` seeds the local database.
53
+ - `vp check --fix` formats, lints, and type-checks the workspace.
54
+ - `vp test` runs the test suite.
55
+ - `vp run build` builds the app.
@@ -40,10 +40,13 @@
40
40
  "zod": "^4.4.3"
41
41
  },
42
42
  "devDependencies": {
43
+ "@rolldown/plugin-babel": "^0.2.3",
43
44
  "@tailwindcss/vite": "^4.2.4",
44
45
  "@types/node": "^25.6.0",
45
46
  "@types/react": "^19.2.14",
46
47
  "@types/react-dom": "^19.2.3",
48
+ "@vitejs/plugin-react": "^6.0.1",
49
+ "babel-plugin-react-compiler": "^1.0.0",
47
50
  "tailwindcss": "^4.2.4",
48
51
  "ts-node": "^10.9.2",
49
52
  "vite": "catalog:",
@@ -1,7 +1,9 @@
1
+ import babel from '@rolldown/plugin-babel';
1
2
  import tailwindcss from '@tailwindcss/vite';
3
+ import { reactCompilerPreset } from '@vitejs/plugin-react';
2
4
  import { voidReact } from '@void/react/plugin';
3
5
  import { fate } from 'react-fate/vite';
4
- import { defineConfig } from 'vite-plus';
6
+ import { defineConfig, lazyPlugins } from 'vite-plus';
5
7
  import { voidPlugin } from 'void';
6
8
 
7
9
  const isTest = process.env.NODE_ENV === 'test' || process.env.VITEST;
@@ -63,8 +65,13 @@ export default defineConfig({
63
65
  },
64
66
  },
65
67
  plugins: [
66
- tailwindcss(),
67
- ...(isTest ? [] : [voidPlugin(), voidReact()]),
68
+ ...(lazyPlugins(() => [
69
+ babel({
70
+ presets: [reactCompilerPreset()],
71
+ }),
72
+ tailwindcss(),
73
+ ...(isTest ? [] : [voidPlugin(), voidReact()]),
74
+ ]) ?? []),
68
75
  fate({
69
76
  module: './src/fate/server.ts',
70
77
  transport: 'void',