create-fate 1.0.3 → 1.3.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 +3 -0
- package/bin/create-fate.mjs +594 -9
- package/package.json +2 -2
- package/templates/fate/README.md +3 -0
- package/templates/fate/_shared/vue-app/package.json +54 -0
- package/templates/fate/_shared/vue-app/pages/index.vue +57 -0
- package/templates/fate/_shared/vue-app/pages/layout.vue +32 -0
- package/templates/fate/_shared/vue-app/pages/post/[id].vue +24 -0
- package/templates/fate/_shared/vue-app/src/App.css +63 -0
- package/templates/fate/_shared/vue-app/src/fateViews.ts +14 -0
- package/templates/fate/_shared/vue-app/src/lib/cx.ts +6 -0
- package/templates/fate/_shared/vue-app/src/lib/env.ts +3 -0
- package/templates/fate/_shared/vue-app/src/ui/Button.vue +67 -0
- package/templates/fate/_shared/vue-app/src/ui/Card.vue +16 -0
- package/templates/fate/_shared/vue-app/src/ui/Error.vue +9 -0
- package/templates/fate/_shared/vue-app/src/ui/ErrorBoundary.vue +22 -0
- package/templates/fate/_shared/vue-app/src/ui/Header.vue +35 -0
- package/templates/fate/_shared/vue-app/src/ui/Link.vue +15 -0
- package/templates/fate/_shared/vue-app/src/ui/PostCard.vue +29 -0
- package/templates/fate/_shared/vue-app/src/ui/Section.vue +19 -0
- package/templates/fate/_shared/vue-app/src/ui/Thinking.vue +11 -0
- package/templates/fate/_shared/vue-app/src/ui/UserCard.vue +23 -0
- package/templates/fate/_shared/vue-app/src/vue-shims.d.ts +6 -0
- package/templates/fate/_shared/vue-app/tsconfig.json +22 -0
- package/templates/fate/_shared/vue-app/vite.config.ts +58 -0
- package/templates/fate/_shared/vue-app/void.json +5 -0
- package/templates/fate/cloudflare/.github/workflows/push.yml +27 -0
- package/templates/fate/cloudflare/.vite-hooks/pre-commit +1 -0
- package/templates/fate/cloudflare/.vscode/extensions.json +12 -0
- package/templates/fate/cloudflare/.vscode/settings.json +25 -0
- package/templates/fate/cloudflare/AGENTS.md +55 -0
- package/templates/fate/cloudflare/README.md +126 -0
- package/templates/fate/cloudflare/_gitignore +20 -0
- package/templates/fate/cloudflare/client/package.json +56 -0
- package/templates/fate/cloudflare/client/pages/index.tsx +96 -0
- package/templates/fate/cloudflare/client/pages/layout.tsx +82 -0
- package/templates/fate/cloudflare/client/pages/login.tsx +107 -0
- package/templates/fate/cloudflare/client/pages/post/[id].tsx +22 -0
- package/templates/fate/cloudflare/client/pages/search.tsx +99 -0
- package/templates/fate/cloudflare/client/public/icon.svg +1 -0
- package/templates/fate/cloudflare/client/src/App.css +153 -0
- package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +5 -0
- package/templates/fate/cloudflare/client/src/lib/cx.tsx +6 -0
- package/templates/fate/cloudflare/client/src/lib/env.tsx +5 -0
- package/templates/fate/cloudflare/client/src/ui/Badge.tsx +31 -0
- package/templates/fate/cloudflare/client/src/ui/Button.tsx +108 -0
- package/templates/fate/cloudflare/client/src/ui/Card.tsx +32 -0
- package/templates/fate/cloudflare/client/src/ui/CommentCard.tsx +75 -0
- package/templates/fate/cloudflare/client/src/ui/Error.tsx +19 -0
- package/templates/fate/cloudflare/client/src/ui/H2.tsx +15 -0
- package/templates/fate/cloudflare/client/src/ui/H3.tsx +20 -0
- package/templates/fate/cloudflare/client/src/ui/Header.tsx +98 -0
- package/templates/fate/cloudflare/client/src/ui/Input.tsx +17 -0
- package/templates/fate/cloudflare/client/src/ui/Link.tsx +11 -0
- package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +52 -0
- package/templates/fate/cloudflare/client/src/ui/PostCard.tsx +303 -0
- package/templates/fate/cloudflare/client/src/ui/Section.tsx +23 -0
- package/templates/fate/cloudflare/client/src/ui/UserCard.tsx +131 -0
- package/templates/fate/cloudflare/client/src/user/AuthClient.tsx +6 -0
- package/templates/fate/cloudflare/client/translations/de_DE.json +689 -0
- package/templates/fate/cloudflare/client/translations/ja_JP.json +689 -0
- package/templates/fate/cloudflare/client/vite.config.ts +45 -0
- package/templates/fate/cloudflare/client/void.json +29 -0
- package/templates/fate/cloudflare/package.json +46 -0
- package/templates/fate/cloudflare/pnpm-workspace.yaml +39 -0
- package/templates/fate/cloudflare/server/.env.example +1 -0
- package/templates/fate/cloudflare/server/db/db.ts +30 -0
- package/templates/fate/cloudflare/server/db/migrations/20260508073357_initial_schema.sql +133 -0
- package/templates/fate/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql +170 -0
- package/templates/fate/cloudflare/server/db/migrations/meta/20260508073357_snapshot.json +868 -0
- package/templates/fate/cloudflare/server/db/migrations/meta/_journal.json +20 -0
- package/templates/fate/cloudflare/server/db/queries.ts +211 -0
- package/templates/fate/cloudflare/server/db/schema.ts +332 -0
- package/templates/fate/cloudflare/server/package.json +34 -0
- package/templates/fate/cloudflare/server/src/fate/commentPermissions.ts +17 -0
- package/templates/fate/cloudflare/server/src/fate/context.ts +15 -0
- package/templates/fate/cloudflare/server/src/fate/live.ts +7 -0
- package/templates/fate/cloudflare/server/src/fate/server.ts +411 -0
- package/templates/fate/cloudflare/server/src/fate/views.ts +164 -0
- package/templates/fate/cloudflare/server/src/index.ts +80 -0
- package/templates/fate/cloudflare/server/src/lib/auth.ts +66 -0
- package/templates/fate/cloudflare/server/src/lib/origins.ts +17 -0
- package/templates/fate/cloudflare/server/src/router.ts +2 -0
- package/templates/fate/cloudflare/server/src/user/SessionUser.tsx +17 -0
- package/templates/fate/cloudflare/server/tsconfig.json +10 -0
- package/templates/fate/cloudflare/server/wrangler.jsonc +29 -0
- package/templates/fate/cloudflare/tsconfig.json +33 -0
- package/templates/fate/cloudflare/vite.config.ts +72 -0
- package/templates/fate/drizzle/README.md +2 -8
- package/templates/fate/drizzle/client/pages/layout.tsx +2 -2
- package/templates/fate/drizzle/client/src/ui/PostCard.tsx +38 -15
- package/templates/fate/drizzle/pnpm-workspace.yaml +9 -3
- package/templates/fate/drizzle/server/vite.config.ts +1 -1
- package/templates/fate/graphql/.github/workflows/push.yml +27 -0
- package/templates/fate/graphql/.vite-hooks/pre-commit +1 -0
- package/templates/fate/graphql/.vscode/extensions.json +12 -0
- package/templates/fate/graphql/.vscode/settings.json +25 -0
- package/templates/fate/graphql/AGENTS.md +55 -0
- package/templates/fate/graphql/README.md +121 -0
- package/templates/fate/graphql/_gitignore +23 -0
- package/templates/fate/graphql/client/package.json +57 -0
- package/templates/fate/graphql/client/pages/index.tsx +96 -0
- package/templates/fate/graphql/client/pages/layout.tsx +83 -0
- package/templates/fate/graphql/client/pages/login.tsx +107 -0
- package/templates/fate/graphql/client/pages/post/[id].tsx +22 -0
- package/templates/fate/graphql/client/pages/search.tsx +99 -0
- package/templates/fate/graphql/client/public/icon.svg +1 -0
- package/templates/fate/graphql/client/src/App.css +153 -0
- package/templates/fate/graphql/client/src/lib/AvailableLanguages.tsx +5 -0
- package/templates/fate/graphql/client/src/lib/cx.tsx +6 -0
- package/templates/fate/graphql/client/src/lib/env.tsx +5 -0
- package/templates/fate/graphql/client/src/ui/Badge.tsx +31 -0
- package/templates/fate/graphql/client/src/ui/Button.tsx +108 -0
- package/templates/fate/graphql/client/src/ui/Card.tsx +32 -0
- package/templates/fate/graphql/client/src/ui/CommentCard.tsx +75 -0
- package/templates/fate/graphql/client/src/ui/Error.tsx +19 -0
- package/templates/fate/graphql/client/src/ui/H2.tsx +15 -0
- package/templates/fate/graphql/client/src/ui/H3.tsx +20 -0
- package/templates/fate/graphql/client/src/ui/Header.tsx +98 -0
- package/templates/fate/graphql/client/src/ui/Input.tsx +17 -0
- package/templates/fate/graphql/client/src/ui/Link.tsx +11 -0
- package/templates/fate/graphql/client/src/ui/LocaleSwitcher.tsx +52 -0
- package/templates/fate/graphql/client/src/ui/PostCard.tsx +303 -0
- package/templates/fate/graphql/client/src/ui/Section.tsx +23 -0
- package/templates/fate/graphql/client/src/ui/UserCard.tsx +131 -0
- package/templates/fate/graphql/client/src/user/AuthClient.tsx +6 -0
- package/templates/fate/graphql/client/translations/de_DE.json +689 -0
- package/templates/fate/graphql/client/translations/ja_JP.json +689 -0
- package/templates/fate/graphql/client/vite.config.ts +40 -0
- package/templates/fate/graphql/client/void.json +29 -0
- package/templates/fate/graphql/docker-compose.yml +11 -0
- package/templates/fate/graphql/package.json +47 -0
- package/templates/fate/graphql/pnpm-workspace.yaml +45 -0
- package/templates/fate/graphql/server/.env.example +5 -0
- package/templates/fate/graphql/server/README.md +27 -0
- package/templates/fate/graphql/server/package.json +64 -0
- package/templates/fate/graphql/server/prisma.config.ts +17 -0
- package/templates/fate/graphql/server/scripts/generate-graphql.tsx +40 -0
- package/templates/fate/graphql/server/src/__tests__/index.test.tsx +5 -0
- package/templates/fate/graphql/server/src/app.tsx +47 -0
- package/templates/fate/graphql/server/src/graphql/builder.tsx +121 -0
- package/templates/fate/graphql/server/src/graphql/context.tsx +22 -0
- package/templates/fate/graphql/server/src/graphql/fate.ts +85 -0
- package/templates/fate/graphql/server/src/graphql/lib/authDirectives.tsx +93 -0
- package/templates/fate/graphql/server/src/graphql/lib/decodeGlobalID.tsx +12 -0
- package/templates/fate/graphql/server/src/graphql/lib/decodeIDOrThrow.tsx +10 -0
- package/templates/fate/graphql/server/src/graphql/lib/encodeGlobalID.tsx +5 -0
- package/templates/fate/graphql/server/src/graphql/live.tsx +241 -0
- package/templates/fate/graphql/server/src/graphql/mutations.tsx +265 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Category.tsx +35 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Comment.tsx +46 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Event.tsx +71 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Post.tsx +45 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Tag.tsx +11 -0
- package/templates/fate/graphql/server/src/graphql/nodes/User.tsx +28 -0
- package/templates/fate/graphql/server/src/graphql/schema.tsx +16 -0
- package/templates/fate/graphql/server/src/graphql/views.ts +193 -0
- package/templates/fate/graphql/server/src/index.tsx +45 -0
- package/templates/fate/graphql/server/src/lib/auth.tsx +31 -0
- package/templates/fate/graphql/server/src/lib/env.tsx +3 -0
- package/templates/fate/graphql/server/src/lib/origins.ts +18 -0
- package/templates/fate/graphql/server/src/prisma/prisma.tsx +9 -0
- package/templates/fate/graphql/server/src/prisma/schema.prisma +181 -0
- package/templates/fate/graphql/server/src/prisma/seed.tsx +161 -0
- package/templates/fate/graphql/server/src/user/SessionUser.tsx +17 -0
- package/templates/fate/graphql/server/src/user/isAdmin.tsx +3 -0
- package/templates/fate/graphql/server/tsconfig.json +33 -0
- package/templates/fate/graphql/server/vite.config.ts +55 -0
- package/templates/fate/graphql/tsconfig.json +33 -0
- package/templates/fate/graphql/vite.config.ts +80 -0
- package/templates/fate/graphql-client/.env.example +3 -0
- package/templates/fate/graphql-client/AGENTS.md +3 -0
- package/templates/fate/graphql-client/README.md +69 -0
- package/templates/fate/graphql-client/_gitignore +14 -0
- package/templates/fate/graphql-client/package.json +62 -0
- package/templates/fate/graphql-client/pages/index.tsx +69 -0
- package/templates/fate/graphql-client/pages/layout.tsx +41 -0
- package/templates/fate/graphql-client/pnpm-workspace.yaml +38 -0
- package/templates/fate/graphql-client/public/icon.svg +8 -0
- package/templates/fate/graphql-client/src/App.css +28 -0
- package/templates/fate/graphql-client/src/fate/graphql.ts +52 -0
- package/templates/fate/graphql-client/tsconfig.json +30 -0
- package/templates/fate/graphql-client/vite.config.ts +87 -0
- package/templates/fate/graphql-client/void.json +29 -0
- package/templates/fate/http/README.md +2 -8
- package/templates/fate/http/client/pages/layout.tsx +2 -2
- package/templates/fate/http/client/src/ui/PostCard.tsx +37 -15
- package/templates/fate/http/pnpm-workspace.yaml +9 -3
- package/templates/fate/http/server/vite.config.ts +1 -1
- package/templates/fate/prisma/README.md +2 -8
- package/templates/fate/prisma/client/pages/layout.tsx +2 -2
- package/templates/fate/prisma/client/src/ui/PostCard.tsx +37 -15
- package/templates/fate/prisma/pnpm-workspace.yaml +9 -3
- package/templates/fate/prisma/server/vite.config.ts +1 -1
- package/templates/fate/prisma/vite.config.ts +1 -1
- package/templates/fate/void/package.json +10 -2
- package/templates/fate/void/pages/login.tsx +3 -3
- package/templates/fate/void/pnpm-workspace.yaml +12 -4
- package/templates/fate/void/src/ui/Button.tsx +42 -26
- package/templates/fate/void/src/ui/CategoryCard.tsx +3 -7
- package/templates/fate/void/src/ui/PostCard.tsx +38 -15
- package/templates/fate/void/vite.config.ts +15 -14
- package/templates/fate/drizzle/public/fate-logo-dark.svg +0 -4
- package/templates/fate/drizzle/public/fate-logo.svg +0 -4
- package/templates/fate/http/public/fate-logo-dark.svg +0 -4
- package/templates/fate/http/public/fate-logo.svg +0 -4
- package/templates/fate/prisma/public/fate-logo-dark.svg +0 -4
- package/templates/fate/prisma/public/fate-logo.svg +0 -4
package/bin/create-fate.mjs
CHANGED
|
@@ -7,11 +7,26 @@ import { fileURLToPath, URL } from 'node:url';
|
|
|
7
7
|
import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts';
|
|
8
8
|
|
|
9
9
|
const variants = {
|
|
10
|
+
cloudflare: {
|
|
11
|
+
description: 'Cloudflare Workers with D1 and Drizzle',
|
|
12
|
+
label: 'Cloudflare',
|
|
13
|
+
template: 'cloudflare',
|
|
14
|
+
},
|
|
10
15
|
drizzle: {
|
|
11
16
|
description: 'tRPC with Drizzle',
|
|
12
17
|
label: 'Drizzle',
|
|
13
18
|
template: 'drizzle',
|
|
14
19
|
},
|
|
20
|
+
graphql: {
|
|
21
|
+
description: 'GraphQL with Prisma',
|
|
22
|
+
label: 'GraphQL',
|
|
23
|
+
template: 'graphql',
|
|
24
|
+
},
|
|
25
|
+
'graphql-client': {
|
|
26
|
+
description: 'Existing GraphQL server',
|
|
27
|
+
label: 'GraphQL Client',
|
|
28
|
+
template: 'graphql-client',
|
|
29
|
+
},
|
|
15
30
|
http: {
|
|
16
31
|
description: 'Native HTTP with Drizzle',
|
|
17
32
|
label: 'Native HTTP',
|
|
@@ -29,14 +44,27 @@ const variants = {
|
|
|
29
44
|
},
|
|
30
45
|
};
|
|
31
46
|
|
|
32
|
-
const
|
|
47
|
+
const frontendFrameworks = {
|
|
48
|
+
react: {
|
|
49
|
+
description: 'React with react-fate',
|
|
50
|
+
label: 'React',
|
|
51
|
+
},
|
|
52
|
+
vue: {
|
|
53
|
+
description: 'Vue with vue-fate',
|
|
54
|
+
label: 'Vue',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const fateDependencyNames = ['@nkzw/fate', 'cf-fate', 'react-fate', 'void-fate', 'vue-fate'];
|
|
33
59
|
|
|
34
60
|
const usage = () => {
|
|
35
|
-
process.stdout
|
|
61
|
+
process.stdout
|
|
62
|
+
.write(`Usage: create-fate [directory] [--template cloudflare|void|drizzle|graphql|graphql-client|http|prisma] [--framework react|vue]
|
|
36
63
|
|
|
37
64
|
Create a new fate app.
|
|
38
65
|
|
|
39
66
|
Options:
|
|
67
|
+
--framework, -f UI framework to create (react or vue)
|
|
40
68
|
--template, -t Template variant to create
|
|
41
69
|
--no-setup Skip dependency installation and fate client generation
|
|
42
70
|
--help, -h Show this help message
|
|
@@ -51,6 +79,7 @@ const normalizePackageName = (name) =>
|
|
|
51
79
|
.replaceAll(/^-+|-+$/g, '') || 'my-app';
|
|
52
80
|
|
|
53
81
|
const parseArgs = (args) => {
|
|
82
|
+
let framework;
|
|
54
83
|
let setup = true;
|
|
55
84
|
let targetDir;
|
|
56
85
|
let variant;
|
|
@@ -66,6 +95,21 @@ const parseArgs = (args) => {
|
|
|
66
95
|
continue;
|
|
67
96
|
}
|
|
68
97
|
|
|
98
|
+
if (arg === '--framework' || arg === '-f' || arg === '--ui') {
|
|
99
|
+
framework = args[++index];
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (arg?.startsWith('--framework=')) {
|
|
104
|
+
framework = arg.slice('--framework='.length);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (arg?.startsWith('--ui=')) {
|
|
109
|
+
framework = arg.slice('--ui='.length);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
if (arg === '--template' || arg === '-t' || arg === '--variant') {
|
|
70
114
|
variant = args[++index];
|
|
71
115
|
continue;
|
|
@@ -89,7 +133,7 @@ const parseArgs = (args) => {
|
|
|
89
133
|
throw new Error(`Unexpected argument: ${arg}`);
|
|
90
134
|
}
|
|
91
135
|
|
|
92
|
-
return { setup, targetDir, variant };
|
|
136
|
+
return { framework, setup, targetDir, variant };
|
|
93
137
|
};
|
|
94
138
|
|
|
95
139
|
const validateTargetDir = (value) => {
|
|
@@ -155,6 +199,24 @@ const promptForVariant = async () => {
|
|
|
155
199
|
return result;
|
|
156
200
|
};
|
|
157
201
|
|
|
202
|
+
const promptForFramework = async () => {
|
|
203
|
+
const result = await select({
|
|
204
|
+
initialValue: 'react',
|
|
205
|
+
message: 'Select a UI framework',
|
|
206
|
+
options: Object.entries(frontendFrameworks).map(([value, framework]) => ({
|
|
207
|
+
hint: framework.description,
|
|
208
|
+
label: framework.label,
|
|
209
|
+
value,
|
|
210
|
+
})),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (isCancel(result)) {
|
|
214
|
+
abortPrompt();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
};
|
|
219
|
+
|
|
158
220
|
const getRegistryURL = () => {
|
|
159
221
|
const registry = process.env.npm_config_registry || 'https://registry.npmjs.org/';
|
|
160
222
|
return registry.endsWith('/') ? registry : `${registry}/`;
|
|
@@ -215,10 +277,21 @@ const resolveFateDependencyVersions = async (rootDir) => {
|
|
|
215
277
|
const dependencies = [...collectFateDependencies(rootDir)];
|
|
216
278
|
return Object.fromEntries(
|
|
217
279
|
await Promise.all(
|
|
218
|
-
dependencies.map(async (dependencyName) =>
|
|
219
|
-
dependencyName
|
|
220
|
-
|
|
221
|
-
|
|
280
|
+
dependencies.map(async (dependencyName) => {
|
|
281
|
+
if (dependencyName === 'cf-fate') {
|
|
282
|
+
return [dependencyName, 'latest'];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
return [dependencyName, `^${await fetchLatestPackageVersion(dependencyName)}`];
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (dependencyName === 'vue-fate') {
|
|
289
|
+
return [dependencyName, 'latest'];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}),
|
|
222
295
|
),
|
|
223
296
|
);
|
|
224
297
|
};
|
|
@@ -261,6 +334,77 @@ const updatePackageJsonFiles = (rootDir, dir, packageName, fateDependencyVersion
|
|
|
261
334
|
}
|
|
262
335
|
};
|
|
263
336
|
|
|
337
|
+
const readPackageJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
338
|
+
|
|
339
|
+
const writePackageJson = (filePath, json) => {
|
|
340
|
+
fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const removePackageEntries = (record, names) => {
|
|
344
|
+
if (!record) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const name of names) {
|
|
349
|
+
delete record[name];
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const reactPackageEntries = [
|
|
354
|
+
'@nkzw/babel-preset-fbtee',
|
|
355
|
+
'@radix-ui/react-slot',
|
|
356
|
+
'@nkzw/stack',
|
|
357
|
+
'@rolldown/plugin-babel',
|
|
358
|
+
'@types/react',
|
|
359
|
+
'@types/react-dom',
|
|
360
|
+
'@vitejs/plugin-react',
|
|
361
|
+
'@void/react',
|
|
362
|
+
'babel-plugin-react-compiler',
|
|
363
|
+
'eslint-plugin-react-hooks',
|
|
364
|
+
'fbtee',
|
|
365
|
+
'lucide-react',
|
|
366
|
+
'react',
|
|
367
|
+
'react-dom',
|
|
368
|
+
'react-error-boundary',
|
|
369
|
+
'react-fate',
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const vueAgentsGuide = `# Using _fate_ for Vue applications
|
|
373
|
+
|
|
374
|
+
This guidance is for agents working in projects bootstrapped from the fate Vue template. It focuses on Vue-native composables for fate's view composition and request execution.
|
|
375
|
+
|
|
376
|
+
## Core Workflow
|
|
377
|
+
|
|
378
|
+
- **Define views with explicit selections:** Define reusable view objects with \`view<T>()({...})\`, compose them via spreads, and resolve them with \`useView\`.
|
|
379
|
+
- **Resolve data via references:** Avoid passing raw fetched objects; instead pass \`ViewRef\` values to downstream components to keep selections scoped and cache-aware. Then call \`const result = useView(viewDefinition, ref)\` in the component that reads the data.
|
|
380
|
+
- **Request data at route roots:** Use \`useRequest\` to compose all views required for a route into a single typed request; use Vue \`<Suspense>\` and an error boundary around route content.
|
|
381
|
+
- **Compose reusable views:** Extract nested selections into their own views, such as \`UserView\`, and compose them inside parent views, such as \`author: UserView\`, to avoid duplication and overfetching.
|
|
382
|
+
|
|
383
|
+
## Mutations
|
|
384
|
+
|
|
385
|
+
- **Use generated mutations deliberately:** Trigger mutations through the generated fate client APIs and pass optimistic payloads when the UI should update immediately.
|
|
386
|
+
- **Select extra fields when side effects matter:** When a mutation affects related entities, pass a \`view\` to fetch the needed fields in the same round-trip and keep the cache coherent for all dependent views.
|
|
387
|
+
- **Handle loading and errors explicitly:** Vue components should expose pending and error states through refs/resources rather than React Action patterns.
|
|
388
|
+
- **Handle deletions and resets explicitly:** Use the \`delete: true\` flag to evict records from the cache, and send the \`'reset'\` token to clear stale mutation errors without re-running the mutation.
|
|
389
|
+
|
|
390
|
+
## Common Pitfalls
|
|
391
|
+
|
|
392
|
+
- **Use ViewRefs:** Pass references instead of raw data. Keep components focused on views and \`ViewRef\` values to let fate manage data masking and updates.
|
|
393
|
+
- **Co-locate data needs:** Keep view definitions near their components and compose them upward to the request root for predictable fetching.
|
|
394
|
+
- **Use Vue Suspense:** Wrap pages in \`<Suspense>\` and an error boundary instead of ad-hoc loading/error branches where route-level data is required.
|
|
395
|
+
- **Do not duplicate types:** Keep generated fate types and views imported from the server package. Avoid redefining entity shapes on the client.
|
|
396
|
+
- **\`useRequest\` only at route roots:** Create a dedicated \`useRequest\` call per screen root that pulls together all child views; do not scatter \`useRequest\` across leaf components unless it is necessary to issue a separate request.
|
|
397
|
+
- **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.
|
|
398
|
+
|
|
399
|
+
Full documentation can be found on the filesystem at \`./client/node_modules/vue-fate/README.md\` or online at [fate.technology](https://fate.technology/).
|
|
400
|
+
|
|
401
|
+
## Review Checklist for Agents
|
|
402
|
+
|
|
403
|
+
- [ ] Every component that reads server data does so through \`useView\` and receives a \`ViewRef\` prop.
|
|
404
|
+
- [ ] Route components gather data with a single \`useRequest\` call.
|
|
405
|
+
- [ ] Mutations use generated fate client APIs with optimistic updates and necessary \`view\` selections.
|
|
406
|
+
- [ ] Shared selections live in reusable views using regular JavaScript object spreads rather than duplicated field lists.`;
|
|
407
|
+
|
|
264
408
|
const restoreTemplateFileNames = (dir) => {
|
|
265
409
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
266
410
|
const entryPath = path.join(dir, entry.name);
|
|
@@ -275,6 +419,425 @@ const restoreTemplateFileNames = (dir) => {
|
|
|
275
419
|
}
|
|
276
420
|
};
|
|
277
421
|
|
|
422
|
+
const replaceInFiles = (dir, replacements) => {
|
|
423
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
424
|
+
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const entryPath = path.join(dir, entry.name);
|
|
429
|
+
if (entry.isDirectory()) {
|
|
430
|
+
replaceInFiles(entryPath, replacements);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let source = fs.readFileSync(entryPath, 'utf8');
|
|
435
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
436
|
+
source = source.replaceAll(token, value);
|
|
437
|
+
}
|
|
438
|
+
fs.writeFileSync(entryPath, source);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const vueTransportConfigs = {
|
|
443
|
+
cloudflare: {
|
|
444
|
+
clientImports: `import env from '../src/lib/env.ts';`,
|
|
445
|
+
createOptions: `{
|
|
446
|
+
fetch: (input, init) =>
|
|
447
|
+
fetch(input, {
|
|
448
|
+
...init,
|
|
449
|
+
credentials: 'include',
|
|
450
|
+
}),
|
|
451
|
+
liveUrl: \`\${env('SERVER_URL')}/fate-live\`,
|
|
452
|
+
url: \`\${env('SERVER_URL')}/fate\`,
|
|
453
|
+
}`,
|
|
454
|
+
dotenvConfig: `dotenv.config({
|
|
455
|
+
path: join(root, '../server', process.env.NODE_ENV === 'development' || process.env.DEV ? '.env' : '.prod.env'),
|
|
456
|
+
quiet: true,
|
|
457
|
+
})`,
|
|
458
|
+
envKeys: `['SERVER_URL']`,
|
|
459
|
+
envValues: `{
|
|
460
|
+
SERVER_URL: import.meta.env.VITE_SERVER_URL,
|
|
461
|
+
}`,
|
|
462
|
+
fateModule: '@app/server/src/router.ts',
|
|
463
|
+
fateTransport: `transport: 'cloudflare',`,
|
|
464
|
+
typeModule: '@app/server/src/router.ts',
|
|
465
|
+
},
|
|
466
|
+
drizzle: {
|
|
467
|
+
clientImports: `import { httpBatchLink } from '@trpc/client';
|
|
468
|
+
import env from '../src/lib/env.ts';`,
|
|
469
|
+
createOptions: `{
|
|
470
|
+
fetch: (input, init) =>
|
|
471
|
+
fetch(input, {
|
|
472
|
+
...init,
|
|
473
|
+
credentials: 'include',
|
|
474
|
+
}),
|
|
475
|
+
links: [
|
|
476
|
+
httpBatchLink({
|
|
477
|
+
fetch: (input, init) =>
|
|
478
|
+
fetch(input, {
|
|
479
|
+
...init,
|
|
480
|
+
credentials: 'include',
|
|
481
|
+
}),
|
|
482
|
+
url: \`\${env('SERVER_URL')}/trpc\`,
|
|
483
|
+
}),
|
|
484
|
+
],
|
|
485
|
+
liveUrl: \`\${env('SERVER_URL')}/fate\`,
|
|
486
|
+
}`,
|
|
487
|
+
dotenvConfig: `dotenv.config({
|
|
488
|
+
path: join(root, '../server', process.env.NODE_ENV === 'development' || process.env.DEV ? '.env' : '.prod.env'),
|
|
489
|
+
quiet: true,
|
|
490
|
+
})`,
|
|
491
|
+
envKeys: `['SERVER_URL']`,
|
|
492
|
+
envValues: `{
|
|
493
|
+
SERVER_URL: import.meta.env.VITE_SERVER_URL,
|
|
494
|
+
}`,
|
|
495
|
+
fateModule: '@app/server/src/router.ts',
|
|
496
|
+
fateTransport: `transport: 'trpc',`,
|
|
497
|
+
typeModule: '@app/server/src/router.ts',
|
|
498
|
+
},
|
|
499
|
+
graphql: {
|
|
500
|
+
clientImports: `import env from '../src/lib/env.ts';`,
|
|
501
|
+
createOptions: `{
|
|
502
|
+
live: { url: \`\${env('SERVER_URL')}/graphql/stream\` },
|
|
503
|
+
url: \`\${env('SERVER_URL')}/graphql\`,
|
|
504
|
+
}`,
|
|
505
|
+
dotenvConfig: `dotenv.config({
|
|
506
|
+
path: join(root, '../server', process.env.NODE_ENV === 'development' || process.env.DEV ? '.env' : '.prod.env'),
|
|
507
|
+
quiet: true,
|
|
508
|
+
})`,
|
|
509
|
+
envKeys: `['SERVER_URL']`,
|
|
510
|
+
envValues: `{
|
|
511
|
+
SERVER_URL: import.meta.env.VITE_SERVER_URL,
|
|
512
|
+
}`,
|
|
513
|
+
fateModule: '@app/server/src/graphql/fate.ts',
|
|
514
|
+
fateTransport: `transport: 'graphql',`,
|
|
515
|
+
typeModule: '@app/server/src/graphql/fate.ts',
|
|
516
|
+
},
|
|
517
|
+
'graphql-client': {
|
|
518
|
+
clientImports: ``,
|
|
519
|
+
createOptions: `{
|
|
520
|
+
live: import.meta.env.VITE_GRAPHQL_LIVE_URL
|
|
521
|
+
? { url: import.meta.env.VITE_GRAPHQL_LIVE_URL }
|
|
522
|
+
: false,
|
|
523
|
+
url: import.meta.env.VITE_GRAPHQL_URL,
|
|
524
|
+
}`,
|
|
525
|
+
dotenvConfig: `dotenv.config({ path: join(root, '.env'), quiet: true })`,
|
|
526
|
+
envKeys: `[]`,
|
|
527
|
+
envValues: `{}`,
|
|
528
|
+
fateModule: './src/fate/graphql.ts',
|
|
529
|
+
fateTransport: `transport: 'graphql',`,
|
|
530
|
+
typeModule: './fate/graphql.ts',
|
|
531
|
+
},
|
|
532
|
+
http: {
|
|
533
|
+
clientImports: `import env from '../src/lib/env.ts';`,
|
|
534
|
+
createOptions: `{
|
|
535
|
+
fetch: (input, init) =>
|
|
536
|
+
fetch(input, {
|
|
537
|
+
...init,
|
|
538
|
+
credentials: 'include',
|
|
539
|
+
}),
|
|
540
|
+
liveUrl: \`\${env('SERVER_URL')}/fate\`,
|
|
541
|
+
url: \`\${env('SERVER_URL')}/fate\`,
|
|
542
|
+
}`,
|
|
543
|
+
dotenvConfig: `dotenv.config({
|
|
544
|
+
path: join(root, '../server', process.env.NODE_ENV === 'development' || process.env.DEV ? '.env' : '.prod.env'),
|
|
545
|
+
quiet: true,
|
|
546
|
+
})`,
|
|
547
|
+
envKeys: `['SERVER_URL']`,
|
|
548
|
+
envValues: `{
|
|
549
|
+
SERVER_URL: import.meta.env.VITE_SERVER_URL,
|
|
550
|
+
}`,
|
|
551
|
+
fateModule: '@app/server/src/router.ts',
|
|
552
|
+
fateTransport: `transport: 'native',`,
|
|
553
|
+
typeModule: '@app/server/src/router.ts',
|
|
554
|
+
},
|
|
555
|
+
prisma: null,
|
|
556
|
+
void: {
|
|
557
|
+
clientImports: `import { useShared } from '@void/vue';
|
|
558
|
+
import type { SharedData } from '../src/lib/shared.ts';
|
|
559
|
+
|
|
560
|
+
const shared = useShared<SharedData>();`,
|
|
561
|
+
createOptions: `{
|
|
562
|
+
origin: shared.origin,
|
|
563
|
+
userId: shared.auth.user?.id,
|
|
564
|
+
}`,
|
|
565
|
+
dotenvConfig: `dotenv.config({ path: join(root, '.env'), quiet: true })`,
|
|
566
|
+
envKeys: `[]`,
|
|
567
|
+
envValues: `{}`,
|
|
568
|
+
fateModule: './src/fate/server.ts',
|
|
569
|
+
fateTransport: `transport: 'void',`,
|
|
570
|
+
typeModule: './fate/server.ts',
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
vueTransportConfigs.prisma = vueTransportConfigs.drizzle;
|
|
574
|
+
|
|
575
|
+
const vueTemplatePackage = () =>
|
|
576
|
+
path.resolve(packageRoot, 'templates', 'fate', '_shared', 'vue-app');
|
|
577
|
+
|
|
578
|
+
const frontendRootForVariant = (targetPath, selectedVariant) =>
|
|
579
|
+
selectedVariant === 'void' || selectedVariant === 'graphql-client'
|
|
580
|
+
? targetPath
|
|
581
|
+
: path.join(targetPath, 'client');
|
|
582
|
+
|
|
583
|
+
const removeIfExists = (entryPath) => {
|
|
584
|
+
if (fs.existsSync(entryPath)) {
|
|
585
|
+
fs.rmSync(entryPath, { force: true, recursive: true });
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const removeReactFrontend = (frontendRoot, selectedVariant) => {
|
|
590
|
+
if (selectedVariant === 'void' || selectedVariant === 'graphql-client') {
|
|
591
|
+
for (const entry of ['pages', 'src/ui']) {
|
|
592
|
+
removeIfExists(path.join(frontendRoot, entry));
|
|
593
|
+
}
|
|
594
|
+
removeIfExists(path.join(frontendRoot, 'src', 'App.css'));
|
|
595
|
+
removeIfExists(path.join(frontendRoot, 'src', 'lib', 'cx.tsx'));
|
|
596
|
+
removeIfExists(path.join(frontendRoot, 'src', 'user', 'AuthClient.tsx'));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
removeIfExists(frontendRoot);
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const configureVuePackageJson = (frontendRoot, selectedVariant, originalPackageJson) => {
|
|
604
|
+
const packagePath = path.join(frontendRoot, 'package.json');
|
|
605
|
+
const vuePackageJson = readPackageJson(packagePath);
|
|
606
|
+
const packageJson = originalPackageJson
|
|
607
|
+
? {
|
|
608
|
+
...originalPackageJson,
|
|
609
|
+
dependencies: {
|
|
610
|
+
...originalPackageJson.dependencies,
|
|
611
|
+
...vuePackageJson.dependencies,
|
|
612
|
+
},
|
|
613
|
+
devDependencies: {
|
|
614
|
+
...originalPackageJson.devDependencies,
|
|
615
|
+
...vuePackageJson.devDependencies,
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
: vuePackageJson;
|
|
619
|
+
|
|
620
|
+
removePackageEntries(packageJson.dependencies, reactPackageEntries);
|
|
621
|
+
removePackageEntries(packageJson.devDependencies, reactPackageEntries);
|
|
622
|
+
|
|
623
|
+
if (selectedVariant !== 'drizzle' && selectedVariant !== 'prisma') {
|
|
624
|
+
delete packageJson.dependencies?.['@trpc/client'];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (selectedVariant !== 'graphql' && selectedVariant !== 'graphql-client') {
|
|
628
|
+
delete packageJson.dependencies?.['graphql-sse'];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (selectedVariant !== 'graphql-client' && selectedVariant !== 'void') {
|
|
632
|
+
packageJson.dependencies = {
|
|
633
|
+
'@app/server': 'workspace:*',
|
|
634
|
+
...packageJson.dependencies,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (selectedVariant === 'cloudflare') {
|
|
639
|
+
packageJson.dependencies = {
|
|
640
|
+
...packageJson.dependencies,
|
|
641
|
+
'cf-fate': 'latest',
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (packageJson.description) {
|
|
646
|
+
packageJson.description = packageJson.description.replaceAll('React', 'Vue');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
writePackageJson(packagePath, packageJson);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const configureVueRootPackageJson = (targetPath) => {
|
|
653
|
+
const packagePath = path.join(targetPath, 'package.json');
|
|
654
|
+
if (!fs.existsSync(packagePath)) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const packageJson = readPackageJson(packagePath);
|
|
659
|
+
removePackageEntries(packageJson.dependencies, reactPackageEntries);
|
|
660
|
+
removePackageEntries(packageJson.devDependencies, reactPackageEntries);
|
|
661
|
+
removePackageEntries(packageJson.peerDependencies, reactPackageEntries);
|
|
662
|
+
|
|
663
|
+
if (packageJson.description) {
|
|
664
|
+
packageJson.description = packageJson.description.replaceAll('React', 'Vue');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
writePackageJson(packagePath, packageJson);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const configureVueVoidServerFiles = (frontendRoot) => {
|
|
671
|
+
const oldSessionUser = path.join(frontendRoot, 'src', 'user', 'SessionUser.tsx');
|
|
672
|
+
const nextSessionUser = path.join(frontendRoot, 'src', 'user', 'SessionUser.ts');
|
|
673
|
+
if (fs.existsSync(oldSessionUser)) {
|
|
674
|
+
fs.renameSync(oldSessionUser, nextSessionUser);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const contextPath = path.join(frontendRoot, 'src', 'fate', 'context.ts');
|
|
678
|
+
fs.writeFileSync(
|
|
679
|
+
contextPath,
|
|
680
|
+
fs
|
|
681
|
+
.readFileSync(contextPath, 'utf8')
|
|
682
|
+
.replace('../user/SessionUser.tsx', '../user/SessionUser.ts'),
|
|
683
|
+
);
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const configureVueAgents = (targetPath) => {
|
|
687
|
+
const agentsPath = path.join(targetPath, 'AGENTS.md');
|
|
688
|
+
if (!fs.existsSync(agentsPath)) {
|
|
689
|
+
fs.writeFileSync(agentsPath, `${vueAgentsGuide}\n`);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const agents = fs.readFileSync(agentsPath, 'utf8');
|
|
694
|
+
const vitePlusIndex = agents.indexOf('<!--VITE PLUS START-->');
|
|
695
|
+
const suffix = vitePlusIndex === -1 ? agents : agents.slice(vitePlusIndex);
|
|
696
|
+
fs.writeFileSync(agentsPath, `${vueAgentsGuide}\n\n${suffix.trim()}\n`);
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const configureVueReadme = (targetPath, selectedVariant) => {
|
|
700
|
+
const readmePath = path.join(targetPath, 'README.md');
|
|
701
|
+
if (!fs.existsSync(readmePath)) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const readme = fs.readFileSync(readmePath, 'utf8');
|
|
706
|
+
fs.writeFileSync(
|
|
707
|
+
readmePath,
|
|
708
|
+
readme
|
|
709
|
+
.replaceAll(
|
|
710
|
+
`vp create fate my-app --template ${selectedVariant}`,
|
|
711
|
+
`vp create fate my-app --template ${selectedVariant} --framework vue`,
|
|
712
|
+
)
|
|
713
|
+
.replaceAll('modern data client for React', 'modern data client for Vue')
|
|
714
|
+
.replaceAll(
|
|
715
|
+
'data masking, Async React features, and',
|
|
716
|
+
'data masking, Vue-native composables, and',
|
|
717
|
+
)
|
|
718
|
+
.replaceAll('Async React features', 'Vue-native composables')
|
|
719
|
+
.replaceAll('React applications', 'Vue applications')
|
|
720
|
+
.replaceAll(
|
|
721
|
+
'- [React](https://reactjs.org/) with [React Compiler](https://react.dev/learn/react-compiler) enabled',
|
|
722
|
+
'- [Vue](https://vuejs.org/)',
|
|
723
|
+
)
|
|
724
|
+
.replaceAll('The React client application', 'The Vue client application')
|
|
725
|
+
.replaceAll('React components', 'Vue components')
|
|
726
|
+
.replaceAll('React client', 'Vue client')
|
|
727
|
+
.replaceAll('`react-fate`', '`vue-fate`')
|
|
728
|
+
.replaceAll('`@void/react`', '`@void/vue`')
|
|
729
|
+
.replaceAll(', and React Compiler', '')
|
|
730
|
+
.replaceAll(' and React Compiler', '')
|
|
731
|
+
.replaceAll(
|
|
732
|
+
'This template uses the Void pages router with Drizzle, `void-fate`, `vue-fate`, Better Auth, Tailwind.',
|
|
733
|
+
'This template uses Vue, the Void pages router with Drizzle, `void-fate`, `vue-fate`, Better Auth, and Tailwind.',
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const configureVueVoidDemoContent = (targetPath) => {
|
|
739
|
+
replaceInFiles(targetPath, {
|
|
740
|
+
'Actions in fate are exposed for useActionState, and mutations can carry an optimistic object alongside the input. The cache applies the optimistic update immediately, re-renders affected views, and rolls the affected records back if the server rejects the mutation. This example uses the like button because it is easy to see, but the same model works for comments and other records.':
|
|
741
|
+
'Mutations in fate can carry an optimistic object alongside the input. The cache applies the optimistic update immediately, re-renders affected views, and rolls the affected records back if the server rejects the mutation. This example uses the like button because it is easy to see, but the same model works for comments and other records.',
|
|
742
|
+
'async-react': 'vue-runtime',
|
|
743
|
+
'Async React, Suspense, Actions, and the react-fate hooks.':
|
|
744
|
+
'Vue composables, Suspense, resources, and the vue-fate APIs.',
|
|
745
|
+
'normal React': 'the host framework',
|
|
746
|
+
'react-fate': 'vue-fate',
|
|
747
|
+
'React Integration': 'Vue Integration',
|
|
748
|
+
'Suspense, Actions, use, and concurrent rendering patterns.':
|
|
749
|
+
'Vue Suspense, composables, resources, and reactive rendering patterns.',
|
|
750
|
+
'The cache lifetime post is a good reminder to retain manual requests outside React.':
|
|
751
|
+
'The cache lifetime post is a good reminder to retain manual requests outside component setup.',
|
|
752
|
+
'The React integration leans on Suspense instead of local loading flags.':
|
|
753
|
+
'The Vue integration uses Suspense and composables instead of local loading flags.',
|
|
754
|
+
'The React integration posts make it clear that fate is not trying to replace React state.':
|
|
755
|
+
'The Vue integration posts make it clear that fate is not trying to replace Vue state.',
|
|
756
|
+
'The server adapters now cover Prisma and Drizzle with the same data view concepts. Sources describe how to resolve records, fields, lists, counts, and computed values, while fate builds the selected shape for the client. This makes the examples useful for teams with different database layers without changing the React component model.':
|
|
757
|
+
'The server adapters now cover Prisma and Drizzle with the same data view concepts. Sources describe how to resolve records, fields, lists, counts, and computed values, while fate builds the selected shape for the client. This makes the examples useful for teams with different database layers without changing the component data model.',
|
|
758
|
+
'The useActionState integration is practical because the form code still looks like React instead of a custom mutation framework.':
|
|
759
|
+
'The Vue composable integration is practical because the component code still looks like Vue instead of a custom mutation framework.',
|
|
760
|
+
'The Void example is a nice proof that the transport and React APIs are portable.':
|
|
761
|
+
'The Void example is a nice proof that the transport and framework adapters are portable.',
|
|
762
|
+
'The Void example uses the same fate ideas in a full-stack app with file-system routing, shared data, auth, mutations, live comments, categories, tags, and events. It exists to prove the core protocol and React APIs are not tied to one server framework. The seed data includes this post so search and category views can show the newest example alongside the Prisma and Drizzle servers.':
|
|
763
|
+
'The Void example uses the same fate ideas in a full-stack app with file-system routing, shared data, auth, mutations, live comments, categories, tags, and events. It exists to prove the core protocol and framework adapters are not tied to one server framework. The seed data includes this post so search and category views can show the newest example alongside the Prisma and Drizzle servers.',
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const configureVueTemplate = (targetPath, selectedVariant) => {
|
|
768
|
+
const frontendRoot = frontendRootForVariant(targetPath, selectedVariant);
|
|
769
|
+
const originalPackageJson =
|
|
770
|
+
selectedVariant === 'void' || selectedVariant === 'graphql-client'
|
|
771
|
+
? readPackageJson(path.join(frontendRoot, 'package.json'))
|
|
772
|
+
: null;
|
|
773
|
+
|
|
774
|
+
removeReactFrontend(frontendRoot, selectedVariant);
|
|
775
|
+
fs.mkdirSync(frontendRoot, { recursive: true });
|
|
776
|
+
fs.cpSync(vueTemplatePackage(), frontendRoot, { recursive: true });
|
|
777
|
+
|
|
778
|
+
if (selectedVariant === 'void') {
|
|
779
|
+
configureVueVoidServerFiles(frontendRoot);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
configureVuePackageJson(frontendRoot, selectedVariant, originalPackageJson);
|
|
783
|
+
if (frontendRoot !== targetPath) {
|
|
784
|
+
configureVueRootPackageJson(targetPath);
|
|
785
|
+
}
|
|
786
|
+
configureVueAgents(targetPath);
|
|
787
|
+
if (selectedVariant === 'void') {
|
|
788
|
+
configureVueVoidDemoContent(targetPath);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const config = vueTransportConfigs[selectedVariant];
|
|
792
|
+
replaceInFiles(frontendRoot, {
|
|
793
|
+
__CREATE_FATE_OPTIONS__: config.createOptions,
|
|
794
|
+
__DOTENV_CONFIG__: config.dotenvConfig,
|
|
795
|
+
__ENV_KEYS__: config.envKeys,
|
|
796
|
+
__ENV_VALUES__: config.envValues,
|
|
797
|
+
__FATE_MODULE__: config.fateModule,
|
|
798
|
+
__FATE_TYPE_MODULE__: config.typeModule,
|
|
799
|
+
__FMT_IGNORE_PATTERNS__:
|
|
800
|
+
selectedVariant === 'void' || selectedVariant === 'graphql-client'
|
|
801
|
+
? "['.fate/', '.void/', 'dist/', 'node_modules/', 'pnpm-lock.yaml']"
|
|
802
|
+
: "['.fate/', '.void/', 'dist/', 'node_modules/', '../dist/', 'pnpm-lock.yaml']",
|
|
803
|
+
__LINT_IGNORE_PATTERNS__:
|
|
804
|
+
selectedVariant === 'void' || selectedVariant === 'graphql-client'
|
|
805
|
+
? "['.fate', '.void', 'dist', 'node_modules', 'vite.config.ts.timestamp-*']"
|
|
806
|
+
: "['.fate', '.void', 'dist', 'node_modules', '../dist', 'vite.config.ts.timestamp-*']",
|
|
807
|
+
__TSCONFIG_EXTENDS__:
|
|
808
|
+
selectedVariant === 'void' || selectedVariant === 'graphql-client'
|
|
809
|
+
? './.void/tsconfig.json'
|
|
810
|
+
: '../tsconfig.json',
|
|
811
|
+
'"__TSCONFIG_INCLUDE__"':
|
|
812
|
+
selectedVariant === 'void'
|
|
813
|
+
? `[
|
|
814
|
+
".fate/**/*.ts",
|
|
815
|
+
".void/**/*.d.ts",
|
|
816
|
+
"db/**/*.ts",
|
|
817
|
+
"middleware/**/*.ts",
|
|
818
|
+
"pages/**/*.ts",
|
|
819
|
+
"pages/**/*.vue",
|
|
820
|
+
"routes/**/*.ts",
|
|
821
|
+
"src/**/*.ts",
|
|
822
|
+
"src/**/*.vue"
|
|
823
|
+
]`
|
|
824
|
+
: `[
|
|
825
|
+
".fate/**/*.ts",
|
|
826
|
+
".void/**/*.d.ts",
|
|
827
|
+
"pages/**/*.ts",
|
|
828
|
+
"pages/**/*.vue",
|
|
829
|
+
"src/**/*.ts",
|
|
830
|
+
"src/**/*.vue"
|
|
831
|
+
]`,
|
|
832
|
+
'/* __BUILD_CONFIG__ */':
|
|
833
|
+
selectedVariant === 'void' || selectedVariant === 'graphql-client'
|
|
834
|
+
? 'build: {},'
|
|
835
|
+
: "build: { outDir: join(root, '../dist/client') },",
|
|
836
|
+
'/* __CLIENT_IMPORTS__ */': config.clientImports,
|
|
837
|
+
'/* __FATE_TRANSPORT__ */': config.fateTransport,
|
|
838
|
+
});
|
|
839
|
+
};
|
|
840
|
+
|
|
278
841
|
const copyExampleEnvFiles = (dir) => {
|
|
279
842
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
280
843
|
const entryPath = path.join(dir, entry.name);
|
|
@@ -314,10 +877,19 @@ const setupProject = (targetPath, selectedVariant) => {
|
|
|
314
877
|
runCommand('vp', ['install'], targetPath);
|
|
315
878
|
|
|
316
879
|
switch (selectedVariant) {
|
|
880
|
+
case 'cloudflare':
|
|
881
|
+
runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
|
|
882
|
+
runCommand('vp', ['run', 'fate:generate'], targetPath);
|
|
883
|
+
break;
|
|
884
|
+
case 'graphql':
|
|
317
885
|
case 'prisma':
|
|
318
886
|
runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
|
|
319
887
|
runCommand('vp', ['run', 'fate:generate'], targetPath);
|
|
320
888
|
break;
|
|
889
|
+
case 'graphql-client':
|
|
890
|
+
runCommand('vp', ['run', 'prepare:void'], targetPath);
|
|
891
|
+
runCommand('vp', ['run', 'fate:generate'], targetPath);
|
|
892
|
+
break;
|
|
321
893
|
case 'void':
|
|
322
894
|
runCommand('vp', ['run', 'prepare:void'], targetPath);
|
|
323
895
|
runCommand('vp', ['run', 'fate:generate'], targetPath);
|
|
@@ -368,6 +940,13 @@ const main = async () => {
|
|
|
368
940
|
`Unknown template '${selectedVariant}'. Expected one of: ${Object.keys(variants).join(', ')}.`,
|
|
369
941
|
);
|
|
370
942
|
}
|
|
943
|
+
const selectedFramework =
|
|
944
|
+
options.framework ?? (interactive ? await promptForFramework() : 'react');
|
|
945
|
+
if (!selectedFramework || !frontendFrameworks[selectedFramework]) {
|
|
946
|
+
throw new Error(
|
|
947
|
+
`Unknown framework '${selectedFramework}'. Expected one of: ${Object.keys(frontendFrameworks).join(', ')}.`,
|
|
948
|
+
);
|
|
949
|
+
}
|
|
371
950
|
|
|
372
951
|
const targetDir = validateTargetDir(
|
|
373
952
|
options.targetDir ?? (interactive ? await promptForTargetDir() : 'my-app'),
|
|
@@ -378,11 +957,17 @@ const main = async () => {
|
|
|
378
957
|
}
|
|
379
958
|
|
|
380
959
|
const templateRoot = resolveTemplateRoot(variants[selectedVariant].template);
|
|
381
|
-
const fateDependencyVersions = await resolveFateDependencyVersions(templateRoot);
|
|
382
960
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
383
961
|
fs.cpSync(templateRoot, targetPath, { recursive: true });
|
|
962
|
+
if (selectedFramework === 'vue') {
|
|
963
|
+
configureVueTemplate(targetPath, selectedVariant);
|
|
964
|
+
}
|
|
384
965
|
restoreTemplateFileNames(targetPath);
|
|
966
|
+
if (selectedFramework === 'vue') {
|
|
967
|
+
configureVueReadme(targetPath, selectedVariant);
|
|
968
|
+
}
|
|
385
969
|
copyExampleEnvFiles(targetPath);
|
|
970
|
+
const fateDependencyVersions = await resolveFateDependencyVersions(targetPath);
|
|
386
971
|
updatePackageJsonFiles(
|
|
387
972
|
targetPath,
|
|
388
973
|
targetPath,
|
|
@@ -394,7 +979,7 @@ const main = async () => {
|
|
|
394
979
|
setupProject(targetPath, selectedVariant);
|
|
395
980
|
}
|
|
396
981
|
|
|
397
|
-
const message = `Created ${variants[selectedVariant].label} fate app in ${targetDir}`;
|
|
982
|
+
const message = `Created ${frontendFrameworks[selectedFramework].label} ${variants[selectedVariant].label} fate app in ${targetDir}`;
|
|
398
983
|
if (interactive) {
|
|
399
984
|
outro(message);
|
|
400
985
|
} else {
|