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 +2 -0
- package/bin/create-fate.mjs +80 -1
- package/package.json +2 -2
- package/templates/fate/drizzle/README.md +45 -10
- package/templates/fate/drizzle/_gitignore +2 -0
- package/templates/fate/drizzle/client/pages/layout.tsx +10 -0
- package/templates/fate/drizzle/client/src/ui/PostCard.tsx +6 -3
- package/templates/fate/drizzle/client/vite.config.ts +9 -7
- package/templates/fate/drizzle/package.json +1 -1
- package/templates/fate/drizzle/server/src/app.tsx +4 -0
- package/templates/fate/drizzle/server/src/router.ts +1 -0
- package/templates/fate/drizzle/server/src/trpc/init.ts +32 -0
- package/templates/fate/drizzle/server/src/trpc/routers/comment.ts +7 -1
- package/templates/fate/drizzle/server/src/trpc/routers/post.ts +5 -1
- package/templates/fate/drizzle/server/vite.config.ts +1 -1
- package/templates/fate/http/README.md +45 -10
- package/templates/fate/http/_gitignore +2 -0
- package/templates/fate/http/client/vite.config.ts +9 -7
- package/templates/fate/http/package.json +1 -1
- package/templates/fate/prisma/README.md +59 -12
- package/templates/fate/prisma/_gitignore +2 -0
- package/templates/fate/prisma/client/vite.config.ts +9 -7
- package/templates/fate/prisma/package.json +1 -1
- package/templates/fate/void/README.md +42 -5
- package/templates/fate/void/package.json +3 -0
- package/templates/fate/void/vite.config.ts +10 -3
package/README.md
CHANGED
package/bin/create-fate.mjs
CHANGED
|
@@ -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
|
|
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": "^
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
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.
|
|
@@ -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,
|
|
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 =
|
|
129
|
+
const post = useLiveView(PostView, postRef);
|
|
127
130
|
const author = useView(UserView, post.author);
|
|
128
|
-
const [comments, loadNext] =
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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));
|
|
@@ -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
|
});
|
|
@@ -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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
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.
|
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
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.
|
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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',
|