create-fate 1.14.1 → 1.16.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.
Files changed (52) hide show
  1. package/README.md +2 -1
  2. package/bin/create-fate.mjs +64 -24
  3. package/package.json +1 -1
  4. package/templates/fate/_shared/client-only-vue/pages/about.vue +6 -0
  5. package/templates/fate/_shared/client-only-vue/pages/index.vue +72 -0
  6. package/templates/fate/_shared/client-only-vue/pages/layout.vue +30 -0
  7. package/templates/fate/_shared/client-only-vue/vite.config.ts +12 -0
  8. package/templates/fate/_shared/vue-app/package.json +2 -2
  9. package/templates/fate/_shared/vue-app/pages/post/[id].vue +1 -1
  10. package/templates/fate/_shared/vue-app/void.json +3 -0
  11. package/templates/fate/client-only/AGENTS.md +5 -0
  12. package/templates/fate/client-only/README.md +25 -0
  13. package/templates/fate/client-only/_gitignore +6 -0
  14. package/templates/fate/client-only/package.json +47 -0
  15. package/templates/fate/client-only/pages/about.server.ts +1 -0
  16. package/templates/fate/client-only/pages/about.tsx +11 -0
  17. package/templates/fate/client-only/pages/index.server.ts +1 -0
  18. package/templates/fate/client-only/pages/index.tsx +82 -0
  19. package/templates/fate/client-only/pages/layout.tsx +24 -0
  20. package/templates/fate/client-only/pnpm-workspace.yaml +24 -0
  21. package/templates/fate/client-only/src/state.ts +79 -0
  22. package/templates/fate/client-only/src/style.css +42 -0
  23. package/templates/fate/client-only/tsconfig.json +33 -0
  24. package/templates/fate/client-only/vite.config.ts +12 -0
  25. package/templates/fate/client-only/void.config.ts +11 -0
  26. package/templates/fate/drizzle/client/package.json +4 -3
  27. package/templates/fate/drizzle/client/pages/post/[id].tsx +1 -1
  28. package/templates/fate/drizzle/client/src/ui/Link.tsx +5 -3
  29. package/templates/fate/drizzle/client/tsconfig.json +16 -0
  30. package/templates/fate/drizzle/client/void.json +3 -0
  31. package/templates/fate/graphql/client/package.json +4 -3
  32. package/templates/fate/graphql/client/pages/post/[id].tsx +1 -1
  33. package/templates/fate/graphql/client/src/ui/Link.tsx +5 -3
  34. package/templates/fate/graphql/client/tsconfig.json +16 -0
  35. package/templates/fate/graphql/client/void.json +3 -0
  36. package/templates/fate/graphql/server/vite.config.ts +0 -13
  37. package/templates/fate/graphql-client/package.json +2 -2
  38. package/templates/fate/graphql-client/void.json +3 -0
  39. package/templates/fate/http/client/package.json +4 -3
  40. package/templates/fate/http/client/pages/post/[id].tsx +1 -1
  41. package/templates/fate/http/client/src/ui/Link.tsx +5 -3
  42. package/templates/fate/http/client/tsconfig.json +16 -0
  43. package/templates/fate/http/client/void.json +3 -0
  44. package/templates/fate/prisma/client/package.json +4 -3
  45. package/templates/fate/prisma/client/pages/post/[id].tsx +1 -1
  46. package/templates/fate/prisma/client/src/ui/Link.tsx +5 -3
  47. package/templates/fate/prisma/client/tsconfig.json +16 -0
  48. package/templates/fate/prisma/client/void.json +3 -0
  49. package/templates/fate/void/package.json +2 -2
  50. package/templates/fate/void/pages/category/[id].tsx +1 -1
  51. package/templates/fate/void/pages/post/[id].tsx +1 -1
  52. package/templates/fate/void/src/fate/server.ts +3 -14
package/README.md CHANGED
@@ -6,10 +6,11 @@ Create a new fate app:
6
6
  vp create fate
7
7
  ```
8
8
 
9
- The generated app is installed and the fate client is generated during creation.
9
+ Dependencies are installed during creation. Templates that use a server also generate the fate client.
10
10
 
11
11
  Choose between these templates:
12
12
 
13
+ - `client-only` uses Void Pages Routing with local state persisted in IndexedDB.
13
14
  - `void`: Void pages router with Drizzle, live updates, and native Cloudflare deployment.
14
15
  - `drizzle`: tRPC with Drizzle.
15
16
  - `graphql`: GraphQL with Prisma.
@@ -7,6 +7,11 @@ import { URL } from 'node:url';
7
7
  import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts';
8
8
 
9
9
  const variants = {
10
+ 'client-only': {
11
+ description: 'Void pages with local state saved in IndexedDB',
12
+ label: 'Client Only',
13
+ template: 'client-only',
14
+ },
10
15
  drizzle: {
11
16
  description: 'tRPC with Drizzle',
12
17
  label: 'Drizzle',
@@ -50,11 +55,17 @@ const frontendFrameworks = {
50
55
  },
51
56
  };
52
57
 
53
- const fateDependencyNames = ['@nkzw/fate', 'react-fate', 'void-fate', 'vue-fate'];
58
+ const fateDependencyNames = [
59
+ '@nkzw/fate',
60
+ '@nkzw/fate-indexeddb',
61
+ 'react-fate',
62
+ 'void-fate',
63
+ 'vue-fate',
64
+ ];
54
65
 
55
66
  const usage = () => {
56
67
  process.stdout
57
- .write(`Usage: create-fate [directory] [--template void|drizzle|graphql|graphql-client|http|prisma] [--framework react|vue]
68
+ .write(`Usage: create-fate [directory] [--template ${Object.keys(variants).join('|')}] [--framework react|vue]
58
69
 
59
70
  Create a new fate app.
60
71
 
@@ -329,6 +340,13 @@ const updatePackageJsonFiles = (rootDir, dir, packageName, fateDependencyVersion
329
340
  const readPackageJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'));
330
341
 
331
342
  const writePackageJson = (filePath, json) => {
343
+ for (const section of ['dependencies', 'devDependencies', 'peerDependencies']) {
344
+ if (json[section]) {
345
+ json[section] = Object.fromEntries(
346
+ Object.entries(json[section]).sort(([left], [right]) => left.localeCompare(right)),
347
+ );
348
+ }
349
+ }
332
350
  fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`);
333
351
  };
334
352
 
@@ -548,7 +566,7 @@ const vueTemplatePackage = () =>
548
566
  path.resolve(packageRoot, 'templates', 'fate', '_shared', 'vue-app');
549
567
 
550
568
  const frontendRootForVariant = (targetPath, selectedVariant) =>
551
- selectedVariant === 'void' || selectedVariant === 'graphql-client'
569
+ ['client-only', 'graphql-client', 'void'].includes(selectedVariant)
552
570
  ? targetPath
553
571
  : path.join(targetPath, 'client');
554
572
 
@@ -558,6 +576,35 @@ const removeIfExists = (entryPath) => {
558
576
  }
559
577
  };
560
578
 
579
+ const configureClientOnlyVue = (targetPath) => {
580
+ const pages = path.join(targetPath, 'pages');
581
+ for (const file of fs.readdirSync(pages)) {
582
+ if (file.endsWith('.tsx')) {
583
+ removeIfExists(path.join(pages, file));
584
+ }
585
+ }
586
+ fs.cpSync(path.resolve(packageRoot, 'templates/fate/_shared/client-only-vue'), targetPath, {
587
+ recursive: true,
588
+ });
589
+ const packagePath = path.join(targetPath, 'package.json');
590
+ const packageJson = readPackageJson(packagePath);
591
+ const vuePackageJson = readPackageJson(path.join(vueTemplatePackage(), 'package.json'));
592
+ removePackageEntries(packageJson.dependencies, reactPackageEntries);
593
+ removePackageEntries(packageJson.devDependencies, reactPackageEntries);
594
+ for (const name of ['@void/vue', 'vue', 'vue-fate']) {
595
+ packageJson.dependencies[name] = vuePackageJson.dependencies[name];
596
+ }
597
+ writePackageJson(packagePath, packageJson);
598
+ const agentsPath = path.join(targetPath, 'AGENTS.md');
599
+ fs.writeFileSync(
600
+ agentsPath,
601
+ fs
602
+ .readFileSync(agentsPath, 'utf8')
603
+ .replaceAll('react-fate', 'vue-fate')
604
+ .replaceAll('React', 'Vue'),
605
+ );
606
+ };
607
+
561
608
  const removeReactFrontend = (frontendRoot, selectedVariant) => {
562
609
  if (selectedVariant === 'void' || selectedVariant === 'graphql-client') {
563
610
  for (const entry of [
@@ -834,10 +881,10 @@ const configureVueTemplate = (targetPath, selectedVariant) => {
834
881
  selectedVariant === 'void' || selectedVariant === 'graphql-client'
835
882
  ? "['**/.cloudflare/**', '.fate', '.void', '.void-wrangler.jsonc', 'dist', 'node_modules', 'vite.config.ts.timestamp-*']"
836
883
  : "['**/.cloudflare/**', '.fate', '.void', 'dist', 'node_modules', '../dist', 'vite.config.ts.timestamp-*']",
837
- __TSCONFIG_EXTENDS__:
884
+ '"__TSCONFIG_EXTENDS__"':
838
885
  selectedVariant === 'void' || selectedVariant === 'graphql-client'
839
- ? './.void/tsconfig.json'
840
- : '../tsconfig.json',
886
+ ? '"./.void/tsconfig.json"'
887
+ : '["../tsconfig.json", "./.void/tsconfig.json"]',
841
888
  '"__TSCONFIG_INCLUDE__"':
842
889
  selectedVariant === 'void'
843
890
  ? `[
@@ -918,23 +965,12 @@ const setupProject = (targetPath, selectedVariant) => {
918
965
  runCommand('vp', ['run', 'fbtee:all'], frontendRoot);
919
966
  }
920
967
 
921
- switch (selectedVariant) {
922
- case 'graphql':
923
- case 'prisma':
924
- runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
925
- runCommand('vp', ['run', 'fate:generate'], targetPath);
926
- break;
927
- case 'graphql-client':
928
- runCommand('vp', ['run', 'prepare:void'], targetPath);
929
- runCommand('vp', ['run', 'fate:generate'], targetPath);
930
- break;
931
- case 'void':
932
- runCommand('vp', ['run', 'prepare:void'], targetPath);
933
- runCommand('vp', ['run', 'fate:generate'], targetPath);
934
- break;
935
- default:
936
- runCommand('vp', ['run', 'fate:generate'], targetPath);
937
- break;
968
+ if (selectedVariant === 'graphql' || selectedVariant === 'prisma') {
969
+ runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
970
+ }
971
+ runCommand('vp', ['run', 'prepare:void'], frontendRoot);
972
+ if (selectedVariant !== 'client-only') {
973
+ runCommand('vp', ['run', 'fate:generate'], targetPath);
938
974
  }
939
975
  };
940
976
 
@@ -1004,7 +1040,11 @@ const main = async () => {
1004
1040
  );
1005
1041
  }
1006
1042
  if (selectedFramework === 'vue') {
1007
- configureVueTemplate(targetPath, selectedVariant);
1043
+ if (selectedVariant === 'client-only') {
1044
+ configureClientOnlyVue(targetPath);
1045
+ } else {
1046
+ configureVueTemplate(targetPath, selectedVariant);
1047
+ }
1008
1048
  }
1009
1049
  restoreTemplateFileNames(targetPath);
1010
1050
  if (selectedFramework === 'vue') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fate",
3
- "version": "1.14.1",
3
+ "version": "1.16.0",
4
4
  "description": "Create a new fate app.",
5
5
  "homepage": "https://github.com/nkzw-tech/fate",
6
6
  "license": "MIT",
@@ -0,0 +1,6 @@
1
+ <template>
2
+ <h1>About</h1>
3
+ <p>
4
+ fate saves your tasks in this browser. You can reload the page and pick up where you left off.
5
+ </p>
6
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue';
3
+ import { useFateClient, useRequest, useView } from 'vue-fate';
4
+ import { WorkspaceView } from '../src/state.ts';
5
+
6
+ const fate = useFateClient();
7
+ const request = useRequest({ workspace: { view: WorkspaceView } });
8
+ const state = useView(WorkspaceView, () => request.data.value?.workspace);
9
+ const title = ref('');
10
+ const pending = ref(false);
11
+ const error = ref('');
12
+ await request.ready();
13
+ await state.ready();
14
+
15
+ const run = async (mutate: () => Promise<{ error?: Error }>) => {
16
+ pending.value = true;
17
+ error.value = '';
18
+ try {
19
+ const result = await mutate();
20
+ if (result.error) {
21
+ error.value = result.error.message;
22
+ }
23
+ } catch (cause) {
24
+ error.value = cause instanceof Error ? cause.message : String(cause);
25
+ } finally {
26
+ pending.value = false;
27
+ }
28
+ };
29
+
30
+ const add = () => {
31
+ if (title.value.trim()) {
32
+ void run(async () => {
33
+ const result = await fate.mutations.task.add({ input: { title: title.value } });
34
+ if (!result.error) {
35
+ title.value = '';
36
+ }
37
+ return result;
38
+ });
39
+ }
40
+ };
41
+ </script>
42
+
43
+ <template>
44
+ <h1>Tasks</h1>
45
+ <p>{{ state?.progress }}</p>
46
+ <form @submit.prevent="add">
47
+ <input v-model="title" aria-label="Task title" required />
48
+ <button :disabled="pending" type="submit">Add task</button>
49
+ </form>
50
+ <p v-if="error" role="alert">{{ error }}</p>
51
+ <ul>
52
+ <li v-for="task in state?.tasks" :key="task.id">
53
+ <label>
54
+ <input
55
+ :checked="task.done"
56
+ :disabled="pending"
57
+ type="checkbox"
58
+ @change="run(() => fate.mutations.task.toggle({ input: { id: task.id } }))"
59
+ />
60
+ {{ task.title }}
61
+ </label>
62
+ <button
63
+ :aria-label="`Remove ${task.title}`"
64
+ :disabled="pending"
65
+ type="button"
66
+ @click="run(() => fate.mutations.task.remove({ input: { id: task.id } }))"
67
+ >
68
+ Remove
69
+ </button>
70
+ </li>
71
+ </ul>
72
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import '../src/style.css';
3
+ import { Link } from '@void/vue';
4
+ import { onErrorCaptured, ref } from 'vue';
5
+ import { FateClient } from 'vue-fate';
6
+ import { getTaskClient } from '../src/state.ts';
7
+
8
+ const client = getTaskClient();
9
+ const error = ref('');
10
+ onErrorCaptured((cause) => {
11
+ error.value = String(cause);
12
+ return false;
13
+ });
14
+ </script>
15
+
16
+ <template>
17
+ <nav aria-label="Main navigation">
18
+ <Link href="/">Tasks</Link>
19
+ <Link href="/about">About</Link>
20
+ </nav>
21
+ <main>
22
+ <p v-if="error" role="alert">{{ error }}</p>
23
+ <FateClient v-else :client="client">
24
+ <Suspense>
25
+ <slot />
26
+ <template #fallback><p>Restoring tasks…</p></template>
27
+ </Suspense>
28
+ </FateClient>
29
+ </main>
30
+ </template>
@@ -0,0 +1,12 @@
1
+ import { voidVue } from '@void/vue/plugin';
2
+ import { defineConfig } from 'vite-plus';
3
+ import { voidPlugin } from 'void';
4
+
5
+ export default defineConfig({
6
+ fmt: { ignorePatterns: ['.void/', 'dist/', 'pnpm-lock.yaml'], singleQuote: true },
7
+ lint: {
8
+ ignorePatterns: ['.void/', 'dist/'],
9
+ options: { typeAware: true, typeCheck: true },
10
+ },
11
+ plugins: [voidPlugin(), voidVue()],
12
+ });
@@ -22,13 +22,13 @@
22
22
  "@nkzw/core": "^1.3.1",
23
23
  "@nkzw/define-env": "^1.1.0",
24
24
  "@trpc/client": "^11.19.0",
25
- "@void/vue": "^1.6.1",
25
+ "@void/vue": "^1.7.0",
26
26
  "class-variance-authority": "^0.7.1",
27
27
  "clsx": "^2.1.1",
28
28
  "dotenv": "^18.0.9",
29
29
  "graphql-sse": "^2.6.3",
30
30
  "tailwind-merge": "^3.7.0",
31
- "void": "^1.6.1",
31
+ "void": "^1.7.0",
32
32
  "vue": "^3.5.44",
33
33
  "vue-fate": "latest"
34
34
  },
@@ -5,7 +5,7 @@ import { PostView } from '../../src/fateViews.ts';
5
5
  import PostCard from '../../src/ui/PostCard.vue';
6
6
  import Section from '../../src/ui/Section.vue';
7
7
 
8
- const params = useParams<{ id: string }>();
8
+ const params = useParams<'/post/[id]'>();
9
9
  const id = params.id;
10
10
 
11
11
  if (!id) {
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "pages": {
3
3
  "router": true
4
+ },
5
+ "routing": {
6
+ "typedRoutes": true
4
7
  }
5
8
  }
@@ -0,0 +1,5 @@
1
+ # Client-only fate
2
+
3
+ Use `node_modules/react-fate/docs/` for bundled guidance on building React applications with fate. Start with `index.md`.
4
+
5
+ Client definitions and mutations live in `src/state.ts`. Run `vp run dev:setup` after changing Void routes. Run `vp run test:all` and `vp run build` to verify changes.
@@ -0,0 +1,25 @@
1
+ # Client-only fate
2
+
3
+ A task list with Void Pages Routing, `react-fate`, and IndexedDB. Try adding, completing, or removing a task, then reload to see the saved state. The About page demonstrates navigation between static routes.
4
+
5
+ ## Development
6
+
7
+ ```sh
8
+ vp install
9
+ vp run dev:setup
10
+ vp run dev
11
+ ```
12
+
13
+ The app's schema, views, and mutations live in `src/state.ts`. The project uses Void route types from `void prepare`. fate registers the client schema at runtime.
14
+
15
+ ## Deployment
16
+
17
+ Build static files into `dist/client/` with `vp run build`. Serve that directory with a static host that resolves clean URLs to the corresponding HTML files.
18
+
19
+ To deploy directly to Cloudflare, run:
20
+
21
+ ```sh
22
+ vp exec void deploy --platform cloudflare
23
+ ```
24
+
25
+ Void deploys both pages as static assets. IndexedDB holds each browser's task data.
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ .void/
3
+ .void-wrangler.jsonc
4
+ .cloudflare/
5
+ dist/
6
+ *.tsbuildinfo
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "fate-app",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "description": "fate is a modern data client for the web.",
6
+ "author": "Christoph Nakazawa <christoph.pojer@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git://github.com/nkzw-tech/fate.git"
10
+ },
11
+ "type": "module",
12
+ "scripts": {
13
+ "build": "vp build",
14
+ "dev": "vp dev",
15
+ "dev:setup": "vp run prepare:void",
16
+ "prepare:void": "void prepare",
17
+ "test:all": "vp check"
18
+ },
19
+ "dependencies": {
20
+ "@nkzw/fate": "latest",
21
+ "@nkzw/fate-indexeddb": "latest",
22
+ "@void/react": "^1.7.0",
23
+ "react": "^19.3.1",
24
+ "react-dom": "^19.3.1",
25
+ "react-error-boundary": "^6.1.6",
26
+ "react-fate": "latest",
27
+ "void": "^1.7.0",
28
+ "zod": "^4.6.7"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^19.3.0",
32
+ "@types/react-dom": "^19.3.0",
33
+ "typescript": "^7.0.2",
34
+ "vite": "catalog:",
35
+ "vite-plus": "catalog:"
36
+ },
37
+ "browserslist": [
38
+ ">0.2%",
39
+ "not dead",
40
+ "not op_mini all"
41
+ ],
42
+ "engines": {
43
+ "node": "^24.22.0 || >=26.12.0",
44
+ "pnpm": ">=11.0.0"
45
+ },
46
+ "packageManager": "pnpm@12.6.0"
47
+ }
@@ -0,0 +1 @@
1
+ export const ssr = false;
@@ -0,0 +1,11 @@
1
+ export default function About() {
2
+ return (
3
+ <>
4
+ <h1>About</h1>
5
+ <p>
6
+ fate saves your tasks in this browser. You can reload the page and pick up where you left
7
+ off.
8
+ </p>
9
+ </>
10
+ );
11
+ }
@@ -0,0 +1 @@
1
+ export const ssr = false;
@@ -0,0 +1,82 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useFateClient, useRequest, useView } from 'react-fate';
3
+ import { WorkspaceView } from '../src/state.ts';
4
+
5
+ export default function Tasks() {
6
+ const fate = useFateClient();
7
+ const { workspace } = useRequest({ workspace: { view: WorkspaceView } });
8
+ const { progress, tasks } = useView(WorkspaceView, workspace);
9
+ const [title, setTitle] = useState('');
10
+ const [pending, setPending] = useState(false);
11
+ const [error, setError] = useState('');
12
+
13
+ const run = async (mutate: () => Promise<{ error?: Error }>) => {
14
+ setPending(true);
15
+ setError('');
16
+ try {
17
+ const result = await mutate();
18
+ if (result.error) {
19
+ setError(result.error.message);
20
+ }
21
+ } catch (error) {
22
+ setError(error instanceof Error ? error.message : String(error));
23
+ } finally {
24
+ setPending(false);
25
+ }
26
+ };
27
+
28
+ const add = (event: FormEvent<HTMLFormElement>) => {
29
+ event.preventDefault();
30
+ if (title.trim()) {
31
+ void run(async () => {
32
+ const result = await fate.mutations.task.add({ input: { title } });
33
+ if (!result.error) {
34
+ setTitle('');
35
+ }
36
+ return result;
37
+ });
38
+ }
39
+ };
40
+
41
+ return (
42
+ <>
43
+ <h1>Tasks</h1>
44
+ <p>{progress}</p>
45
+ <form onSubmit={add}>
46
+ <input
47
+ aria-label="Task title"
48
+ onChange={(event) => setTitle(event.target.value)}
49
+ required
50
+ value={title}
51
+ />
52
+ <button disabled={pending} type="submit">
53
+ Add task
54
+ </button>
55
+ </form>
56
+ {error && <p role="alert">{error}</p>}
57
+ <ul>
58
+ {tasks.map(({ done, id, title }) => (
59
+ <li key={id}>
60
+ <label>
61
+ <input
62
+ checked={done}
63
+ disabled={pending}
64
+ onChange={() => void run(() => fate.mutations.task.toggle({ input: { id } }))}
65
+ type="checkbox"
66
+ />
67
+ {title}
68
+ </label>
69
+ <button
70
+ aria-label={`Remove ${title}`}
71
+ disabled={pending}
72
+ onClick={() => void run(() => fate.mutations.task.remove({ input: { id } }))}
73
+ type="button"
74
+ >
75
+ Remove
76
+ </button>
77
+ </li>
78
+ ))}
79
+ </ul>
80
+ </>
81
+ );
82
+ }
@@ -0,0 +1,24 @@
1
+ import '../src/style.css';
2
+ import { Link } from '@void/react';
3
+ import { Suspense, type PropsWithChildren } from 'react';
4
+ import { ErrorBoundary } from 'react-error-boundary';
5
+ import { FateClient } from 'react-fate';
6
+ import { getTaskClient } from '../src/state.ts';
7
+
8
+ export default function Layout({ children }: PropsWithChildren) {
9
+ return (
10
+ <>
11
+ <nav aria-label="Main navigation">
12
+ <Link href="/">Tasks</Link>
13
+ <Link href="/about">About</Link>
14
+ </nav>
15
+ <main>
16
+ <ErrorBoundary fallbackRender={({ error }) => <p role="alert">{String(error)}</p>}>
17
+ <FateClient client={getTaskClient()}>
18
+ <Suspense fallback={<p>Restoring tasks…</p>}>{children}</Suspense>
19
+ </FateClient>
20
+ </ErrorBoundary>
21
+ </main>
22
+ </>
23
+ );
24
+ }
@@ -0,0 +1,24 @@
1
+ packages:
2
+ - .
3
+
4
+ allowBuilds:
5
+ better-sqlite3: false
6
+ esbuild: true
7
+ workerd: true
8
+
9
+ catalog:
10
+ vite: npm:@voidzero-dev/vite-plus-core@1.4.0
11
+ vite-plus: 1.4.0
12
+
13
+ minimumReleaseAgeExclude:
14
+ - '@voidzero-dev/vite-plus-core'
15
+ - vite-plus
16
+
17
+ overrides:
18
+ vite: 'catalog:'
19
+
20
+ peerDependencyRules:
21
+ allowAny:
22
+ - vite
23
+ allowedVersions:
24
+ vite: '*'
@@ -0,0 +1,79 @@
1
+ import {
2
+ clientSchema,
3
+ computed,
4
+ createClient,
5
+ dataView,
6
+ field,
7
+ list,
8
+ view,
9
+ type EntityOf,
10
+ } from '@nkzw/fate';
11
+ import { createIndexedDBStorage } from '@nkzw/fate-indexeddb';
12
+ import { createPersistence } from '@nkzw/fate/persistence';
13
+ import { z } from 'zod';
14
+
15
+ type TaskData = { done: boolean; id: string; title: string };
16
+ type WorkspaceData = { id: string; tasks: Array<TaskData> };
17
+ const task = dataView<TaskData>('Task')({ done: true, id: true, title: true });
18
+ const workspace = dataView<WorkspaceData>('Workspace')({
19
+ id: true,
20
+ progress: computed({
21
+ resolve: (_item: WorkspaceData, { done = [] }) =>
22
+ `${done.filter(Boolean).length} of ${done.length} complete`,
23
+ select: { done: field('tasks.done') },
24
+ }),
25
+ tasks: list(task),
26
+ });
27
+ const schema = clientSchema({
28
+ roots: { workspace: { id: 'main', type: 'Workspace' } },
29
+ types: {
30
+ Task: { defaults: (id) => ({ done: false, id, title: '' }), view: task },
31
+ Workspace: {
32
+ defaults: (id) => ({
33
+ id,
34
+ tasks: [{ done: false, id: 'welcome', title: 'Add your first task' }],
35
+ }),
36
+ view: workspace,
37
+ },
38
+ },
39
+ });
40
+
41
+ const createTaskClient = () =>
42
+ createClient({
43
+ persistence: createPersistence({ key: 'tasks', storage: createIndexedDBStorage() }),
44
+ }).registerClientSchema(schema, (builder) => ({
45
+ 'task.add': builder.mutation('Task', {
46
+ input: z.object({ title: z.string().trim().min(1) }),
47
+ resolve: ({ input, store }) => {
48
+ const task = store.create('Task', { id: crypto.randomUUID(), title: input.title });
49
+ store.update('Workspace', 'main', {
50
+ tasks: [...(store.get('Workspace', 'main')?.tasks ?? []), task],
51
+ });
52
+ return task;
53
+ },
54
+ }),
55
+ 'task.remove': builder.mutation('Task', {
56
+ input: z.object({ id: z.string() }),
57
+ resolve: ({ input, store }) => store.delete('Task', input.id),
58
+ }),
59
+ 'task.toggle': builder.mutation('Task', {
60
+ input: z.object({ id: z.string() }),
61
+ resolve: ({ input, store }) =>
62
+ store.update('Task', input.id, { done: !store.get('Task', input.id)?.done }),
63
+ }),
64
+ }));
65
+
66
+ type TaskClient = ReturnType<typeof createTaskClient>;
67
+ let client: TaskClient | undefined;
68
+ export const getTaskClient = () => (client ??= createTaskClient());
69
+
70
+ export const WorkspaceView = view<EntityOf<TaskClient, 'Workspace'>>()({
71
+ progress: true,
72
+ tasks: { done: true, id: true, title: true },
73
+ });
74
+
75
+ declare module '@nkzw/fate' {
76
+ interface ClientRegistration {
77
+ client: TaskClient;
78
+ }
79
+ }
@@ -0,0 +1,42 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ font-family: system-ui, sans-serif;
4
+ }
5
+
6
+ body {
7
+ margin: 0 auto;
8
+ max-width: 38rem;
9
+ padding: 2rem 1rem;
10
+ }
11
+
12
+ nav,
13
+ form,
14
+ li {
15
+ align-items: center;
16
+ display: flex;
17
+ gap: 1rem;
18
+ }
19
+
20
+ form input {
21
+ flex: 1;
22
+ }
23
+
24
+ input,
25
+ button {
26
+ font: inherit;
27
+ padding: 0.4rem;
28
+ }
29
+
30
+ li {
31
+ border-bottom: 1px solid GrayText;
32
+ justify-content: space-between;
33
+ padding: 0.75rem 0;
34
+ }
35
+
36
+ ul {
37
+ padding: 0;
38
+ }
39
+
40
+ [aria-current='page'] {
41
+ font-weight: bold;
42
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "extends": "./.void/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowImportingTsExtensions": true,
5
+ "esModuleInterop": true,
6
+ "forceConsistentCasingInFileNames": true,
7
+ "incremental": true,
8
+ "isolatedModules": true,
9
+ "jsx": "react-jsx",
10
+ "lib": ["dom", "dom.iterable", "esnext"],
11
+ "module": "ESNext",
12
+ "moduleResolution": "Bundler",
13
+ "noEmit": true,
14
+ "noImplicitOverride": true,
15
+ "noUnusedLocals": true,
16
+ "resolveJsonModule": true,
17
+ "skipLibCheck": true,
18
+ "strict": true,
19
+ "target": "ES2022",
20
+ "types": ["vite/client", "void/env"]
21
+ },
22
+ "include": [
23
+ ".void/**/*.d.ts",
24
+ "pages/**/*.ts",
25
+ "pages/**/*.tsx",
26
+ "pages/**/*.vue",
27
+ "src/**/*.ts",
28
+ "src/**/*.tsx",
29
+ "src/**/*.vue",
30
+ "vite.config.ts",
31
+ "void.config.ts"
32
+ ]
33
+ }
@@ -0,0 +1,12 @@
1
+ import { voidReact } from '@void/react/plugin';
2
+ import { defineConfig } from 'vite-plus';
3
+ import { voidPlugin } from 'void';
4
+
5
+ export default defineConfig({
6
+ fmt: { ignorePatterns: ['.void/', 'dist/', 'pnpm-lock.yaml'], singleQuote: true },
7
+ lint: {
8
+ ignorePatterns: ['.void/', 'dist/'],
9
+ options: { typeAware: true, typeCheck: true },
10
+ },
11
+ plugins: [voidPlugin(), voidReact()],
12
+ });
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'void/config';
2
+
3
+ export default defineConfig({
4
+ head: {
5
+ htmlAttrs: { lang: 'en' },
6
+ meta: [{ content: 'width=device-width, initial-scale=1', name: 'viewport' }],
7
+ title: 'Tasks',
8
+ },
9
+ output: 'static',
10
+ routing: { typedRoutes: true },
11
+ });
@@ -11,12 +11,13 @@
11
11
  "scripts": {
12
12
  "build": "vp run fbtee:all && vp build",
13
13
  "dev": "vp dev",
14
- "dev:setup": "vp run fbtee:all",
14
+ "dev:setup": "vp run fbtee:all && vp run prepare:void",
15
15
  "fate:generate": "vp exec fate generate",
16
16
  "fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
17
17
  "fbtee:collect": "vp exec fbtee collect",
18
18
  "fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
19
19
  "fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
20
+ "prepare:void": "void prepare",
20
21
  "start": "node start.mjs"
21
22
  },
22
23
  "dependencies": {
@@ -27,7 +28,7 @@
27
28
  "@nkzw/stack": "^2.3.2",
28
29
  "@radix-ui/react-slot": "^1.5.0",
29
30
  "@trpc/client": "^11.19.0",
30
- "@void/react": "^1.6.1",
31
+ "@void/react": "^1.7.0",
31
32
  "better-auth": "^1.7.14",
32
33
  "class-variance-authority": "^0.7.1",
33
34
  "clsx": "^2.1.1",
@@ -38,7 +39,7 @@
38
39
  "react-error-boundary": "^6.1.6",
39
40
  "react-fate": "latest",
40
41
  "tailwind-merge": "^3.7.0",
41
- "void": "^1.6.1"
42
+ "void": "^1.7.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@nkzw/fbtee-compiler": "^5.0.0",
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
4
4
  import Section from '../../src/ui/Section.tsx';
5
5
 
6
6
  export default function PostPage() {
7
- const { id } = useParams<{ id: string }>();
7
+ const { id } = useParams<'/post/[id]'>();
8
8
 
9
9
  if (!id) {
10
10
  throw new Error('fate: Post ID is required.');
@@ -1,9 +1,11 @@
1
1
  import { Link as VoidLink } from '@void/react';
2
2
  import type { ComponentProps } from 'react';
3
3
 
4
- export type LinkProps = Omit<ComponentProps<typeof VoidLink>, 'href'> & {
5
- href?: string;
6
- to?: string;
4
+ type VoidLinkProps = ComponentProps<typeof VoidLink>;
5
+
6
+ export type LinkProps = Omit<VoidLinkProps, 'href'> & {
7
+ href?: VoidLinkProps['href'];
8
+ to?: VoidLinkProps['href'];
7
9
  };
8
10
 
9
11
  export default function Link({ href, to, ...props }: LinkProps) {
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": ["../tsconfig.json", "./.void/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "types": ["vite/client", "void/env"]
5
+ },
6
+ "include": [
7
+ ".fate/**/*.ts",
8
+ ".void/**/*.d.ts",
9
+ "pages/**/*.ts",
10
+ "pages/**/*.tsx",
11
+ "src/**/*.ts",
12
+ "src/**/*.tsx",
13
+ "env.ts",
14
+ "vite.config.ts"
15
+ ]
16
+ }
@@ -25,5 +25,8 @@
25
25
  "href": "/icon.svg"
26
26
  }
27
27
  ]
28
+ },
29
+ "routing": {
30
+ "typedRoutes": true
28
31
  }
29
32
  }
@@ -11,12 +11,13 @@
11
11
  "scripts": {
12
12
  "build": "vp run fbtee:all && vp build",
13
13
  "dev": "vp dev",
14
- "dev:setup": "vp run fbtee:all",
14
+ "dev:setup": "vp run fbtee:all && vp run prepare:void",
15
15
  "fate:generate": "vp exec fate generate",
16
16
  "fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
17
17
  "fbtee:collect": "vp exec fbtee collect",
18
18
  "fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
19
19
  "fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
20
+ "prepare:void": "void prepare",
20
21
  "start": "node start.mjs"
21
22
  },
22
23
  "dependencies": {
@@ -26,7 +27,7 @@
26
27
  "@nkzw/define-env": "^1.1.0",
27
28
  "@nkzw/stack": "^2.3.2",
28
29
  "@radix-ui/react-slot": "^1.5.0",
29
- "@void/react": "^1.6.1",
30
+ "@void/react": "^1.7.0",
30
31
  "better-auth": "^1.7.14",
31
32
  "class-variance-authority": "^0.7.1",
32
33
  "clsx": "^2.1.1",
@@ -38,7 +39,7 @@
38
39
  "react-error-boundary": "^6.1.6",
39
40
  "react-fate": "latest",
40
41
  "tailwind-merge": "^3.7.0",
41
- "void": "^1.6.1"
42
+ "void": "^1.7.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@nkzw/fbtee-compiler": "^5.0.0",
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
4
4
  import Section from '../../src/ui/Section.tsx';
5
5
 
6
6
  export default function PostPage() {
7
- const { id } = useParams<{ id: string }>();
7
+ const { id } = useParams<'/post/[id]'>();
8
8
 
9
9
  if (!id) {
10
10
  throw new Error('fate: Post ID is required.');
@@ -1,9 +1,11 @@
1
1
  import { Link as VoidLink } from '@void/react';
2
2
  import type { ComponentProps } from 'react';
3
3
 
4
- export type LinkProps = Omit<ComponentProps<typeof VoidLink>, 'href'> & {
5
- href?: string;
6
- to?: string;
4
+ type VoidLinkProps = ComponentProps<typeof VoidLink>;
5
+
6
+ export type LinkProps = Omit<VoidLinkProps, 'href'> & {
7
+ href?: VoidLinkProps['href'];
8
+ to?: VoidLinkProps['href'];
7
9
  };
8
10
 
9
11
  export default function Link({ href, to, ...props }: LinkProps) {
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": ["../tsconfig.json", "./.void/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "types": ["vite/client", "void/env"]
5
+ },
6
+ "include": [
7
+ ".fate/**/*.ts",
8
+ ".void/**/*.d.ts",
9
+ "pages/**/*.ts",
10
+ "pages/**/*.tsx",
11
+ "src/**/*.ts",
12
+ "src/**/*.tsx",
13
+ "env.ts",
14
+ "vite.config.ts"
15
+ ]
16
+ }
@@ -25,5 +25,8 @@
25
25
  "href": "/icon.svg"
26
26
  }
27
27
  ]
28
+ },
29
+ "routing": {
30
+ "typedRoutes": true
28
31
  }
29
32
  }
@@ -24,19 +24,6 @@ export default defineConfig({
24
24
  },
25
25
  },
26
26
  ],
27
- rules: {
28
- 'import-x-js/no-extraneous-dependencies': [
29
- 'error',
30
- {
31
- devDependencies: [
32
- './prisma.config.ts',
33
- './scripts/**/*.tsx',
34
- './src/__tests__/**/*.tsx',
35
- './vite.config.ts',
36
- ],
37
- },
38
- ],
39
- },
40
27
  },
41
28
  pack: {
42
29
  deps: { resolveDepSubpath: true },
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@nkzw/fate": "latest",
26
- "@void/react": "^1.6.1",
26
+ "@void/react": "^1.7.0",
27
27
  "fbtee": "^5.0.0",
28
28
  "graphql-sse": "^2.6.3",
29
29
  "lucide-react": "^1.61.0",
@@ -31,7 +31,7 @@
31
31
  "react-dom": "^19.3.1",
32
32
  "react-error-boundary": "^6.1.6",
33
33
  "react-fate": "latest",
34
- "void": "^1.6.1"
34
+ "void": "^1.7.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@nkzw/eslint-plugin": "^2.0.0",
@@ -25,5 +25,8 @@
25
25
  "worker": {
26
26
  "compatibility_date": "2025-12-01",
27
27
  "compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"]
28
+ },
29
+ "routing": {
30
+ "typedRoutes": true
28
31
  }
29
32
  }
@@ -11,12 +11,13 @@
11
11
  "scripts": {
12
12
  "build": "vp run fbtee:all && vp build",
13
13
  "dev": "vp dev",
14
- "dev:setup": "vp run fbtee:all",
14
+ "dev:setup": "vp run fbtee:all && vp run prepare:void",
15
15
  "fate:generate": "vp exec fate generate",
16
16
  "fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
17
17
  "fbtee:collect": "vp exec fbtee collect",
18
18
  "fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
19
19
  "fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
20
+ "prepare:void": "void prepare",
20
21
  "start": "node start.mjs"
21
22
  },
22
23
  "dependencies": {
@@ -26,7 +27,7 @@
26
27
  "@nkzw/define-env": "^1.1.0",
27
28
  "@nkzw/stack": "^2.3.2",
28
29
  "@radix-ui/react-slot": "^1.5.0",
29
- "@void/react": "^1.6.1",
30
+ "@void/react": "^1.7.0",
30
31
  "better-auth": "^1.7.14",
31
32
  "class-variance-authority": "^0.7.1",
32
33
  "clsx": "^2.1.1",
@@ -37,7 +38,7 @@
37
38
  "react-error-boundary": "^6.1.6",
38
39
  "react-fate": "latest",
39
40
  "tailwind-merge": "^3.7.0",
40
- "void": "^1.6.1"
41
+ "void": "^1.7.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@nkzw/fbtee-compiler": "^5.0.0",
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
4
4
  import Section from '../../src/ui/Section.tsx';
5
5
 
6
6
  export default function PostPage() {
7
- const { id } = useParams<{ id: string }>();
7
+ const { id } = useParams<'/post/[id]'>();
8
8
 
9
9
  if (!id) {
10
10
  throw new Error('fate: Post ID is required.');
@@ -1,9 +1,11 @@
1
1
  import { Link as VoidLink } from '@void/react';
2
2
  import type { ComponentProps } from 'react';
3
3
 
4
- export type LinkProps = Omit<ComponentProps<typeof VoidLink>, 'href'> & {
5
- href?: string;
6
- to?: string;
4
+ type VoidLinkProps = ComponentProps<typeof VoidLink>;
5
+
6
+ export type LinkProps = Omit<VoidLinkProps, 'href'> & {
7
+ href?: VoidLinkProps['href'];
8
+ to?: VoidLinkProps['href'];
7
9
  };
8
10
 
9
11
  export default function Link({ href, to, ...props }: LinkProps) {
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": ["../tsconfig.json", "./.void/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "types": ["vite/client", "void/env"]
5
+ },
6
+ "include": [
7
+ ".fate/**/*.ts",
8
+ ".void/**/*.d.ts",
9
+ "pages/**/*.ts",
10
+ "pages/**/*.tsx",
11
+ "src/**/*.ts",
12
+ "src/**/*.tsx",
13
+ "env.ts",
14
+ "vite.config.ts"
15
+ ]
16
+ }
@@ -25,5 +25,8 @@
25
25
  "href": "/icon.svg"
26
26
  }
27
27
  ]
28
+ },
29
+ "routing": {
30
+ "typedRoutes": true
28
31
  }
29
32
  }
@@ -11,12 +11,13 @@
11
11
  "scripts": {
12
12
  "build": "vp run fbtee:all && vp build",
13
13
  "dev": "vp dev",
14
- "dev:setup": "vp run fbtee:all",
14
+ "dev:setup": "vp run fbtee:all && vp run prepare:void",
15
15
  "fate:generate": "vp exec fate generate",
16
16
  "fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
17
17
  "fbtee:collect": "vp exec fbtee collect",
18
18
  "fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
19
19
  "fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
20
+ "prepare:void": "void prepare",
20
21
  "start": "node start.mjs"
21
22
  },
22
23
  "dependencies": {
@@ -27,7 +28,7 @@
27
28
  "@nkzw/stack": "^2.3.2",
28
29
  "@radix-ui/react-slot": "^1.5.0",
29
30
  "@trpc/client": "^11.19.0",
30
- "@void/react": "^1.6.1",
31
+ "@void/react": "^1.7.0",
31
32
  "better-auth": "^1.7.14",
32
33
  "class-variance-authority": "^0.7.1",
33
34
  "clsx": "^2.1.1",
@@ -38,7 +39,7 @@
38
39
  "react-error-boundary": "^6.1.6",
39
40
  "react-fate": "latest",
40
41
  "tailwind-merge": "^3.7.0",
41
- "void": "^1.6.1"
42
+ "void": "^1.7.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@nkzw/fbtee-compiler": "^5.0.0",
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
4
4
  import Section from '../../src/ui/Section.tsx';
5
5
 
6
6
  export default function PostPage() {
7
- const { id } = useParams<{ id: string }>();
7
+ const { id } = useParams<'/post/[id]'>();
8
8
 
9
9
  if (!id) {
10
10
  throw new Error('fate: Post ID is required.');
@@ -1,9 +1,11 @@
1
1
  import { Link as VoidLink } from '@void/react';
2
2
  import type { ComponentProps } from 'react';
3
3
 
4
- export type LinkProps = Omit<ComponentProps<typeof VoidLink>, 'href'> & {
5
- href?: string;
6
- to?: string;
4
+ type VoidLinkProps = ComponentProps<typeof VoidLink>;
5
+
6
+ export type LinkProps = Omit<VoidLinkProps, 'href'> & {
7
+ href?: VoidLinkProps['href'];
8
+ to?: VoidLinkProps['href'];
7
9
  };
8
10
 
9
11
  export default function Link({ href, to, ...props }: LinkProps) {
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": ["../tsconfig.json", "./.void/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "types": ["vite/client", "void/env"]
5
+ },
6
+ "include": [
7
+ ".fate/**/*.ts",
8
+ ".void/**/*.d.ts",
9
+ "pages/**/*.ts",
10
+ "pages/**/*.tsx",
11
+ "src/**/*.ts",
12
+ "src/**/*.tsx",
13
+ "env.ts",
14
+ "vite.config.ts"
15
+ ]
16
+ }
@@ -25,5 +25,8 @@
25
25
  "href": "/icon.svg"
26
26
  }
27
27
  ]
28
+ },
29
+ "routing": {
30
+ "typedRoutes": true
28
31
  }
29
32
  }
@@ -29,7 +29,7 @@
29
29
  "@nkzw/fate": "latest",
30
30
  "@nkzw/stack": "^2.3.2",
31
31
  "@radix-ui/react-slot": "^1.5.0",
32
- "@void/react": "^1.6.1",
32
+ "@void/react": "^1.7.0",
33
33
  "better-auth": "^1.7.15",
34
34
  "class-variance-authority": "^0.7.1",
35
35
  "clsx": "^2.1.1",
@@ -41,7 +41,7 @@
41
41
  "react-error-boundary": "^6.1.6",
42
42
  "react-fate": "latest",
43
43
  "tailwind-merge": "^3.7.0",
44
- "void": "^1.6.1",
44
+ "void": "^1.7.0",
45
45
  "void-fate": "latest",
46
46
  "zod": "^4.6.7"
47
47
  },
@@ -4,7 +4,7 @@ import CategoryCard, { CategoryView } from '../../src/ui/CategoryCard.tsx';
4
4
  import Section from '../../src/ui/Section.tsx';
5
5
 
6
6
  export default function CategoryPage() {
7
- const { id } = useParams<{ id: string }>();
7
+ const { id } = useParams<'/category/[id]'>();
8
8
 
9
9
  if (!id) {
10
10
  throw new Error('fate: Category ID is required.');
@@ -4,7 +4,7 @@ import { PostCard, PostView } from '../../src/ui/PostCard.tsx';
4
4
  import Section from '../../src/ui/Section.tsx';
5
5
 
6
6
  export default function PostPage() {
7
- const { id } = useParams<{ id: string }>();
7
+ const { id } = useParams<'/post/[id]'>();
8
8
 
9
9
  if (!id) {
10
10
  throw new Error('fate: Post ID is required.');
@@ -152,15 +152,6 @@ export const fateServer = createFateServer({
152
152
  throw new Error('Comment not found.');
153
153
  }
154
154
 
155
- const liveComment = await source.resolveById({
156
- ctx,
157
- id: commentId,
158
- input: {
159
- select: ['author.id', 'author.name', 'author.username', 'content', 'id'],
160
- },
161
- view: commentDataView,
162
- });
163
-
164
155
  if (hasNestedSelection(select, 'post')) {
165
156
  created.post = (await source.resolveById<PostItem>({
166
157
  ctx,
@@ -173,9 +164,7 @@ export const fateServer = createFateServer({
173
164
  })) as PostItem | null;
174
165
  }
175
166
 
176
- live
177
- .connection('Post.comments', { id: input.postId })
178
- .appendNode('Comment', commentId, { node: liveComment ?? created });
167
+ live.connection('Post.comments', { id: input.postId }).appendNode('Comment', commentId);
179
168
  live.update('Post', input.postId, { changed: ['commentCount', 'comments'] });
180
169
 
181
170
  return created as CommentItem & { post?: { commentCount: number } };
@@ -317,7 +306,7 @@ export const fateServer = createFateServer({
317
306
  throw new Error('Post not found.');
318
307
  }
319
308
 
320
- live.update('Post', input.id, { changed: ['likes'], data: post });
309
+ live.update('Post', input.id, { changed: ['likes'] });
321
310
 
322
311
  return post as Post;
323
312
  },
@@ -344,7 +333,7 @@ export const fateServer = createFateServer({
344
333
  throw new Error('Post not found.');
345
334
  }
346
335
 
347
- live.update('Post', input.id, { changed: ['likes'], data: post });
336
+ live.update('Post', input.id, { changed: ['likes'] });
348
337
 
349
338
  return post as Post;
350
339
  },