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.
- package/README.md +2 -0
- package/bin/create-fate.mjs +80 -1
- package/package.json +2 -2
- package/templates/fate/drizzle/AGENTS.md +1 -1
- package/templates/fate/drizzle/README.md +45 -10
- package/templates/fate/drizzle/_gitignore +2 -0
- package/templates/fate/drizzle/client/package.json +2 -2
- package/templates/fate/drizzle/client/pages/layout.tsx +31 -29
- package/templates/fate/drizzle/client/src/ui/Header.tsx +13 -11
- 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/drizzle/seedData.ts +2 -2
- 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/AGENTS.md +1 -1
- package/templates/fate/http/README.md +45 -10
- package/templates/fate/http/_gitignore +2 -0
- package/templates/fate/http/client/package.json +2 -2
- package/templates/fate/http/client/pages/layout.tsx +21 -29
- package/templates/fate/http/client/src/ui/Header.tsx +13 -11
- package/templates/fate/http/client/vite.config.ts +9 -7
- package/templates/fate/http/package.json +1 -1
- package/templates/fate/http/server/src/drizzle/seedData.ts +2 -2
- package/templates/fate/prisma/AGENTS.md +1 -1
- package/templates/fate/prisma/README.md +59 -12
- package/templates/fate/prisma/_gitignore +2 -0
- package/templates/fate/prisma/client/package.json +2 -2
- package/templates/fate/prisma/client/pages/layout.tsx +21 -29
- package/templates/fate/prisma/client/src/ui/Header.tsx +13 -11
- package/templates/fate/prisma/client/vite.config.ts +9 -7
- package/templates/fate/prisma/package.json +1 -1
- package/templates/fate/prisma/server/src/prisma/seedData.ts +2 -2
- package/templates/fate/void/README.md +42 -5
- package/templates/fate/void/db/migrations/20260508120500_seed_void_demo.sql +3 -3
- package/templates/fate/void/package.json +5 -2
- package/templates/fate/void/seedData.ts +3 -3
- package/templates/fate/void/src/ui/Header.tsx +11 -11
- 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.
|
|
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": "^
|
|
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
|
-
- **
|
|
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
|
-
|
|
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 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
|
|
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 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.
|
|
@@ -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.
|
|
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.
|
|
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
|
-
<
|
|
75
|
-
<div className="min-h-screen bg-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</
|
|
90
|
-
|
|
91
|
-
|
|
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,
|
|
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));
|
|
@@ -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
|
|
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
|
|
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.',
|
|
@@ -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
|
});
|
|
@@ -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
|
-
- **
|
|
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/).
|