@farm.js/create-app 0.1.0-beta.7 → 0.1.0-beta.71
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 +38 -6
- package/bin/create-farm-app.js +7 -2
- package/dist/index.js +3298 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3294 -14
- package/dist/index.mjs.map +1 -1
- package/dist/utils.js +9 -6
- package/dist/utils.js.map +1 -1
- package/dist/utils.mjs +9 -6
- package/dist/utils.mjs.map +1 -1
- package/package.json +2 -2
- package/templates/_integrations/better-auth/preact/farm.config.ts +17 -0
- package/templates/_integrations/better-auth/preact/src/app/dashboard/page.tsx +73 -0
- package/templates/_integrations/better-auth/preact/src/app/layout.tsx +14 -0
- package/templates/_integrations/better-auth/preact/src/app/page.tsx +36 -0
- package/templates/_integrations/better-auth/preact/src/app/sign-in/page.tsx +10 -0
- package/templates/_integrations/better-auth/preact/src/app/sign-up/page.tsx +10 -0
- package/templates/_integrations/better-auth/preact/src/components/auth-form.tsx +92 -0
- package/templates/_integrations/better-auth/preact/src/components/auth-shell.tsx +16 -0
- package/templates/_integrations/better-auth/preact/src/lib/auth-client.ts +5 -0
- package/templates/_integrations/better-auth/solid/farm.config.ts +17 -0
- package/templates/_integrations/better-auth/solid/src/app/dashboard/page.tsx +67 -0
- package/templates/_integrations/better-auth/solid/src/app/layout.tsx +14 -0
- package/templates/_integrations/better-auth/solid/src/app/page.tsx +36 -0
- package/templates/_integrations/better-auth/solid/src/app/sign-in/page.tsx +10 -0
- package/templates/_integrations/better-auth/solid/src/app/sign-up/page.tsx +10 -0
- package/templates/_integrations/better-auth/solid/src/components/auth-form.tsx +91 -0
- package/templates/_integrations/better-auth/solid/src/components/auth-shell.tsx +16 -0
- package/templates/_integrations/better-auth/solid/src/lib/auth-client.ts +5 -0
- package/templates/_integrations/better-auth/svelte/farm.config.ts +17 -0
- package/templates/_integrations/better-auth/svelte/src/app/dashboard/page.svelte +62 -0
- package/templates/_integrations/better-auth/svelte/src/app/layout.svelte +20 -0
- package/templates/_integrations/better-auth/svelte/src/app/page.svelte +32 -0
- package/templates/_integrations/better-auth/svelte/src/app/sign-in/page.svelte +10 -0
- package/templates/_integrations/better-auth/svelte/src/app/sign-up/page.svelte +10 -0
- package/templates/_integrations/better-auth/svelte/src/components/auth-form.svelte +83 -0
- package/templates/_integrations/better-auth/svelte/src/components/auth-shell.svelte +16 -0
- package/templates/_integrations/better-auth/svelte/src/components/resource-links.svelte +47 -0
- package/templates/_integrations/better-auth/svelte/src/lib/auth-client.ts +5 -0
- package/templates/_integrations/better-auth/vue/farm.config.ts +17 -0
- package/templates/_integrations/better-auth/vue/src/app/dashboard/page.vue +62 -0
- package/templates/_integrations/better-auth/vue/src/app/layout.vue +20 -0
- package/templates/_integrations/better-auth/vue/src/app/page.vue +34 -0
- package/templates/_integrations/better-auth/vue/src/app/sign-in/page.vue +12 -0
- package/templates/_integrations/better-auth/vue/src/app/sign-up/page.vue +12 -0
- package/templates/_integrations/better-auth/vue/src/components/auth-form.vue +86 -0
- package/templates/_integrations/better-auth/vue/src/components/auth-shell.vue +15 -0
- package/templates/_integrations/better-auth/vue/src/components/resource-links.vue +43 -0
- package/templates/_integrations/better-auth/vue/src/lib/auth-client.ts +5 -0
- package/templates/_renderers/preact/farm.config.ts +12 -0
- package/templates/_renderers/preact/package.json +26 -0
- package/templates/_renderers/preact/src/app/api/greeting/route.ts +15 -0
- package/templates/_renderers/preact/src/app/layout.tsx +16 -0
- package/templates/_renderers/preact/src/app/page.tsx +66 -0
- package/templates/_renderers/preact/src/app/preact.css +26 -0
- package/templates/_renderers/preact/src/components/resource-links.tsx +71 -0
- package/templates/_renderers/preact/src/lib/api-client.ts +4 -0
- package/templates/_renderers/preact/src/lib/api.generated.ts +7 -0
- package/templates/_renderers/preact/tsconfig.json +25 -0
- package/templates/_renderers/solid/farm.config.ts +12 -0
- package/templates/_renderers/solid/package.json +25 -0
- package/templates/_renderers/solid/src/app/api/greeting/route.ts +15 -0
- package/templates/_renderers/solid/src/app/layout.tsx +15 -0
- package/templates/_renderers/solid/src/app/page.tsx +56 -0
- package/templates/_renderers/solid/src/app/solid.css +22 -0
- package/templates/_renderers/solid/src/components/resource-links.tsx +71 -0
- package/templates/_renderers/solid/src/lib/api-client.ts +4 -0
- package/templates/_renderers/solid/src/lib/api.generated.ts +7 -0
- package/templates/_renderers/solid/tsconfig.json +25 -0
- package/templates/_renderers/svelte/farm.config.ts +9 -0
- package/templates/_renderers/svelte/package.json +28 -0
- package/templates/_renderers/svelte/src/app/api/greeting/route.ts +15 -0
- package/templates/_renderers/svelte/src/app/layout.svelte +21 -0
- package/templates/_renderers/svelte/src/app/page.svelte +64 -0
- package/templates/_renderers/svelte/src/app/svelte.css +26 -0
- package/templates/_renderers/svelte/src/components/resource-links.svelte +31 -0
- package/templates/_renderers/svelte/src/lib/api-client.ts +4 -0
- package/templates/_renderers/svelte/src/lib/api.generated.ts +7 -0
- package/templates/_renderers/svelte/tsconfig.json +23 -0
- package/templates/_renderers/vue/farm.config.ts +12 -0
- package/templates/_renderers/vue/package.json +26 -0
- package/templates/_renderers/vue/src/app/api/greeting/route.ts +15 -0
- package/templates/_renderers/vue/src/app/layout.vue +21 -0
- package/templates/_renderers/vue/src/app/page.vue +69 -0
- package/templates/_renderers/vue/src/app/vue.css +26 -0
- package/templates/_renderers/vue/src/components/resource-links.vue +33 -0
- package/templates/_renderers/vue/src/env.d.ts +7 -0
- package/templates/_renderers/vue/src/lib/api-client.ts +4 -0
- package/templates/_renderers/vue/src/lib/api.generated.ts +7 -0
- package/templates/_renderers/vue/tsconfig.json +23 -0
- package/templates/auth/.env.example +6 -0
- package/templates/auth/README.md +95 -0
- package/templates/auth/docs.config.ts +33 -0
- package/templates/auth/docs.json +36 -0
- package/templates/auth/farm.config.ts +20 -0
- package/templates/auth/gitignore +10 -0
- package/templates/auth/package.json +34 -0
- package/templates/auth/pnpm-workspace.yaml +13 -0
- package/templates/auth/src/app/dashboard/middleware.ts +13 -0
- package/templates/auth/src/app/dashboard/page.tsx +140 -0
- package/templates/auth/src/app/docs/page.md +29 -0
- package/templates/auth/src/app/error.tsx +25 -0
- package/templates/auth/src/app/globals.css +1064 -0
- package/templates/auth/src/app/layout.tsx +11 -0
- package/templates/auth/src/app/loading.tsx +15 -0
- package/templates/auth/src/app/not-found.tsx +26 -0
- package/templates/auth/src/app/page.tsx +46 -0
- package/templates/auth/src/app/sign-in/page.tsx +16 -0
- package/templates/auth/src/app/sign-up/page.tsx +16 -0
- package/templates/auth/src/components/auth-form.tsx +120 -0
- package/templates/auth/src/components/auth-shell.tsx +21 -0
- package/templates/auth/src/components/resource-links.tsx +78 -0
- package/templates/auth/src/components/sign-out-button.tsx +45 -0
- package/templates/auth/src/components/site-header.tsx +46 -0
- package/templates/auth/tsconfig.json +20 -0
- package/templates/basic/docs.config.ts +33 -0
- package/templates/basic/docs.json +36 -0
- package/templates/basic/farm.config.ts +9 -0
- package/templates/basic/package.json +7 -3
- package/templates/basic/pnpm-workspace.yaml +8 -0
- package/templates/basic/src/app/docs/page.md +29 -0
- package/templates/basic/src/app/globals.css +241 -0
- package/templates/basic/src/app/layout.tsx +3 -5
- package/templates/basic/src/app/page.tsx +20 -61
- package/templates/basic/src/components/resource-links.tsx +78 -0
- package/templates/basic/tsconfig.json +4 -0
- package/templates/better-auth/.env.example +3 -0
- package/templates/better-auth/README.md +89 -0
- package/templates/better-auth/docs.config.ts +33 -0
- package/templates/better-auth/docs.json +36 -0
- package/templates/better-auth/farm.config.ts +38 -0
- package/templates/better-auth/gitignore +10 -0
- package/templates/better-auth/package.json +36 -0
- package/templates/better-auth/pnpm-workspace.yaml +13 -0
- package/templates/better-auth/src/app/dashboard/middleware.ts +15 -0
- package/templates/better-auth/src/app/dashboard/page.tsx +188 -0
- package/templates/better-auth/src/app/docs/page.md +29 -0
- package/templates/better-auth/src/app/error.tsx +25 -0
- package/templates/better-auth/src/app/globals.css +1064 -0
- package/templates/better-auth/src/app/layout.tsx +11 -0
- package/templates/better-auth/src/app/loading.tsx +15 -0
- package/templates/better-auth/src/app/not-found.tsx +26 -0
- package/templates/better-auth/src/app/page.tsx +50 -0
- package/templates/better-auth/src/app/sign-in/page.tsx +16 -0
- package/templates/better-auth/src/app/sign-up/page.tsx +16 -0
- package/templates/better-auth/src/components/auth-form.tsx +120 -0
- package/templates/better-auth/src/components/auth-shell.tsx +21 -0
- package/templates/better-auth/src/components/resource-links.tsx +78 -0
- package/templates/better-auth/src/components/sign-out-button.tsx +45 -0
- package/templates/better-auth/src/components/site-header.tsx +46 -0
- package/templates/better-auth/src/lib/auth-client.ts +5 -0
- package/templates/better-auth/src/lib/auth.ts +50 -0
- package/templates/better-auth/src/lib/session.ts +8 -0
- package/templates/better-auth/tsconfig.json +20 -0
- package/templates/react-compiler/README.md +62 -0
- package/templates/react-compiler/docs.config.ts +33 -0
- package/templates/react-compiler/docs.json +36 -0
- package/templates/react-compiler/farm.config.ts +29 -0
- package/templates/react-compiler/gitignore +23 -0
- package/templates/react-compiler/package.json +34 -0
- package/templates/react-compiler/pnpm-workspace.yaml +8 -0
- package/templates/react-compiler/public/favicon.svg +5 -0
- package/templates/react-compiler/scripts/verify-experiment.mjs +113 -0
- package/templates/react-compiler/src/app/docs/page.md +29 -0
- package/templates/react-compiler/src/app/globals.css +470 -0
- package/templates/react-compiler/src/app/layout.tsx +22 -0
- package/templates/react-compiler/src/app/page.tsx +47 -0
- package/templates/react-compiler/src/components/compiler-comparison.tsx +93 -0
- package/templates/react-compiler/src/components/resource-links.tsx +78 -0
- package/templates/react-compiler/src/farm.d.ts +74 -0
- package/templates/react-compiler/src/lib/api.generated.ts +10 -0
- package/templates/react-compiler/tsconfig.json +24 -0
- package/templates/basic/src/app/about/page.tsx +0 -32
package/dist/index.js
CHANGED
|
@@ -8,14 +8,3053 @@ path = require_rolldown_runtime.__toESM(path);
|
|
|
8
8
|
let fs_promises = require("fs/promises");
|
|
9
9
|
fs_promises = require_rolldown_runtime.__toESM(fs_promises);
|
|
10
10
|
let node_child_process = require("node:child_process");
|
|
11
|
+
let node_fs = require("node:fs");
|
|
12
|
+
let node_fs_promises = require("node:fs/promises");
|
|
13
|
+
let node_path = require("node:path");
|
|
14
|
+
node_path = require_rolldown_runtime.__toESM(node_path);
|
|
15
|
+
let node_crypto = require("node:crypto");
|
|
16
|
+
let node_http = require("node:http");
|
|
17
|
+
let node_https = require("node:https");
|
|
18
|
+
let node_os = require("node:os");
|
|
19
|
+
node_os = require_rolldown_runtime.__toESM(node_os);
|
|
20
|
+
//#region ../farm-cli/src/ui-feature-registry.ts
|
|
21
|
+
async function installUIFeature(input) {
|
|
22
|
+
const feature = input.definition.ui;
|
|
23
|
+
if (!feature) {
|
|
24
|
+
input.result.notes.push(`No --ui feature pack is available for ${input.definition.provider} yet.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
input.result.ui = {
|
|
28
|
+
feature: feature.name,
|
|
29
|
+
components: [...feature.components],
|
|
30
|
+
files: []
|
|
31
|
+
};
|
|
32
|
+
input.result.notes.push(`Installed ${feature.description} with shadcn-style local source components.`, ...feature.notes || []);
|
|
33
|
+
await ensureComponentsJson({
|
|
34
|
+
root: input.root,
|
|
35
|
+
dryRun: input.dryRun,
|
|
36
|
+
result: input.result
|
|
37
|
+
});
|
|
38
|
+
await ensureShadcnGlobals({
|
|
39
|
+
root: input.root,
|
|
40
|
+
dryRun: input.dryRun,
|
|
41
|
+
result: input.result
|
|
42
|
+
});
|
|
43
|
+
await ensureTsconfigAlias({
|
|
44
|
+
root: input.root,
|
|
45
|
+
dryRun: input.dryRun,
|
|
46
|
+
result: input.result
|
|
47
|
+
});
|
|
48
|
+
if (!input.skipPackageJson) await updateUIPackageJson({
|
|
49
|
+
root: input.root,
|
|
50
|
+
dryRun: input.dryRun,
|
|
51
|
+
result: input.result
|
|
52
|
+
});
|
|
53
|
+
await writeGeneratedFile({
|
|
54
|
+
root: input.root,
|
|
55
|
+
relativePath: node_path.default.join("src", "lib", "utils.ts"),
|
|
56
|
+
source: shadcnUtilsTemplate(),
|
|
57
|
+
dryRun: input.dryRun,
|
|
58
|
+
force: input.force,
|
|
59
|
+
result: input.result
|
|
60
|
+
});
|
|
61
|
+
for (const component of feature.components) await writeGeneratedFile({
|
|
62
|
+
root: input.root,
|
|
63
|
+
relativePath: node_path.default.join("src", "components", "ui", `${component}.tsx`),
|
|
64
|
+
source: shadcnComponentTemplate(component),
|
|
65
|
+
dryRun: input.dryRun,
|
|
66
|
+
force: input.force,
|
|
67
|
+
result: input.result
|
|
68
|
+
});
|
|
69
|
+
if (feature.needsApiClient !== false) await writeGeneratedFile({
|
|
70
|
+
root: input.root,
|
|
71
|
+
relativePath: node_path.default.join("src", "lib", "api.ts"),
|
|
72
|
+
source: apiClientTemplate(),
|
|
73
|
+
dryRun: input.dryRun,
|
|
74
|
+
force: input.force,
|
|
75
|
+
result: input.result
|
|
76
|
+
});
|
|
77
|
+
for (const file of feature.files({
|
|
78
|
+
key: input.key,
|
|
79
|
+
provider: input.definition.provider
|
|
80
|
+
})) await writeGeneratedFile({
|
|
81
|
+
root: input.root,
|
|
82
|
+
relativePath: file.path,
|
|
83
|
+
source: file.source,
|
|
84
|
+
dryRun: input.dryRun,
|
|
85
|
+
force: input.force,
|
|
86
|
+
result: input.result
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function stripeBillingUIFeature() {
|
|
90
|
+
return billingUIFeature({
|
|
91
|
+
provider: "stripe",
|
|
92
|
+
label: "Stripe"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function polarBillingUIFeature() {
|
|
96
|
+
return billingUIFeature({
|
|
97
|
+
provider: "polar",
|
|
98
|
+
label: "Polar"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function autumnBillingUIFeature() {
|
|
102
|
+
return billingUIFeature({
|
|
103
|
+
provider: "autumn",
|
|
104
|
+
label: "Autumn"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function aiChatUIFeature() {
|
|
108
|
+
return {
|
|
109
|
+
name: "ai-chat",
|
|
110
|
+
description: "AI chat UI",
|
|
111
|
+
components: [
|
|
112
|
+
"badge",
|
|
113
|
+
"button",
|
|
114
|
+
"card",
|
|
115
|
+
"input"
|
|
116
|
+
],
|
|
117
|
+
needsApiClient: false,
|
|
118
|
+
notes: ["Open \"/integrations/ai\" to try the generated chat UI."],
|
|
119
|
+
files: () => [componentFile("ai-chat.tsx", aiChatTemplate()), integrationPageFile("ai", "AIChat", "ai-chat")]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function supabaseAuthUIFeature() {
|
|
123
|
+
return {
|
|
124
|
+
name: "supabase-auth",
|
|
125
|
+
description: "Supabase auth UI",
|
|
126
|
+
components: [
|
|
127
|
+
"badge",
|
|
128
|
+
"button",
|
|
129
|
+
"card",
|
|
130
|
+
"input",
|
|
131
|
+
"label"
|
|
132
|
+
],
|
|
133
|
+
notes: ["Open \"/integrations/supabase\" to try the generated auth UI."],
|
|
134
|
+
files: (input) => [componentFile("supabase-auth-panel.tsx", supabaseAuthTemplate(input.key)), integrationPageFile("supabase", "SupabaseAuthPanel")]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function workosAuthUIFeature() {
|
|
138
|
+
return {
|
|
139
|
+
name: "workos-auth",
|
|
140
|
+
description: "WorkOS auth UI",
|
|
141
|
+
components: [
|
|
142
|
+
"badge",
|
|
143
|
+
"button",
|
|
144
|
+
"card"
|
|
145
|
+
],
|
|
146
|
+
notes: ["Open \"/integrations/workos\" to try the generated auth UI."],
|
|
147
|
+
files: (input) => [componentFile("workos-auth-panel.tsx", hostedAuthTemplate({
|
|
148
|
+
key: input.key,
|
|
149
|
+
provider: "WorkOS",
|
|
150
|
+
componentName: "WorkOSAuthPanel",
|
|
151
|
+
statusCall: "session",
|
|
152
|
+
statusMethod: "get",
|
|
153
|
+
logoutCall: "logout",
|
|
154
|
+
logoutMethod: "post",
|
|
155
|
+
loginHref: "/login?returnTo=/dashboard",
|
|
156
|
+
signupHref: "/signup?returnTo=/dashboard",
|
|
157
|
+
statusLabel: "Session"
|
|
158
|
+
})), integrationPageFile("workos", "WorkOSAuthPanel", "workos-auth-panel")]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function auth0AuthUIFeature() {
|
|
162
|
+
return authRouteShellUIFeature({
|
|
163
|
+
provider: "auth0",
|
|
164
|
+
label: "Auth0",
|
|
165
|
+
componentName: "Auth0AuthPanel",
|
|
166
|
+
signInHref: "/auth/login?returnTo=/dashboard",
|
|
167
|
+
signUpHref: "/auth/signup?returnTo=/dashboard",
|
|
168
|
+
sessionHref: "/auth/profile"
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function clerkAuthUIFeature() {
|
|
172
|
+
return authRouteShellUIFeature({
|
|
173
|
+
provider: "clerk",
|
|
174
|
+
label: "Clerk",
|
|
175
|
+
componentName: "ClerkAuthPanel",
|
|
176
|
+
signInHref: "/sign-in",
|
|
177
|
+
signUpHref: "/sign-up",
|
|
178
|
+
sessionHref: "/dashboard"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function betterAuthUIFeature() {
|
|
182
|
+
return {
|
|
183
|
+
name: "better-auth-auth",
|
|
184
|
+
description: "Better Auth email and password UI",
|
|
185
|
+
components: [
|
|
186
|
+
"badge",
|
|
187
|
+
"button",
|
|
188
|
+
"card",
|
|
189
|
+
"input",
|
|
190
|
+
"label"
|
|
191
|
+
],
|
|
192
|
+
needsApiClient: false,
|
|
193
|
+
notes: ["Open \"/integrations/better-auth\" to try the generated auth UI."],
|
|
194
|
+
files: () => [
|
|
195
|
+
{
|
|
196
|
+
path: node_path.default.join("src", "lib", "auth-client.ts"),
|
|
197
|
+
source: betterAuthClientTemplate()
|
|
198
|
+
},
|
|
199
|
+
componentFile("better-auth-panel.tsx", betterAuthPanelTemplate()),
|
|
200
|
+
integrationPageFile("better-auth", "BetterAuthPanel")
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function authjsUIFeature() {
|
|
205
|
+
return authRouteShellUIFeature({
|
|
206
|
+
provider: "authjs",
|
|
207
|
+
label: "Auth.js",
|
|
208
|
+
componentName: "AuthJsPanel",
|
|
209
|
+
signInHref: "/api/auth/signin",
|
|
210
|
+
signUpHref: "/api/auth/signin",
|
|
211
|
+
sessionHref: "/api/auth/session"
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function resendEmailUIFeature() {
|
|
215
|
+
return {
|
|
216
|
+
name: "resend-email",
|
|
217
|
+
description: "Resend email console UI",
|
|
218
|
+
components: [
|
|
219
|
+
"badge",
|
|
220
|
+
"button",
|
|
221
|
+
"card",
|
|
222
|
+
"input",
|
|
223
|
+
"label"
|
|
224
|
+
],
|
|
225
|
+
notes: ["Open \"/integrations/resend\" to try the generated email UI."],
|
|
226
|
+
files: (input) => [componentFile("resend-email-console.tsx", resendEmailTemplate(input.key)), integrationPageFile("resend", "ResendEmailConsole")]
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function jobsUIFeature(provider) {
|
|
230
|
+
const label = provider === "inngest" ? "Inngest" : "Trigger.dev";
|
|
231
|
+
return {
|
|
232
|
+
name: `${provider}-jobs`,
|
|
233
|
+
description: `${label} jobs console UI`,
|
|
234
|
+
components: [
|
|
235
|
+
"badge",
|
|
236
|
+
"button",
|
|
237
|
+
"card",
|
|
238
|
+
"input",
|
|
239
|
+
"label"
|
|
240
|
+
],
|
|
241
|
+
notes: [`Open "/integrations/jobs-${provider}" to try the generated jobs UI.`],
|
|
242
|
+
files: (input) => [componentFile(`${provider}-jobs-console.tsx`, jobsConsoleTemplate(input.key, label, provider)), integrationPageFile(`jobs-${provider}`, `${pascalCase(provider)}JobsConsole`)]
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function unkeyApiKeysUIFeature() {
|
|
246
|
+
return {
|
|
247
|
+
name: "unkey-api-keys",
|
|
248
|
+
description: "Unkey protected route UI",
|
|
249
|
+
components: ["badge", "card"],
|
|
250
|
+
needsApiClient: false,
|
|
251
|
+
notes: ["Open \"/integrations/unkey\" to test the generated protected API route."],
|
|
252
|
+
files: () => [
|
|
253
|
+
componentFile("unkey-api-keys-console.tsx", unkeyApiKeysTemplate()),
|
|
254
|
+
integrationPageFile("unkey", "UnkeyApiKeysConsole"),
|
|
255
|
+
{
|
|
256
|
+
path: node_path.default.join("src", "app", "api", "protected", "route.ts"),
|
|
257
|
+
source: unkeyProtectedRouteTemplate()
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function billingUIFeature(input) {
|
|
263
|
+
return {
|
|
264
|
+
name: `${input.provider}-billing`,
|
|
265
|
+
description: `${input.label} pricing and checkout UI`,
|
|
266
|
+
components: [
|
|
267
|
+
"badge",
|
|
268
|
+
"button",
|
|
269
|
+
"card"
|
|
270
|
+
],
|
|
271
|
+
notes: [`Open "/integrations/${input.provider}" to try the generated ${input.label} billing UI.`],
|
|
272
|
+
files: (templateInput) => [componentFile(`${input.provider}-billing.tsx`, billingPricingTemplate({
|
|
273
|
+
key: templateInput.key,
|
|
274
|
+
provider: input.provider,
|
|
275
|
+
label: input.label,
|
|
276
|
+
componentName: `${pascalCase(input.provider)}Billing`
|
|
277
|
+
})), integrationPageFile(input.provider, `${pascalCase(input.provider)}Billing`)]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function authRouteShellUIFeature(input) {
|
|
281
|
+
return {
|
|
282
|
+
name: `${input.provider}-auth`,
|
|
283
|
+
description: `${input.label} auth UI`,
|
|
284
|
+
components: [
|
|
285
|
+
"badge",
|
|
286
|
+
"button",
|
|
287
|
+
"card"
|
|
288
|
+
],
|
|
289
|
+
needsApiClient: false,
|
|
290
|
+
notes: [`Open "/integrations/${input.provider}" to try the generated auth UI.`],
|
|
291
|
+
files: () => [componentFile(`${input.provider}-auth-panel.tsx`, authRouteShellTemplate({
|
|
292
|
+
provider: input.label,
|
|
293
|
+
componentName: input.componentName,
|
|
294
|
+
signInHref: input.signInHref,
|
|
295
|
+
signUpHref: input.signUpHref,
|
|
296
|
+
sessionHref: input.sessionHref
|
|
297
|
+
})), integrationPageFile(input.provider, input.componentName, input.provider === "authjs" ? "authjs-auth-panel" : void 0)]
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function componentFile(fileName, source) {
|
|
301
|
+
return {
|
|
302
|
+
path: node_path.default.join("src", "components", "farm", fileName),
|
|
303
|
+
source
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function integrationPageFile(provider, componentName, sourceFileName = kebabCase(componentName)) {
|
|
307
|
+
return {
|
|
308
|
+
path: node_path.default.join("src", "app", "integrations", provider, "page.tsx"),
|
|
309
|
+
source: `import { ${componentName} } from "@/components/farm/${sourceFileName}";
|
|
310
|
+
|
|
311
|
+
export default function ${componentName}Page() {
|
|
312
|
+
return <${componentName} />;
|
|
313
|
+
}
|
|
314
|
+
`
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function writeGeneratedFile(input) {
|
|
318
|
+
const absolutePath = node_path.default.join(input.root, input.relativePath);
|
|
319
|
+
const exists = (0, node_fs.existsSync)(absolutePath);
|
|
320
|
+
const source = resolveGeneratedAliases(input.source, input.relativePath);
|
|
321
|
+
input.result.ui?.files.push(absolutePath);
|
|
322
|
+
if (exists && !input.force) {
|
|
323
|
+
pushResultPath(input.result.skipped, absolutePath);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!input.dryRun) {
|
|
327
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(absolutePath), { recursive: true });
|
|
328
|
+
await (0, node_fs_promises.writeFile)(absolutePath, source, "utf8");
|
|
329
|
+
}
|
|
330
|
+
pushResultPath(exists ? input.result.updated : input.result.created, absolutePath);
|
|
331
|
+
}
|
|
332
|
+
function resolveGeneratedAliases(source, relativePath) {
|
|
333
|
+
const sourceDirectory = node_path.default.dirname(relativePath);
|
|
334
|
+
return source.replace(/(["'])@\/([^"']+)\1/g, (_match, quote, target) => {
|
|
335
|
+
const relativeTarget = node_path.default.relative(sourceDirectory, node_path.default.join("src", target)).split(node_path.default.sep).join("/");
|
|
336
|
+
return `${quote}${relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`}${quote}`;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
async function ensureComponentsJson(input) {
|
|
340
|
+
const componentsJsonPath = node_path.default.join(input.root, "components.json");
|
|
341
|
+
input.result.ui?.files.push(componentsJsonPath);
|
|
342
|
+
const defaults = createComponentsJson();
|
|
343
|
+
if (!(0, node_fs.existsSync)(componentsJsonPath)) {
|
|
344
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(componentsJsonPath, `${JSON.stringify(defaults, null, 2)}\n`, "utf8");
|
|
345
|
+
pushResultPath(input.result.created, componentsJsonPath);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let current;
|
|
349
|
+
try {
|
|
350
|
+
current = JSON.parse(await (0, node_fs_promises.readFile)(componentsJsonPath, "utf8"));
|
|
351
|
+
} catch {
|
|
352
|
+
pushResultPath(input.result.skipped, componentsJsonPath);
|
|
353
|
+
input.result.notes.push("components.json could not be parsed. Keep shadcn aliases pointed at src/components and src/lib/utils.");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const next = mergeComponentsJson(current, defaults);
|
|
357
|
+
if (JSON.stringify(current) === JSON.stringify(next)) {
|
|
358
|
+
pushResultPath(input.result.skipped, componentsJsonPath);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(componentsJsonPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
362
|
+
pushResultPath(input.result.updated, componentsJsonPath);
|
|
363
|
+
}
|
|
364
|
+
async function ensureShadcnGlobals(input) {
|
|
365
|
+
const globalsPath = node_path.default.join(input.root, "src", "app", "globals.css");
|
|
366
|
+
input.result.ui?.files.push(globalsPath);
|
|
367
|
+
if (!(0, node_fs.existsSync)(globalsPath)) {
|
|
368
|
+
const source = `@import "tailwindcss";
|
|
369
|
+
|
|
370
|
+
${SHADCN_THEME_CSS}
|
|
371
|
+
`;
|
|
372
|
+
if (!input.dryRun) {
|
|
373
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(globalsPath), { recursive: true });
|
|
374
|
+
await (0, node_fs_promises.writeFile)(globalsPath, source, "utf8");
|
|
375
|
+
}
|
|
376
|
+
pushResultPath(input.result.created, globalsPath);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const source = await (0, node_fs_promises.readFile)(globalsPath, "utf8");
|
|
380
|
+
const hasTailwindImport = source.includes("@import \"tailwindcss\"");
|
|
381
|
+
const hasTheme = source.includes("--color-background") || source.includes("--background:");
|
|
382
|
+
if (hasTailwindImport && hasTheme) {
|
|
383
|
+
pushResultPath(input.result.skipped, globalsPath);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const nextSource = `${hasTailwindImport ? "" : "@import \"tailwindcss\";\n\n"}${source.trimEnd()}${hasTheme ? "\n" : `
|
|
387
|
+
|
|
388
|
+
${SHADCN_THEME_CSS}
|
|
389
|
+
`}`;
|
|
390
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(globalsPath, nextSource, "utf8");
|
|
391
|
+
pushResultPath(input.result.updated, globalsPath);
|
|
392
|
+
}
|
|
393
|
+
async function ensureTsconfigAlias(input) {
|
|
394
|
+
const tsconfigPath = node_path.default.join(input.root, "tsconfig.json");
|
|
395
|
+
input.result.ui?.files.push(tsconfigPath);
|
|
396
|
+
const defaults = { compilerOptions: {
|
|
397
|
+
baseUrl: ".",
|
|
398
|
+
paths: { "@/*": ["./src/*"] }
|
|
399
|
+
} };
|
|
400
|
+
if (!(0, node_fs.existsSync)(tsconfigPath)) {
|
|
401
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(tsconfigPath, `${JSON.stringify(defaults, null, 2)}\n`, "utf8");
|
|
402
|
+
pushResultPath(input.result.created, tsconfigPath);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
let tsconfig;
|
|
406
|
+
try {
|
|
407
|
+
tsconfig = JSON.parse(await (0, node_fs_promises.readFile)(tsconfigPath, "utf8"));
|
|
408
|
+
} catch {
|
|
409
|
+
pushResultPath(input.result.skipped, tsconfigPath);
|
|
410
|
+
input.result.notes.push("tsconfig.json could not be parsed. Add paths: { \"@/*\": [\"./src/*\"] } manually.");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const compilerOptions = readObject(tsconfig.compilerOptions);
|
|
414
|
+
const paths = readObject(compilerOptions.paths);
|
|
415
|
+
const nextCompilerOptions = {
|
|
416
|
+
...compilerOptions,
|
|
417
|
+
baseUrl: typeof compilerOptions.baseUrl === "string" ? compilerOptions.baseUrl : ".",
|
|
418
|
+
paths: {
|
|
419
|
+
...paths,
|
|
420
|
+
"@/*": ["./src/*"]
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const nextTsconfig = {
|
|
424
|
+
...tsconfig,
|
|
425
|
+
compilerOptions: nextCompilerOptions
|
|
426
|
+
};
|
|
427
|
+
if (JSON.stringify(tsconfig) === JSON.stringify(nextTsconfig)) {
|
|
428
|
+
pushResultPath(input.result.skipped, tsconfigPath);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(tsconfigPath, `${JSON.stringify(nextTsconfig, null, 2)}\n`, "utf8");
|
|
432
|
+
pushResultPath(input.result.updated, tsconfigPath);
|
|
433
|
+
}
|
|
434
|
+
async function updateUIPackageJson(input) {
|
|
435
|
+
const packageJsonPath = node_path.default.join(input.root, "package.json");
|
|
436
|
+
input.result.ui?.files.push(packageJsonPath);
|
|
437
|
+
if (!(0, node_fs.existsSync)(packageJsonPath)) {
|
|
438
|
+
pushResultPath(input.result.skipped, packageJsonPath);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const source = await (0, node_fs_promises.readFile)(packageJsonPath, "utf8");
|
|
442
|
+
const manifest = JSON.parse(source);
|
|
443
|
+
let changed = false;
|
|
444
|
+
for (const [dependency, version] of Object.entries(UI_DEPENDENCIES)) {
|
|
445
|
+
if (hasPackageDependency$1(manifest, dependency)) continue;
|
|
446
|
+
manifest.dependencies = {
|
|
447
|
+
...manifest.dependencies,
|
|
448
|
+
[dependency]: version
|
|
449
|
+
};
|
|
450
|
+
changed = true;
|
|
451
|
+
}
|
|
452
|
+
if (!changed) {
|
|
453
|
+
pushResultPath(input.result.skipped, packageJsonPath);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(packageJsonPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
457
|
+
input.result.packageJson = packageJsonPath;
|
|
458
|
+
pushResultPath(input.result.updated, packageJsonPath);
|
|
459
|
+
}
|
|
460
|
+
const UI_DEPENDENCIES = {
|
|
461
|
+
"class-variance-authority": "^0.7.1",
|
|
462
|
+
clsx: "^2.1.1",
|
|
463
|
+
"tailwind-merge": "^3.3.1",
|
|
464
|
+
tailwindcss: "^4.1.18"
|
|
465
|
+
};
|
|
466
|
+
const SHADCN_THEME_CSS = `@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
|
467
|
+
|
|
468
|
+
@theme inline {
|
|
469
|
+
--color-background: var(--background);
|
|
470
|
+
--color-foreground: var(--foreground);
|
|
471
|
+
--color-card: var(--card);
|
|
472
|
+
--color-card-foreground: var(--card-foreground);
|
|
473
|
+
--color-popover: var(--popover);
|
|
474
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
475
|
+
--color-primary: var(--primary);
|
|
476
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
477
|
+
--color-secondary: var(--secondary);
|
|
478
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
479
|
+
--color-muted: var(--muted);
|
|
480
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
481
|
+
--color-accent: var(--accent);
|
|
482
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
483
|
+
--color-destructive: var(--destructive);
|
|
484
|
+
--color-border: var(--border);
|
|
485
|
+
--color-input: var(--input);
|
|
486
|
+
--color-ring: var(--ring);
|
|
487
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
488
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
489
|
+
--radius-lg: var(--radius);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
:root,
|
|
493
|
+
[data-theme="light"] {
|
|
494
|
+
--radius: 0.5rem;
|
|
495
|
+
--background: oklch(1 0 0);
|
|
496
|
+
--foreground: oklch(0.145 0 0);
|
|
497
|
+
--card: oklch(1 0 0);
|
|
498
|
+
--card-foreground: oklch(0.145 0 0);
|
|
499
|
+
--popover: oklch(1 0 0);
|
|
500
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
501
|
+
--primary: oklch(0.205 0 0);
|
|
502
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
503
|
+
--secondary: oklch(0.97 0 0);
|
|
504
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
505
|
+
--muted: oklch(0.97 0 0);
|
|
506
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
507
|
+
--accent: oklch(0.97 0 0);
|
|
508
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
509
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
510
|
+
--border: oklch(0.922 0 0);
|
|
511
|
+
--input: oklch(0.922 0 0);
|
|
512
|
+
--ring: oklch(0.708 0 0);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
[data-theme="dark"] {
|
|
516
|
+
--background: oklch(0.145 0 0);
|
|
517
|
+
--foreground: oklch(0.985 0 0);
|
|
518
|
+
--card: oklch(0.205 0 0);
|
|
519
|
+
--card-foreground: oklch(0.985 0 0);
|
|
520
|
+
--popover: oklch(0.205 0 0);
|
|
521
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
522
|
+
--primary: oklch(0.922 0 0);
|
|
523
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
524
|
+
--secondary: oklch(0.269 0 0);
|
|
525
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
526
|
+
--muted: oklch(0.269 0 0);
|
|
527
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
528
|
+
--accent: oklch(0.269 0 0);
|
|
529
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
530
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
531
|
+
--border: oklch(1 0 0 / 10%);
|
|
532
|
+
--input: oklch(1 0 0 / 15%);
|
|
533
|
+
--ring: oklch(0.556 0 0);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@layer base {
|
|
537
|
+
* {
|
|
538
|
+
@apply border-border outline-ring/50;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
body {
|
|
542
|
+
@apply bg-background text-foreground;
|
|
543
|
+
}
|
|
544
|
+
}`;
|
|
545
|
+
function createComponentsJson() {
|
|
546
|
+
return {
|
|
547
|
+
$schema: "https://ui.shadcn.com/schema.json",
|
|
548
|
+
style: "new-york",
|
|
549
|
+
rsc: true,
|
|
550
|
+
tsx: true,
|
|
551
|
+
tailwind: {
|
|
552
|
+
config: "",
|
|
553
|
+
css: "src/app/globals.css",
|
|
554
|
+
baseColor: "zinc",
|
|
555
|
+
cssVariables: true
|
|
556
|
+
},
|
|
557
|
+
aliases: {
|
|
558
|
+
components: "@/components",
|
|
559
|
+
utils: "@/lib/utils",
|
|
560
|
+
ui: "@/components/ui",
|
|
561
|
+
lib: "@/lib",
|
|
562
|
+
hooks: "@/hooks"
|
|
563
|
+
},
|
|
564
|
+
registries: { farm: { url: "https://farmjs.dev/r/{name}.json" } }
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function mergeComponentsJson(current, defaults) {
|
|
568
|
+
const aliases = readObject(current.aliases);
|
|
569
|
+
const tailwind = readObject(current.tailwind);
|
|
570
|
+
const registries = readObject(current.registries);
|
|
571
|
+
return {
|
|
572
|
+
...current,
|
|
573
|
+
$schema: typeof current.$schema === "string" ? current.$schema : defaults.$schema,
|
|
574
|
+
style: typeof current.style === "string" ? current.style : defaults.style,
|
|
575
|
+
rsc: typeof current.rsc === "boolean" ? current.rsc : defaults.rsc,
|
|
576
|
+
tsx: typeof current.tsx === "boolean" ? current.tsx : defaults.tsx,
|
|
577
|
+
tailwind: {
|
|
578
|
+
...defaults.tailwind,
|
|
579
|
+
...tailwind
|
|
580
|
+
},
|
|
581
|
+
aliases: {
|
|
582
|
+
...defaults.aliases,
|
|
583
|
+
...aliases
|
|
584
|
+
},
|
|
585
|
+
registries: {
|
|
586
|
+
...registries,
|
|
587
|
+
farm: readObject(registries.farm).url ? registries.farm : defaults.registries.farm
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function shadcnUtilsTemplate() {
|
|
592
|
+
return `import { clsx, type ClassValue } from "clsx";
|
|
593
|
+
import { twMerge } from "tailwind-merge";
|
|
594
|
+
|
|
595
|
+
export function cn(...inputs: ClassValue[]) {
|
|
596
|
+
return twMerge(clsx(inputs));
|
|
597
|
+
}
|
|
598
|
+
`;
|
|
599
|
+
}
|
|
600
|
+
function apiClientTemplate() {
|
|
601
|
+
return `import { createIntegrations } from "@farm.js/core/client";
|
|
602
|
+
import type { AppIntegrations } from "./integrations";
|
|
603
|
+
|
|
604
|
+
export const { api, apiClient } = createIntegrations<AppIntegrations>();
|
|
605
|
+
`;
|
|
606
|
+
}
|
|
607
|
+
function shadcnComponentTemplate(component) {
|
|
608
|
+
switch (component) {
|
|
609
|
+
case "badge": return `import * as React from "react";
|
|
610
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
611
|
+
import { cn } from "@/lib/utils";
|
|
612
|
+
|
|
613
|
+
const badgeVariants = cva(
|
|
614
|
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
615
|
+
{
|
|
616
|
+
variants: {
|
|
617
|
+
variant: {
|
|
618
|
+
default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
619
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
620
|
+
outline: "text-foreground",
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
defaultVariants: {
|
|
624
|
+
variant: "default",
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
export interface BadgeProps
|
|
630
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
631
|
+
VariantProps<typeof badgeVariants> {}
|
|
632
|
+
|
|
633
|
+
export function Badge({ className, variant, ...props }: BadgeProps) {
|
|
634
|
+
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export { badgeVariants };
|
|
638
|
+
`;
|
|
639
|
+
case "button": return `import * as React from "react";
|
|
640
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
641
|
+
import { cn } from "@/lib/utils";
|
|
642
|
+
|
|
643
|
+
const buttonVariants = cva(
|
|
644
|
+
"inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
645
|
+
{
|
|
646
|
+
variants: {
|
|
647
|
+
variant: {
|
|
648
|
+
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
649
|
+
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
650
|
+
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
651
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
652
|
+
},
|
|
653
|
+
size: {
|
|
654
|
+
default: "h-9 px-4 py-2",
|
|
655
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
656
|
+
lg: "h-10 rounded-md px-8",
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
defaultVariants: {
|
|
660
|
+
variant: "default",
|
|
661
|
+
size: "default",
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
export interface ButtonProps
|
|
667
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
668
|
+
VariantProps<typeof buttonVariants> {}
|
|
669
|
+
|
|
670
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
671
|
+
({ className, variant, size, ...props }, ref) => {
|
|
672
|
+
return (
|
|
673
|
+
<button
|
|
674
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
675
|
+
ref={ref}
|
|
676
|
+
{...props}
|
|
677
|
+
/>
|
|
678
|
+
);
|
|
679
|
+
},
|
|
680
|
+
);
|
|
681
|
+
Button.displayName = "Button";
|
|
682
|
+
|
|
683
|
+
export { buttonVariants };
|
|
684
|
+
`;
|
|
685
|
+
case "card": return `import * as React from "react";
|
|
686
|
+
import { cn } from "@/lib/utils";
|
|
687
|
+
|
|
688
|
+
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
689
|
+
({ className, ...props }, ref) => (
|
|
690
|
+
<div
|
|
691
|
+
ref={ref}
|
|
692
|
+
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
|
693
|
+
{...props}
|
|
694
|
+
/>
|
|
695
|
+
),
|
|
696
|
+
);
|
|
697
|
+
Card.displayName = "Card";
|
|
698
|
+
|
|
699
|
+
export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
700
|
+
({ className, ...props }, ref) => (
|
|
701
|
+
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
702
|
+
),
|
|
703
|
+
);
|
|
704
|
+
CardHeader.displayName = "CardHeader";
|
|
705
|
+
|
|
706
|
+
export const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
707
|
+
({ className, ...props }, ref) => (
|
|
708
|
+
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-normal", className)} {...props} />
|
|
709
|
+
),
|
|
710
|
+
);
|
|
711
|
+
CardTitle.displayName = "CardTitle";
|
|
712
|
+
|
|
713
|
+
export const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
714
|
+
({ className, ...props }, ref) => (
|
|
715
|
+
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
716
|
+
),
|
|
717
|
+
);
|
|
718
|
+
CardDescription.displayName = "CardDescription";
|
|
719
|
+
|
|
720
|
+
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
721
|
+
({ className, ...props }, ref) => (
|
|
722
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
723
|
+
),
|
|
724
|
+
);
|
|
725
|
+
CardContent.displayName = "CardContent";
|
|
726
|
+
|
|
727
|
+
export const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
728
|
+
({ className, ...props }, ref) => (
|
|
729
|
+
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
730
|
+
),
|
|
731
|
+
);
|
|
732
|
+
CardFooter.displayName = "CardFooter";
|
|
733
|
+
`;
|
|
734
|
+
case "input": return `import * as React from "react";
|
|
735
|
+
import { cn } from "@/lib/utils";
|
|
736
|
+
|
|
737
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
738
|
+
|
|
739
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
740
|
+
({ className, type, ...props }, ref) => (
|
|
741
|
+
<input
|
|
742
|
+
type={type}
|
|
743
|
+
className={cn(
|
|
744
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
745
|
+
className,
|
|
746
|
+
)}
|
|
747
|
+
ref={ref}
|
|
748
|
+
{...props}
|
|
749
|
+
/>
|
|
750
|
+
),
|
|
751
|
+
);
|
|
752
|
+
Input.displayName = "Input";
|
|
753
|
+
`;
|
|
754
|
+
case "label": return `import * as React from "react";
|
|
755
|
+
import { cn } from "@/lib/utils";
|
|
756
|
+
|
|
757
|
+
export const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
|
|
758
|
+
({ className, ...props }, ref) => (
|
|
759
|
+
<label
|
|
760
|
+
ref={ref}
|
|
761
|
+
className={cn("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", className)}
|
|
762
|
+
{...props}
|
|
763
|
+
/>
|
|
764
|
+
),
|
|
765
|
+
);
|
|
766
|
+
Label.displayName = "Label";
|
|
767
|
+
`;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function billingPricingTemplate(input) {
|
|
771
|
+
return `"use client";
|
|
772
|
+
|
|
773
|
+
import * as React from "react";
|
|
774
|
+
import { Badge } from "@/components/ui/badge";
|
|
775
|
+
import { Button } from "@/components/ui/button";
|
|
776
|
+
import {
|
|
777
|
+
Card,
|
|
778
|
+
CardContent,
|
|
779
|
+
CardDescription,
|
|
780
|
+
CardFooter,
|
|
781
|
+
CardHeader,
|
|
782
|
+
CardTitle,
|
|
783
|
+
} from "@/components/ui/card";
|
|
784
|
+
import { apiClient } from "@/lib/api";
|
|
785
|
+
|
|
786
|
+
type BillingProduct = NonNullable<Awaited<ReturnType<typeof apiClient.${input.key}.products>>["data"]>[number];
|
|
787
|
+
|
|
788
|
+
export function ${input.componentName}() {
|
|
789
|
+
const [products, setProducts] = React.useState<BillingProduct[]>([]);
|
|
790
|
+
const [loading, setLoading] = React.useState(true);
|
|
791
|
+
const [checkingOut, setCheckingOut] = React.useState<string | null>(null);
|
|
792
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
793
|
+
|
|
794
|
+
React.useEffect(() => {
|
|
795
|
+
let active = true;
|
|
796
|
+
|
|
797
|
+
async function loadProducts() {
|
|
798
|
+
setLoading(true);
|
|
799
|
+
setError(null);
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
const response = await apiClient.${input.key}.products();
|
|
803
|
+
if (response.error) {
|
|
804
|
+
throw new Error(readErrorMessage(response.error, "${input.label} request failed."));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (active) {
|
|
808
|
+
setProducts(Array.from(response.data ?? []));
|
|
809
|
+
}
|
|
810
|
+
} catch (cause) {
|
|
811
|
+
if (active) {
|
|
812
|
+
setError(cause instanceof Error ? cause.message : "Could not load ${input.label} products.");
|
|
813
|
+
}
|
|
814
|
+
} finally {
|
|
815
|
+
if (active) {
|
|
816
|
+
setLoading(false);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
void loadProducts();
|
|
822
|
+
|
|
823
|
+
return () => {
|
|
824
|
+
active = false;
|
|
825
|
+
};
|
|
826
|
+
}, []);
|
|
827
|
+
|
|
828
|
+
async function startCheckout(product: BillingProduct) {
|
|
829
|
+
const productId = String(readProductField(product, "id") ?? "");
|
|
830
|
+
if (!productId) {
|
|
831
|
+
setError("This Stripe product is missing an id.");
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
setCheckingOut(productId);
|
|
836
|
+
setError(null);
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
const response = await apiClient.${input.key}.checkout({
|
|
840
|
+
body: {
|
|
841
|
+
productId,
|
|
842
|
+
successPath: "/billing/success",
|
|
843
|
+
cancelPath: "/integrations/${input.provider}",
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
if (response.error) {
|
|
847
|
+
throw new Error(readErrorMessage(response.error, "${input.label} checkout failed."));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const redirectTo = response.data?.redirectTo;
|
|
851
|
+
if (!redirectTo) {
|
|
852
|
+
throw new Error("${input.label} checkout did not return a redirect URL.");
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
window.location.assign(redirectTo);
|
|
856
|
+
} catch (cause) {
|
|
857
|
+
setError(cause instanceof Error ? cause.message : "Could not start ${input.label} checkout.");
|
|
858
|
+
setCheckingOut(null);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return (
|
|
863
|
+
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
|
|
864
|
+
<section className="mx-auto flex w-full max-w-5xl flex-col gap-8">
|
|
865
|
+
<div className="max-w-2xl space-y-3">
|
|
866
|
+
<Badge variant="secondary">${input.label}</Badge>
|
|
867
|
+
<h1 className="text-3xl font-semibold tracking-normal">${input.label} billing</h1>
|
|
868
|
+
<p className="text-sm leading-6 text-muted-foreground">
|
|
869
|
+
Plans, checkout, and customer billing actions in one place.
|
|
870
|
+
</p>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
873
|
+
{error ? (
|
|
874
|
+
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
875
|
+
{error}
|
|
876
|
+
</div>
|
|
877
|
+
) : null}
|
|
878
|
+
|
|
879
|
+
{loading ? (
|
|
880
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
881
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
882
|
+
<Card key={index} className="min-h-[220px] animate-pulse" />
|
|
883
|
+
))}
|
|
884
|
+
</div>
|
|
885
|
+
) : products.length ? (
|
|
886
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
887
|
+
{products.map((product) => {
|
|
888
|
+
const productId = String(readProductField(product, "id") ?? "");
|
|
889
|
+
const fallbackName = productId || "Product";
|
|
890
|
+
const name = String(readProductField(product, "name") ?? fallbackName);
|
|
891
|
+
const description =
|
|
892
|
+
readProductField(product, "description") ?? "Connected to your Stripe catalog.";
|
|
893
|
+
|
|
894
|
+
return (
|
|
895
|
+
<Card key={productId || name} className="flex min-h-[260px] flex-col">
|
|
896
|
+
<CardHeader>
|
|
897
|
+
<CardTitle className="text-xl">{name}</CardTitle>
|
|
898
|
+
<CardDescription>{String(description)}</CardDescription>
|
|
899
|
+
</CardHeader>
|
|
900
|
+
<CardContent className="flex-1">
|
|
901
|
+
<div className="text-3xl font-semibold">{formatProductPrice(product)}</div>
|
|
902
|
+
</CardContent>
|
|
903
|
+
<CardFooter>
|
|
904
|
+
<Button
|
|
905
|
+
className="w-full"
|
|
906
|
+
disabled={!productId || checkingOut === productId}
|
|
907
|
+
onClick={() => void startCheckout(product)}
|
|
908
|
+
>
|
|
909
|
+
{checkingOut === productId ? "Starting checkout..." : "Checkout"}
|
|
910
|
+
</Button>
|
|
911
|
+
</CardFooter>
|
|
912
|
+
</Card>
|
|
913
|
+
);
|
|
914
|
+
})}
|
|
915
|
+
</div>
|
|
916
|
+
) : (
|
|
917
|
+
<Card>
|
|
918
|
+
<CardHeader>
|
|
919
|
+
<CardTitle>No products yet</CardTitle>
|
|
920
|
+
<CardDescription>
|
|
921
|
+
Add products to the generated ${input.label} integration template or provider dashboard.
|
|
922
|
+
</CardDescription>
|
|
923
|
+
</CardHeader>
|
|
924
|
+
</Card>
|
|
925
|
+
)}
|
|
926
|
+
</section>
|
|
927
|
+
</main>
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function readProductField(product: BillingProduct, field: string) {
|
|
932
|
+
return (product as Record<string, unknown>)[field];
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function formatProductPrice(product: BillingProduct) {
|
|
936
|
+
const amount = readProductField(product, "amount") ?? readProductField(product, "unitAmount");
|
|
937
|
+
const currency = String(readProductField(product, "currency") ?? "USD").toUpperCase();
|
|
938
|
+
const interval = readProductField(product, "interval");
|
|
939
|
+
|
|
940
|
+
if (typeof amount === "number" && Number.isFinite(amount)) {
|
|
941
|
+
const formatted = new Intl.NumberFormat("en", {
|
|
942
|
+
style: "currency",
|
|
943
|
+
currency,
|
|
944
|
+
}).format(amount / 100);
|
|
945
|
+
|
|
946
|
+
return interval ? \`\${formatted}/\${String(interval)}\` : formatted;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return "Custom";
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function readErrorMessage(error: unknown, fallback: string) {
|
|
953
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
954
|
+
return String((error as { message?: unknown }).message);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return fallback;
|
|
958
|
+
}
|
|
959
|
+
`;
|
|
960
|
+
}
|
|
961
|
+
function aiChatTemplate() {
|
|
962
|
+
return `"use client";
|
|
963
|
+
|
|
964
|
+
import * as React from "react";
|
|
965
|
+
import { Badge } from "@/components/ui/badge";
|
|
966
|
+
import { Button } from "@/components/ui/button";
|
|
967
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
968
|
+
import { Input } from "@/components/ui/input";
|
|
969
|
+
|
|
970
|
+
type ChatMessage = {
|
|
971
|
+
role: "user" | "assistant";
|
|
972
|
+
content: string;
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
export function AIChat() {
|
|
976
|
+
const [messages, setMessages] = React.useState<ChatMessage[]>([]);
|
|
977
|
+
const [input, setInput] = React.useState("");
|
|
978
|
+
const [pending, setPending] = React.useState(false);
|
|
979
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
980
|
+
|
|
981
|
+
async function sendMessage(event: React.FormEvent<HTMLFormElement>) {
|
|
982
|
+
event.preventDefault();
|
|
983
|
+
const nextInput = input.trim();
|
|
984
|
+
if (!nextInput) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const nextMessages: ChatMessage[] = [...messages, { role: "user", content: nextInput }];
|
|
989
|
+
setMessages(nextMessages);
|
|
990
|
+
setInput("");
|
|
991
|
+
setPending(true);
|
|
992
|
+
setError(null);
|
|
993
|
+
|
|
994
|
+
try {
|
|
995
|
+
const response = await fetch("/api/chat", {
|
|
996
|
+
method: "POST",
|
|
997
|
+
headers: {
|
|
998
|
+
"content-type": "application/json",
|
|
999
|
+
},
|
|
1000
|
+
body: JSON.stringify({
|
|
1001
|
+
messages: nextMessages.map((message) => ({
|
|
1002
|
+
role: message.role,
|
|
1003
|
+
parts: [{ type: "text", text: message.content }],
|
|
1004
|
+
})),
|
|
1005
|
+
}),
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
const text = await response.text();
|
|
1009
|
+
if (!response.ok) {
|
|
1010
|
+
throw new Error(text || "AI request failed.");
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
setMessages([...nextMessages, { role: "assistant", content: text || "Done." }]);
|
|
1014
|
+
} catch (cause) {
|
|
1015
|
+
setError(cause instanceof Error ? cause.message : "AI request failed.");
|
|
1016
|
+
} finally {
|
|
1017
|
+
setPending(false);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return (
|
|
1022
|
+
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
|
|
1023
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-6">
|
|
1024
|
+
<div className="space-y-3">
|
|
1025
|
+
<Badge variant="secondary">AI</Badge>
|
|
1026
|
+
<h1 className="text-3xl font-semibold tracking-normal">Chat</h1>
|
|
1027
|
+
</div>
|
|
1028
|
+
|
|
1029
|
+
<Card>
|
|
1030
|
+
<CardHeader>
|
|
1031
|
+
<CardTitle className="text-xl">Conversation</CardTitle>
|
|
1032
|
+
</CardHeader>
|
|
1033
|
+
<CardContent className="space-y-4">
|
|
1034
|
+
<div className="min-h-[320px] space-y-3 rounded-md border bg-muted/30 p-4">
|
|
1035
|
+
{messages.length ? (
|
|
1036
|
+
messages.map((message, index) => (
|
|
1037
|
+
<div
|
|
1038
|
+
key={index}
|
|
1039
|
+
className={message.role === "user" ? "ml-auto max-w-[85%] rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground" : "max-w-[85%] rounded-md bg-background px-3 py-2 text-sm"}
|
|
1040
|
+
>
|
|
1041
|
+
{message.content}
|
|
1042
|
+
</div>
|
|
1043
|
+
))
|
|
1044
|
+
) : (
|
|
1045
|
+
<p className="text-sm text-muted-foreground">Start a conversation.</p>
|
|
1046
|
+
)}
|
|
1047
|
+
</div>
|
|
1048
|
+
|
|
1049
|
+
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
|
1050
|
+
|
|
1051
|
+
<form className="flex gap-2" onSubmit={sendMessage}>
|
|
1052
|
+
<Input
|
|
1053
|
+
value={input}
|
|
1054
|
+
onChange={(event) => setInput(event.target.value)}
|
|
1055
|
+
placeholder="Ask something..."
|
|
1056
|
+
/>
|
|
1057
|
+
<Button disabled={pending} type="submit">
|
|
1058
|
+
{pending ? "Sending..." : "Send"}
|
|
1059
|
+
</Button>
|
|
1060
|
+
</form>
|
|
1061
|
+
</CardContent>
|
|
1062
|
+
</Card>
|
|
1063
|
+
</section>
|
|
1064
|
+
</main>
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
`;
|
|
1068
|
+
}
|
|
1069
|
+
function supabaseAuthTemplate(key) {
|
|
1070
|
+
return `"use client";
|
|
1071
|
+
|
|
1072
|
+
import * as React from "react";
|
|
1073
|
+
import { Badge } from "@/components/ui/badge";
|
|
1074
|
+
import { Button } from "@/components/ui/button";
|
|
1075
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
1076
|
+
import { Input } from "@/components/ui/input";
|
|
1077
|
+
import { Label } from "@/components/ui/label";
|
|
1078
|
+
import { apiClient } from "@/lib/api";
|
|
1079
|
+
|
|
1080
|
+
export function SupabaseAuthPanel() {
|
|
1081
|
+
const [email, setEmail] = React.useState("");
|
|
1082
|
+
const [password, setPassword] = React.useState("");
|
|
1083
|
+
const [mode, setMode] = React.useState<"login" | "signup">("login");
|
|
1084
|
+
const [pending, setPending] = React.useState(false);
|
|
1085
|
+
const [status, setStatus] = React.useState<string | null>(null);
|
|
1086
|
+
|
|
1087
|
+
async function submit(event: React.FormEvent<HTMLFormElement>) {
|
|
1088
|
+
event.preventDefault();
|
|
1089
|
+
setPending(true);
|
|
1090
|
+
setStatus(null);
|
|
1091
|
+
|
|
1092
|
+
const response =
|
|
1093
|
+
mode === "login"
|
|
1094
|
+
? await apiClient.${key}.login.post({ body: { email, password, returnTo: "/dashboard" } })
|
|
1095
|
+
: await apiClient.${key}.signup.post({ body: { email, password, returnTo: "/dashboard" } });
|
|
1096
|
+
|
|
1097
|
+
if (response.error) {
|
|
1098
|
+
setStatus(response.error.message);
|
|
1099
|
+
setPending(false);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (response.data?.redirectTo) {
|
|
1104
|
+
window.location.assign(response.data.redirectTo);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
setStatus(response.data?.message ?? "Check your email to continue.");
|
|
1109
|
+
setPending(false);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
async function loadSession() {
|
|
1113
|
+
const response = await apiClient.${key}.session.get();
|
|
1114
|
+
setStatus(response.error ? response.error.message : response.data?.authenticated ? "Authenticated" : "No active session");
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async function logout() {
|
|
1118
|
+
const response = await fetch("/auth/logout", {
|
|
1119
|
+
method: "POST",
|
|
1120
|
+
headers: { "content-type": "application/json" },
|
|
1121
|
+
body: JSON.stringify({ returnTo: "/" }),
|
|
1122
|
+
});
|
|
1123
|
+
const data = (await response.json()) as { redirectTo?: string; error?: string };
|
|
1124
|
+
if (data.redirectTo) {
|
|
1125
|
+
window.location.assign(data.redirectTo);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
setStatus(data.error ?? "Signed out");
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return (
|
|
1132
|
+
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
|
|
1133
|
+
<section className="mx-auto flex w-full max-w-xl flex-col gap-6">
|
|
1134
|
+
<div className="space-y-3">
|
|
1135
|
+
<Badge variant="secondary">Supabase</Badge>
|
|
1136
|
+
<h1 className="text-3xl font-semibold tracking-normal">Auth</h1>
|
|
1137
|
+
</div>
|
|
1138
|
+
|
|
1139
|
+
<Card>
|
|
1140
|
+
<CardHeader>
|
|
1141
|
+
<CardTitle className="text-xl">{mode === "login" ? "Sign in" : "Create account"}</CardTitle>
|
|
1142
|
+
<CardDescription>Email and password access for this app.</CardDescription>
|
|
1143
|
+
</CardHeader>
|
|
1144
|
+
<CardContent>
|
|
1145
|
+
<form className="space-y-4" onSubmit={submit}>
|
|
1146
|
+
<div className="space-y-2">
|
|
1147
|
+
<Label htmlFor="email">Email</Label>
|
|
1148
|
+
<Input id="email" value={email} onChange={(event) => setEmail(event.target.value)} type="email" />
|
|
1149
|
+
</div>
|
|
1150
|
+
<div className="space-y-2">
|
|
1151
|
+
<Label htmlFor="password">Password</Label>
|
|
1152
|
+
<Input id="password" value={password} onChange={(event) => setPassword(event.target.value)} type="password" />
|
|
1153
|
+
</div>
|
|
1154
|
+
{status ? <p className="text-sm text-muted-foreground">{status}</p> : null}
|
|
1155
|
+
<div className="flex flex-wrap gap-2">
|
|
1156
|
+
<Button disabled={pending} type="submit">{pending ? "Working..." : mode === "login" ? "Sign in" : "Sign up"}</Button>
|
|
1157
|
+
<Button type="button" variant="outline" onClick={() => setMode(mode === "login" ? "signup" : "login")}>
|
|
1158
|
+
{mode === "login" ? "Use sign up" : "Use sign in"}
|
|
1159
|
+
</Button>
|
|
1160
|
+
<Button type="button" variant="ghost" onClick={() => void loadSession()}>Session</Button>
|
|
1161
|
+
<Button type="button" variant="ghost" onClick={() => void logout()}>Logout</Button>
|
|
1162
|
+
</div>
|
|
1163
|
+
</form>
|
|
1164
|
+
</CardContent>
|
|
1165
|
+
</Card>
|
|
1166
|
+
</section>
|
|
1167
|
+
</main>
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
`;
|
|
1171
|
+
}
|
|
1172
|
+
function hostedAuthTemplate(input) {
|
|
1173
|
+
return `"use client";
|
|
1174
|
+
|
|
1175
|
+
import * as React from "react";
|
|
1176
|
+
import { Badge } from "@/components/ui/badge";
|
|
1177
|
+
import { Button } from "@/components/ui/button";
|
|
1178
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
1179
|
+
import { apiClient } from "@/lib/api";
|
|
1180
|
+
|
|
1181
|
+
export function ${input.componentName}() {
|
|
1182
|
+
const [status, setStatus] = React.useState<string>("Idle");
|
|
1183
|
+
const [pending, setPending] = React.useState(false);
|
|
1184
|
+
|
|
1185
|
+
async function refreshStatus() {
|
|
1186
|
+
setPending(true);
|
|
1187
|
+
const response = await apiClient.${input.key}.${input.statusCall}.${input.statusMethod}();
|
|
1188
|
+
if (response.error) {
|
|
1189
|
+
setStatus(response.error.message);
|
|
1190
|
+
} else {
|
|
1191
|
+
setStatus(response.data?.authenticated ? "Authenticated" : "No active session");
|
|
1192
|
+
}
|
|
1193
|
+
setPending(false);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function logout() {
|
|
1197
|
+
setPending(true);
|
|
1198
|
+
const response = await apiClient.${input.key}.${input.logoutCall}.${input.logoutMethod}();
|
|
1199
|
+
if (response.data?.redirectTo) {
|
|
1200
|
+
window.location.assign(response.data.redirectTo);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
setStatus(response.error?.message ?? "Signed out");
|
|
1204
|
+
setPending(false);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return (
|
|
1208
|
+
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
|
|
1209
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-6">
|
|
1210
|
+
<div className="space-y-3">
|
|
1211
|
+
<Badge variant="secondary">${input.provider}</Badge>
|
|
1212
|
+
<h1 className="text-3xl font-semibold tracking-normal">Auth</h1>
|
|
1213
|
+
</div>
|
|
1214
|
+
|
|
1215
|
+
<Card>
|
|
1216
|
+
<CardHeader>
|
|
1217
|
+
<CardTitle className="text-xl">${input.provider} session</CardTitle>
|
|
1218
|
+
<CardDescription>Hosted auth, account session, and sign-out controls.</CardDescription>
|
|
1219
|
+
</CardHeader>
|
|
1220
|
+
<CardContent className="space-y-4">
|
|
1221
|
+
<p className="rounded-md border bg-muted/30 px-3 py-2 text-sm">{status}</p>
|
|
1222
|
+
<div className="flex flex-wrap gap-2">
|
|
1223
|
+
<Button type="button" onClick={() => window.location.assign("${input.loginHref}")}>Sign in</Button>
|
|
1224
|
+
<Button type="button" variant="outline" onClick={() => window.location.assign("${input.signupHref}")}>Sign up</Button>
|
|
1225
|
+
<Button type="button" variant="ghost" disabled={pending} onClick={() => void refreshStatus()}>Refresh</Button>
|
|
1226
|
+
<Button type="button" variant="ghost" disabled={pending} onClick={() => void logout()}>Logout</Button>
|
|
1227
|
+
</div>
|
|
1228
|
+
</CardContent>
|
|
1229
|
+
</Card>
|
|
1230
|
+
</section>
|
|
1231
|
+
</main>
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
`;
|
|
1235
|
+
}
|
|
1236
|
+
function authRouteShellTemplate(input) {
|
|
1237
|
+
return `"use client";
|
|
1238
|
+
|
|
1239
|
+
import { Badge } from "@/components/ui/badge";
|
|
1240
|
+
import { Button } from "@/components/ui/button";
|
|
1241
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
1242
|
+
|
|
1243
|
+
export function ${input.componentName}() {
|
|
1244
|
+
return (
|
|
1245
|
+
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
|
|
1246
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-6">
|
|
1247
|
+
<div className="space-y-3">
|
|
1248
|
+
<Badge variant="secondary">${input.provider}</Badge>
|
|
1249
|
+
<h1 className="text-3xl font-semibold tracking-normal">Auth</h1>
|
|
1250
|
+
</div>
|
|
1251
|
+
|
|
1252
|
+
<Card>
|
|
1253
|
+
<CardHeader>
|
|
1254
|
+
<CardTitle className="text-xl">${input.provider} routes</CardTitle>
|
|
1255
|
+
<CardDescription>Account entry points and session route.</CardDescription>
|
|
1256
|
+
</CardHeader>
|
|
1257
|
+
<CardContent className="flex flex-wrap gap-2">
|
|
1258
|
+
<Button type="button" onClick={() => window.location.assign("${input.signInHref}")}>Sign in</Button>
|
|
1259
|
+
<Button type="button" variant="outline" onClick={() => window.location.assign("${input.signUpHref}")}>Sign up</Button>
|
|
1260
|
+
<Button type="button" variant="ghost" onClick={() => window.location.assign("${input.sessionHref}")}>Session</Button>
|
|
1261
|
+
</CardContent>
|
|
1262
|
+
</Card>
|
|
1263
|
+
</section>
|
|
1264
|
+
</main>
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
`;
|
|
1268
|
+
}
|
|
1269
|
+
function betterAuthClientTemplate() {
|
|
1270
|
+
return `import { createAuthClient } from "better-auth/react";
|
|
1271
|
+
|
|
1272
|
+
export const authClient = createAuthClient({
|
|
1273
|
+
baseURL: "",
|
|
1274
|
+
});
|
|
1275
|
+
`;
|
|
1276
|
+
}
|
|
1277
|
+
function betterAuthPanelTemplate() {
|
|
1278
|
+
return `"use client";
|
|
1279
|
+
|
|
1280
|
+
import * as React from "react";
|
|
1281
|
+
import { Badge } from "@/components/ui/badge";
|
|
1282
|
+
import { Button } from "@/components/ui/button";
|
|
1283
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
1284
|
+
import { Input } from "@/components/ui/input";
|
|
1285
|
+
import { Label } from "@/components/ui/label";
|
|
1286
|
+
import { authClient } from "@/lib/auth-client";
|
|
1287
|
+
|
|
1288
|
+
type Mode = "sign-in" | "sign-up";
|
|
1289
|
+
|
|
1290
|
+
export function BetterAuthPanel() {
|
|
1291
|
+
const [mode, setMode] = React.useState<Mode>("sign-in");
|
|
1292
|
+
const [pending, setPending] = React.useState(false);
|
|
1293
|
+
const [message, setMessage] = React.useState("Ready");
|
|
1294
|
+
const [sessionEmail, setSessionEmail] = React.useState<string | null>(null);
|
|
1295
|
+
|
|
1296
|
+
async function submit(event: React.FormEvent<HTMLFormElement>) {
|
|
1297
|
+
event.preventDefault();
|
|
1298
|
+
setPending(true);
|
|
1299
|
+
setMessage(mode === "sign-in" ? "Signing in…" : "Creating account…");
|
|
1300
|
+
|
|
1301
|
+
const form = new FormData(event.currentTarget);
|
|
1302
|
+
const email = String(form.get("email") || "");
|
|
1303
|
+
const password = String(form.get("password") || "");
|
|
1304
|
+
const name = String(form.get("name") || "");
|
|
1305
|
+
try {
|
|
1306
|
+
const response =
|
|
1307
|
+
mode === "sign-in"
|
|
1308
|
+
? await authClient.signIn.email({ email, password })
|
|
1309
|
+
: await authClient.signUp.email({ email, password, name });
|
|
1310
|
+
|
|
1311
|
+
if (response.error) {
|
|
1312
|
+
setMessage(response.error.message || "Authentication failed.");
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
setSessionEmail(email);
|
|
1317
|
+
setMessage(mode === "sign-in" ? "Signed in." : "Account created.");
|
|
1318
|
+
} catch (cause) {
|
|
1319
|
+
setMessage(cause instanceof Error ? cause.message : "Could not reach the auth server.");
|
|
1320
|
+
} finally {
|
|
1321
|
+
setPending(false);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async function refreshSession() {
|
|
1326
|
+
setPending(true);
|
|
1327
|
+
try {
|
|
1328
|
+
const response = await authClient.getSession();
|
|
1329
|
+
setSessionEmail(response.data?.user.email || null);
|
|
1330
|
+
setMessage(response.error?.message || (response.data ? "Session active." : "No active session."));
|
|
1331
|
+
} catch (cause) {
|
|
1332
|
+
setMessage(cause instanceof Error ? cause.message : "Could not read the session.");
|
|
1333
|
+
} finally {
|
|
1334
|
+
setPending(false);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
async function signOut() {
|
|
1339
|
+
setPending(true);
|
|
1340
|
+
try {
|
|
1341
|
+
const response = await authClient.signOut();
|
|
1342
|
+
if (response.error) {
|
|
1343
|
+
setMessage(response.error.message || "Could not sign out.");
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
setSessionEmail(null);
|
|
1347
|
+
setMessage("Signed out.");
|
|
1348
|
+
} catch (cause) {
|
|
1349
|
+
setMessage(cause instanceof Error ? cause.message : "Could not reach the auth server.");
|
|
1350
|
+
} finally {
|
|
1351
|
+
setPending(false);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
return (
|
|
1356
|
+
<main className="min-h-screen bg-background px-5 py-12 text-foreground sm:px-8">
|
|
1357
|
+
<section className="mx-auto grid w-full max-w-5xl gap-8 lg:grid-cols-[1fr_420px] lg:items-start">
|
|
1358
|
+
<div className="space-y-5 py-4">
|
|
1359
|
+
<Badge variant="secondary">Better Auth × Farm.js</Badge>
|
|
1360
|
+
<div className="space-y-3">
|
|
1361
|
+
<h1 className="max-w-xl text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
1362
|
+
Authentication that starts ready.
|
|
1363
|
+
</h1>
|
|
1364
|
+
<p className="max-w-xl text-base leading-7 text-muted-foreground">
|
|
1365
|
+
Test account creation, email sign-in, session reads, and sign-out through Farm’s
|
|
1366
|
+
generated Better Auth integration.
|
|
1367
|
+
</p>
|
|
1368
|
+
</div>
|
|
1369
|
+
<div aria-live="polite" className="border-l-2 border-primary pl-4 text-sm">
|
|
1370
|
+
<p className="font-medium">{message}</p>
|
|
1371
|
+
<p className="mt-1 text-muted-foreground">
|
|
1372
|
+
{sessionEmail ? \`Signed in as \${sessionEmail}\` : "No authenticated user"}
|
|
1373
|
+
</p>
|
|
1374
|
+
</div>
|
|
1375
|
+
</div>
|
|
1376
|
+
|
|
1377
|
+
<Card>
|
|
1378
|
+
<CardHeader>
|
|
1379
|
+
<CardTitle>{mode === "sign-in" ? "Welcome back" : "Create an account"}</CardTitle>
|
|
1380
|
+
<CardDescription>
|
|
1381
|
+
{mode === "sign-in"
|
|
1382
|
+
? "Enter your credentials to start a secure session."
|
|
1383
|
+
: "Use an email and password to create your local account."}
|
|
1384
|
+
</CardDescription>
|
|
1385
|
+
</CardHeader>
|
|
1386
|
+
<CardContent>
|
|
1387
|
+
<form className="space-y-4" onSubmit={submit}>
|
|
1388
|
+
{mode === "sign-up" ? (
|
|
1389
|
+
<div className="space-y-2">
|
|
1390
|
+
<Label htmlFor="name">Name</Label>
|
|
1391
|
+
<Input autoComplete="name" id="name" name="name" required />
|
|
1392
|
+
</div>
|
|
1393
|
+
) : null}
|
|
1394
|
+
<div className="space-y-2">
|
|
1395
|
+
<Label htmlFor="email">Email</Label>
|
|
1396
|
+
<Input autoComplete="email" id="email" name="email" required type="email" />
|
|
1397
|
+
</div>
|
|
1398
|
+
<div className="space-y-2">
|
|
1399
|
+
<Label htmlFor="password">Password</Label>
|
|
1400
|
+
<Input
|
|
1401
|
+
autoComplete={mode === "sign-in" ? "current-password" : "new-password"}
|
|
1402
|
+
id="password"
|
|
1403
|
+
minLength={8}
|
|
1404
|
+
name="password"
|
|
1405
|
+
required
|
|
1406
|
+
type="password"
|
|
1407
|
+
/>
|
|
1408
|
+
</div>
|
|
1409
|
+
<Button className="w-full" disabled={pending} type="submit">
|
|
1410
|
+
{pending ? "Working…" : mode === "sign-in" ? "Sign in" : "Create account"}
|
|
1411
|
+
</Button>
|
|
1412
|
+
</form>
|
|
1413
|
+
|
|
1414
|
+
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
|
1415
|
+
<Button
|
|
1416
|
+
disabled={pending}
|
|
1417
|
+
type="button"
|
|
1418
|
+
variant="outline"
|
|
1419
|
+
onClick={() => {
|
|
1420
|
+
setMode(mode === "sign-in" ? "sign-up" : "sign-in");
|
|
1421
|
+
setMessage("Ready");
|
|
1422
|
+
}}
|
|
1423
|
+
>
|
|
1424
|
+
{mode === "sign-in" ? "Create account" : "Use sign in"}
|
|
1425
|
+
</Button>
|
|
1426
|
+
<Button disabled={pending} type="button" variant="outline" onClick={() => void refreshSession()}>
|
|
1427
|
+
Check session
|
|
1428
|
+
</Button>
|
|
1429
|
+
</div>
|
|
1430
|
+
<Button
|
|
1431
|
+
className="mt-2 w-full"
|
|
1432
|
+
disabled={pending || !sessionEmail}
|
|
1433
|
+
type="button"
|
|
1434
|
+
variant="ghost"
|
|
1435
|
+
onClick={() => void signOut()}
|
|
1436
|
+
>
|
|
1437
|
+
Sign out
|
|
1438
|
+
</Button>
|
|
1439
|
+
</CardContent>
|
|
1440
|
+
</Card>
|
|
1441
|
+
</section>
|
|
1442
|
+
</main>
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
`;
|
|
1446
|
+
}
|
|
1447
|
+
function resendEmailTemplate(key) {
|
|
1448
|
+
return `"use client";
|
|
1449
|
+
|
|
1450
|
+
import * as React from "react";
|
|
1451
|
+
import { Badge } from "@/components/ui/badge";
|
|
1452
|
+
import { Button } from "@/components/ui/button";
|
|
1453
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
1454
|
+
import { Input } from "@/components/ui/input";
|
|
1455
|
+
import { Label } from "@/components/ui/label";
|
|
1456
|
+
import { apiClient } from "@/lib/api";
|
|
1457
|
+
|
|
1458
|
+
export function ResendEmailConsole() {
|
|
1459
|
+
const [to, setTo] = React.useState("");
|
|
1460
|
+
const [name, setName] = React.useState("Ada");
|
|
1461
|
+
const templateId = "welcome" as const;
|
|
1462
|
+
const [status, setStatus] = React.useState("Idle");
|
|
1463
|
+
const [previewHtml, setPreviewHtml] = React.useState("");
|
|
1464
|
+
|
|
1465
|
+
async function loadTemplates() {
|
|
1466
|
+
const response = await apiClient.${key}.templates();
|
|
1467
|
+
const templates = Array.isArray(response.data)
|
|
1468
|
+
? (response.data as Array<{ id: string }>)
|
|
1469
|
+
: [];
|
|
1470
|
+
setStatus(response.error ? response.error.message : "Templates: " + templates.map((item) => item.id).join(", "));
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
async function preview() {
|
|
1474
|
+
const response = await apiClient.${key}.preview({
|
|
1475
|
+
body: {
|
|
1476
|
+
templateId,
|
|
1477
|
+
data: { name },
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
if (response.error) {
|
|
1481
|
+
setStatus(response.error.message);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
setPreviewHtml(response.data?.html ?? "");
|
|
1485
|
+
setStatus(response.data?.subject ?? "Preview loaded");
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async function send() {
|
|
1489
|
+
const response = await apiClient.${key}.send({
|
|
1490
|
+
body: {
|
|
1491
|
+
templateId,
|
|
1492
|
+
to,
|
|
1493
|
+
data: { name },
|
|
1494
|
+
},
|
|
1495
|
+
});
|
|
1496
|
+
setStatus(response.error ? response.error.message : "Sent " + (response.data?.id ?? "email"));
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
return (
|
|
1500
|
+
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
|
|
1501
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-6">
|
|
1502
|
+
<div className="space-y-3">
|
|
1503
|
+
<Badge variant="secondary">Resend</Badge>
|
|
1504
|
+
<h1 className="text-3xl font-semibold tracking-normal">Email console</h1>
|
|
1505
|
+
</div>
|
|
1506
|
+
<Card>
|
|
1507
|
+
<CardHeader>
|
|
1508
|
+
<CardTitle className="text-xl">Send template</CardTitle>
|
|
1509
|
+
<CardDescription>Template previews and delivery controls.</CardDescription>
|
|
1510
|
+
</CardHeader>
|
|
1511
|
+
<CardContent className="space-y-4">
|
|
1512
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
1513
|
+
<div className="space-y-2">
|
|
1514
|
+
<Label htmlFor="to">To</Label>
|
|
1515
|
+
<Input id="to" value={to} onChange={(event) => setTo(event.target.value)} />
|
|
1516
|
+
</div>
|
|
1517
|
+
<div className="space-y-2">
|
|
1518
|
+
<Label htmlFor="name">Name</Label>
|
|
1519
|
+
<Input id="name" value={name} onChange={(event) => setName(event.target.value)} />
|
|
1520
|
+
</div>
|
|
1521
|
+
</div>
|
|
1522
|
+
<p className="rounded-md border bg-muted/30 px-3 py-2 text-sm">{status}</p>
|
|
1523
|
+
{previewHtml ? <div className="max-h-64 overflow-auto rounded-md border p-3 text-sm" dangerouslySetInnerHTML={{ __html: previewHtml }} /> : null}
|
|
1524
|
+
<div className="flex flex-wrap gap-2">
|
|
1525
|
+
<Button type="button" variant="outline" onClick={() => void loadTemplates()}>Templates</Button>
|
|
1526
|
+
<Button type="button" variant="outline" onClick={() => void preview()}>Preview</Button>
|
|
1527
|
+
<Button type="button" onClick={() => void send()}>Send</Button>
|
|
1528
|
+
</div>
|
|
1529
|
+
</CardContent>
|
|
1530
|
+
</Card>
|
|
1531
|
+
</section>
|
|
1532
|
+
</main>
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
`;
|
|
1536
|
+
}
|
|
1537
|
+
function jobsConsoleTemplate(key, label, provider) {
|
|
1538
|
+
return `"use client";
|
|
1539
|
+
|
|
1540
|
+
import * as React from "react";
|
|
1541
|
+
import { Badge } from "@/components/ui/badge";
|
|
1542
|
+
import { Button } from "@/components/ui/button";
|
|
1543
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
1544
|
+
import { Input } from "@/components/ui/input";
|
|
1545
|
+
import { Label } from "@/components/ui/label";
|
|
1546
|
+
import { apiClient } from "@/lib/api";
|
|
1547
|
+
|
|
1548
|
+
type JobTask = {
|
|
1549
|
+
key: string;
|
|
1550
|
+
description?: string | null;
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
export function ${`${pascalCase(provider)}JobsConsole`}() {
|
|
1554
|
+
const [tasks, setTasks] = React.useState<JobTask[]>([]);
|
|
1555
|
+
const [taskKey, setTaskKey] = React.useState("");
|
|
1556
|
+
const [runId, setRunId] = React.useState("");
|
|
1557
|
+
const [status, setStatus] = React.useState("Idle");
|
|
1558
|
+
|
|
1559
|
+
async function loadTasks() {
|
|
1560
|
+
const response = await apiClient.${key}.tasks.list();
|
|
1561
|
+
if (response.error) {
|
|
1562
|
+
setStatus(response.error.message);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const nextTasks = Array.from(response.data ?? []);
|
|
1566
|
+
setTasks(nextTasks);
|
|
1567
|
+
setTaskKey(nextTasks[0]?.key ?? "");
|
|
1568
|
+
setStatus(nextTasks.length ? "Tasks loaded" : "No tasks registered yet");
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function triggerTask() {
|
|
1572
|
+
const task = (apiClient.${key} as Record<string, any>)[taskKey];
|
|
1573
|
+
if (!task?.trigger) {
|
|
1574
|
+
setStatus("Select a task first.");
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const response = await task.trigger({ body: { input: {} } });
|
|
1578
|
+
setStatus(response.error ? response.error.message : "Triggered " + (response.data?.runId ?? response.data?.id ?? taskKey));
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
async function checkStatus() {
|
|
1582
|
+
const task = (apiClient.${key} as Record<string, any>)[taskKey];
|
|
1583
|
+
if (!task?.status || !runId) {
|
|
1584
|
+
setStatus("Enter a task and run id.");
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const response = await task.status({ query: { runId } });
|
|
1588
|
+
setStatus(response.error ? response.error.message : JSON.stringify(response.data));
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
return (
|
|
1592
|
+
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
|
|
1593
|
+
<section className="mx-auto flex w-full max-w-4xl flex-col gap-6">
|
|
1594
|
+
<div className="space-y-3">
|
|
1595
|
+
<Badge variant="secondary">${label}</Badge>
|
|
1596
|
+
<h1 className="text-3xl font-semibold tracking-normal">Jobs console</h1>
|
|
1597
|
+
</div>
|
|
1598
|
+
<Card>
|
|
1599
|
+
<CardHeader>
|
|
1600
|
+
<CardTitle className="text-xl">Tasks</CardTitle>
|
|
1601
|
+
<CardDescription>Task runs and status checks.</CardDescription>
|
|
1602
|
+
</CardHeader>
|
|
1603
|
+
<CardContent className="space-y-4">
|
|
1604
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
1605
|
+
<div className="space-y-2">
|
|
1606
|
+
<Label htmlFor="taskKey">Task key</Label>
|
|
1607
|
+
<Input id="taskKey" value={taskKey} onChange={(event) => setTaskKey(event.target.value)} />
|
|
1608
|
+
</div>
|
|
1609
|
+
<div className="space-y-2">
|
|
1610
|
+
<Label htmlFor="runId">Run id</Label>
|
|
1611
|
+
<Input id="runId" value={runId} onChange={(event) => setRunId(event.target.value)} />
|
|
1612
|
+
</div>
|
|
1613
|
+
</div>
|
|
1614
|
+
<p className="rounded-md border bg-muted/30 px-3 py-2 text-sm">{status}</p>
|
|
1615
|
+
<div className="flex flex-wrap gap-2">
|
|
1616
|
+
<Button type="button" variant="outline" onClick={() => void loadTasks()}>Load tasks</Button>
|
|
1617
|
+
<Button type="button" onClick={() => void triggerTask()}>Trigger</Button>
|
|
1618
|
+
<Button type="button" variant="ghost" onClick={() => void checkStatus()}>Status</Button>
|
|
1619
|
+
</div>
|
|
1620
|
+
{tasks.length ? (
|
|
1621
|
+
<div className="grid gap-2">
|
|
1622
|
+
{tasks.map((task) => (
|
|
1623
|
+
<button key={task.key} className="rounded-md border px-3 py-2 text-left text-sm" onClick={() => setTaskKey(task.key)} type="button">
|
|
1624
|
+
{task.key}
|
|
1625
|
+
</button>
|
|
1626
|
+
))}
|
|
1627
|
+
</div>
|
|
1628
|
+
) : null}
|
|
1629
|
+
</CardContent>
|
|
1630
|
+
</Card>
|
|
1631
|
+
</section>
|
|
1632
|
+
</main>
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
`;
|
|
1636
|
+
}
|
|
1637
|
+
function unkeyApiKeysTemplate() {
|
|
1638
|
+
return `import { Badge } from "@/components/ui/badge";
|
|
1639
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
1640
|
+
|
|
1641
|
+
export function UnkeyApiKeysConsole() {
|
|
1642
|
+
return (
|
|
1643
|
+
<main className="min-h-screen bg-background px-6 py-12 text-foreground">
|
|
1644
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-6">
|
|
1645
|
+
<div className="space-y-3">
|
|
1646
|
+
<Badge variant="secondary">Unkey</Badge>
|
|
1647
|
+
<h1 className="text-3xl font-semibold tracking-normal">API keys</h1>
|
|
1648
|
+
</div>
|
|
1649
|
+
<Card>
|
|
1650
|
+
<CardHeader>
|
|
1651
|
+
<CardTitle className="text-xl">Protected API</CardTitle>
|
|
1652
|
+
<CardDescription>Requests are verified by Unkey before the route runs.</CardDescription>
|
|
1653
|
+
</CardHeader>
|
|
1654
|
+
<CardContent className="space-y-3">
|
|
1655
|
+
<code className="block overflow-x-auto rounded-md border bg-muted/30 p-3 text-xs">
|
|
1656
|
+
curl http://localhost:3000/api/protected -H "Authorization: Bearer YOUR_KEY"
|
|
1657
|
+
</code>
|
|
1658
|
+
<p className="text-sm text-muted-foreground">
|
|
1659
|
+
Create and rotate keys from trusted server code or the Unkey dashboard.
|
|
1660
|
+
</p>
|
|
1661
|
+
</CardContent>
|
|
1662
|
+
</Card>
|
|
1663
|
+
</section>
|
|
1664
|
+
</main>
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
`;
|
|
1668
|
+
}
|
|
1669
|
+
function unkeyProtectedRouteTemplate() {
|
|
1670
|
+
return `export function GET() {
|
|
1671
|
+
return Response.json({
|
|
1672
|
+
ok: true,
|
|
1673
|
+
message: "Valid Unkey API key.",
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
`;
|
|
1677
|
+
}
|
|
1678
|
+
function pascalCase(input) {
|
|
1679
|
+
return input.split(/[^A-Za-z0-9]+/g).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
|
|
1680
|
+
}
|
|
1681
|
+
function kebabCase(input) {
|
|
1682
|
+
return input.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
1683
|
+
}
|
|
1684
|
+
function hasPackageDependency$1(manifest, dependency) {
|
|
1685
|
+
return dependency in (manifest.dependencies || {}) || dependency in (manifest.devDependencies || {}) || dependency in (manifest.peerDependencies || {}) || dependency in (manifest.optionalDependencies || {});
|
|
1686
|
+
}
|
|
1687
|
+
function readObject(value) {
|
|
1688
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1689
|
+
}
|
|
1690
|
+
function pushResultPath(list, filePath) {
|
|
1691
|
+
if (!list.includes(filePath)) list.push(filePath);
|
|
1692
|
+
}
|
|
1693
|
+
//#endregion
|
|
1694
|
+
//#region ../farm-cli/src/add-integration.ts
|
|
1695
|
+
const PROVIDERS = [
|
|
1696
|
+
{
|
|
1697
|
+
provider: "ai",
|
|
1698
|
+
aliases: [
|
|
1699
|
+
"ai-sdk",
|
|
1700
|
+
"vercel-ai",
|
|
1701
|
+
"vercel-ai-sdk",
|
|
1702
|
+
"chat"
|
|
1703
|
+
],
|
|
1704
|
+
defaultKey: "chat",
|
|
1705
|
+
packageName: "@farm.js/ai",
|
|
1706
|
+
fileName: "chat",
|
|
1707
|
+
exportName: "POST",
|
|
1708
|
+
description: "Vercel AI SDK chat route",
|
|
1709
|
+
env: ["AI_GATEWAY_API_KEY"],
|
|
1710
|
+
notes: [
|
|
1711
|
+
"Use @ai-sdk/react useChat with api: \"/api/chat\" on the client.",
|
|
1712
|
+
"Replace model with any AI SDK provider model or Vercel AI Gateway model id.",
|
|
1713
|
+
"No farm.config integration wiring is required for this route."
|
|
1714
|
+
],
|
|
1715
|
+
ui: aiChatUIFeature(),
|
|
1716
|
+
template: () => `import { aiChatRoute } from "@farm.js/ai";
|
|
1717
|
+
|
|
1718
|
+
export const POST = aiChatRoute({
|
|
1719
|
+
model: "openai/gpt-4o-mini",
|
|
1720
|
+
system: "You are a helpful assistant.",
|
|
1721
|
+
});
|
|
1722
|
+
`
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
provider: "stripe",
|
|
1726
|
+
aliases: [
|
|
1727
|
+
"billing-stripe",
|
|
1728
|
+
"payments",
|
|
1729
|
+
"stripe-billing"
|
|
1730
|
+
],
|
|
1731
|
+
defaultKey: "billing",
|
|
1732
|
+
packageName: "@farm.js/stripe",
|
|
1733
|
+
fileName: "stripe",
|
|
1734
|
+
exportName: "stripeIntegration",
|
|
1735
|
+
description: "Stripe billing and checkout routes",
|
|
1736
|
+
env: ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"],
|
|
1737
|
+
template: () => `import type { FarmIntegrationLogEvent } from "@farm.js/core";
|
|
1738
|
+
import { stripe } from "@farm.js/stripe";
|
|
1739
|
+
|
|
1740
|
+
export const stripeIntegration = stripe({
|
|
1741
|
+
secretKey: process.env.STRIPE_SECRET_KEY,
|
|
1742
|
+
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
1743
|
+
products: [],
|
|
1744
|
+
log(event: FarmIntegrationLogEvent) {
|
|
1745
|
+
console.log("[stripe]", event.phase, event.route?.path || "none");
|
|
1746
|
+
},
|
|
1747
|
+
});
|
|
1748
|
+
`,
|
|
1749
|
+
ui: stripeBillingUIFeature()
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
provider: "supabase",
|
|
1753
|
+
aliases: ["auth-supabase", "supabase-auth"],
|
|
1754
|
+
defaultKey: "auth",
|
|
1755
|
+
packageName: "@farm.js/supabase",
|
|
1756
|
+
fileName: "supabase",
|
|
1757
|
+
exportName: "supabaseIntegration",
|
|
1758
|
+
description: "Supabase auth routes and middleware",
|
|
1759
|
+
env: [
|
|
1760
|
+
"SUPABASE_URL",
|
|
1761
|
+
"SUPABASE_ANON_KEY",
|
|
1762
|
+
"APP_BASE_URL"
|
|
1763
|
+
],
|
|
1764
|
+
ui: supabaseAuthUIFeature(),
|
|
1765
|
+
template: () => `import { supabase } from "@farm.js/supabase";
|
|
1766
|
+
|
|
1767
|
+
export const supabaseIntegration = supabase({
|
|
1768
|
+
callbackUrl: \`\${process.env.APP_BASE_URL || "http://localhost:3000"}/auth/callback\`,
|
|
1769
|
+
protectedRoutes: ["/dashboard(.*)"],
|
|
1770
|
+
pages: {
|
|
1771
|
+
signIn: "/sign-in",
|
|
1772
|
+
signUp: "/sign-up",
|
|
1773
|
+
},
|
|
1774
|
+
log(event) {
|
|
1775
|
+
console.log("[supabase]", event.phase, event.route?.path || "none");
|
|
1776
|
+
},
|
|
1777
|
+
});
|
|
1778
|
+
`
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
provider: "workos",
|
|
1782
|
+
aliases: ["auth-workos", "workos-auth"],
|
|
1783
|
+
defaultKey: "auth",
|
|
1784
|
+
packageName: "@farm.js/workos",
|
|
1785
|
+
fileName: "workos",
|
|
1786
|
+
exportName: "workosIntegration",
|
|
1787
|
+
description: "WorkOS auth routes and protected route middleware",
|
|
1788
|
+
env: [
|
|
1789
|
+
"WORKOS_CLIENT_ID",
|
|
1790
|
+
"WORKOS_API_KEY",
|
|
1791
|
+
"WORKOS_COOKIE_PASSWORD"
|
|
1792
|
+
],
|
|
1793
|
+
ui: workosAuthUIFeature(),
|
|
1794
|
+
template: () => `import { workos } from "@farm.js/workos";
|
|
1795
|
+
|
|
1796
|
+
export const workosIntegration = workos({
|
|
1797
|
+
protectedRoutes: ["/dashboard(.*)"],
|
|
1798
|
+
log(event) {
|
|
1799
|
+
console.log("[workos]", event.phase, event.route?.path || "none");
|
|
1800
|
+
},
|
|
1801
|
+
});
|
|
1802
|
+
`
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
provider: "auth0",
|
|
1806
|
+
aliases: ["auth-auth0", "auth0-auth"],
|
|
1807
|
+
defaultKey: "auth",
|
|
1808
|
+
packageName: "@farm.js/auth0",
|
|
1809
|
+
fileName: "auth0",
|
|
1810
|
+
exportName: "auth0Integration",
|
|
1811
|
+
description: "Auth0 login, callback, logout, and profile routes",
|
|
1812
|
+
env: [
|
|
1813
|
+
"AUTH0_DOMAIN",
|
|
1814
|
+
"AUTH0_CLIENT_ID",
|
|
1815
|
+
"AUTH0_CLIENT_SECRET",
|
|
1816
|
+
"AUTH0_SECRET"
|
|
1817
|
+
],
|
|
1818
|
+
ui: auth0AuthUIFeature(),
|
|
1819
|
+
template: () => `import { auth0 } from "@farm.js/auth0";
|
|
1820
|
+
|
|
1821
|
+
export const auth0Integration = auth0({
|
|
1822
|
+
callbackUrl: \`\${process.env.APP_BASE_URL || "http://localhost:3000"}/auth/callback\`,
|
|
1823
|
+
protectedRoutes: ["/dashboard(.*)"],
|
|
1824
|
+
log(event) {
|
|
1825
|
+
console.log("[auth0]", event.phase, event.route?.path || "none");
|
|
1826
|
+
},
|
|
1827
|
+
});
|
|
1828
|
+
`
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
provider: "clerk",
|
|
1832
|
+
aliases: ["auth-clerk", "clerk-auth"],
|
|
1833
|
+
defaultKey: "auth",
|
|
1834
|
+
packageName: "@farm.js/clerk",
|
|
1835
|
+
fileName: "clerk",
|
|
1836
|
+
exportName: "clerkIntegration",
|
|
1837
|
+
description: "Clerk auth provider and protected route middleware",
|
|
1838
|
+
env: ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"],
|
|
1839
|
+
dependencies: { "@clerk/react": "^6.1.0" },
|
|
1840
|
+
ui: clerkAuthUIFeature(),
|
|
1841
|
+
template: () => `import { clerk } from "@farm.js/clerk";
|
|
1842
|
+
|
|
1843
|
+
export const clerkIntegration = clerk({
|
|
1844
|
+
signInUrl: "/sign-in",
|
|
1845
|
+
signUpUrl: "/sign-up",
|
|
1846
|
+
protectedRoutes: ["/dashboard(.*)"],
|
|
1847
|
+
log(event) {
|
|
1848
|
+
console.log("[clerk]", event.phase, event.route?.path || "none");
|
|
1849
|
+
},
|
|
1850
|
+
});
|
|
1851
|
+
`
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
provider: "resend",
|
|
1855
|
+
aliases: ["email", "resend-email"],
|
|
1856
|
+
defaultKey: "email",
|
|
1857
|
+
packageName: "@farm.js/email",
|
|
1858
|
+
fileName: "resend",
|
|
1859
|
+
exportName: "resendIntegration",
|
|
1860
|
+
description: "Resend email send, preview, schedule, and webhook routes",
|
|
1861
|
+
env: [
|
|
1862
|
+
"RESEND_API_KEY",
|
|
1863
|
+
"RESEND_FROM_EMAIL",
|
|
1864
|
+
"RESEND_WEBHOOK_SECRET"
|
|
1865
|
+
],
|
|
1866
|
+
ui: resendEmailUIFeature(),
|
|
1867
|
+
template: () => `import { createElement } from "react";
|
|
1868
|
+
import { resend, template } from "@farm.js/email";
|
|
1869
|
+
|
|
1870
|
+
function WelcomeEmail(props: { name: string }) {
|
|
1871
|
+
return createElement("div", null, \`Welcome \${props.name}\`);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
WelcomeEmail.PreviewProps = {
|
|
1875
|
+
name: "Ada",
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
export const emailTemplates = {
|
|
1879
|
+
welcome: template(WelcomeEmail, {
|
|
1880
|
+
subject: ({ name }) => \`Welcome, \${name}\`,
|
|
1881
|
+
previewText: () => "Welcome to the app",
|
|
1882
|
+
}),
|
|
1883
|
+
} as const;
|
|
1884
|
+
|
|
1885
|
+
export const resendIntegration = resend({
|
|
1886
|
+
apiKey: process.env.RESEND_API_KEY,
|
|
1887
|
+
defaults: {
|
|
1888
|
+
from: process.env.RESEND_FROM_EMAIL,
|
|
1889
|
+
replyTo: process.env.RESEND_REPLY_TO_EMAIL ?? process.env.RESEND_FROM_EMAIL,
|
|
1890
|
+
},
|
|
1891
|
+
templates: emailTemplates,
|
|
1892
|
+
webhooks: process.env.RESEND_WEBHOOK_SECRET
|
|
1893
|
+
? {
|
|
1894
|
+
secret: process.env.RESEND_WEBHOOK_SECRET,
|
|
1895
|
+
}
|
|
1896
|
+
: undefined,
|
|
1897
|
+
log(event) {
|
|
1898
|
+
console.log("[resend]", event.phase, event.route?.path || "none");
|
|
1899
|
+
},
|
|
1900
|
+
});
|
|
1901
|
+
`
|
|
1902
|
+
},
|
|
1903
|
+
{
|
|
1904
|
+
provider: "jobs-inngest",
|
|
1905
|
+
aliases: ["inngest", "jobs"],
|
|
1906
|
+
defaultKey: "jobs",
|
|
1907
|
+
packageName: "@farm.js/jobs",
|
|
1908
|
+
fileName: "jobs-inngest",
|
|
1909
|
+
exportName: "jobsIntegration",
|
|
1910
|
+
description: "Jobs integration backed by Inngest",
|
|
1911
|
+
env: [
|
|
1912
|
+
"INNGEST_APP_ID",
|
|
1913
|
+
"INNGEST_EVENT_KEY",
|
|
1914
|
+
"INNGEST_SIGNING_KEY"
|
|
1915
|
+
],
|
|
1916
|
+
notes: ["Add tasks to jobTasks before using the generated jobs API."],
|
|
1917
|
+
ui: jobsUIFeature("inngest"),
|
|
1918
|
+
template: () => `import { defineTasks, inngest, jobs } from "@farm.js/jobs";
|
|
1919
|
+
|
|
1920
|
+
export const jobTasks = defineTasks({});
|
|
1921
|
+
|
|
1922
|
+
export const jobsIntegration = jobs({
|
|
1923
|
+
runtime: inngest({
|
|
1924
|
+
appId: process.env.INNGEST_APP_ID,
|
|
1925
|
+
eventKey: process.env.INNGEST_EVENT_KEY,
|
|
1926
|
+
signingKey: process.env.INNGEST_SIGNING_KEY,
|
|
1927
|
+
}),
|
|
1928
|
+
tasks: jobTasks,
|
|
1929
|
+
log(event) {
|
|
1930
|
+
console.log("[jobs:inngest]", event.phase, event.route?.path || "none");
|
|
1931
|
+
},
|
|
1932
|
+
});
|
|
1933
|
+
`
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
provider: "jobs-trigger",
|
|
1937
|
+
aliases: [
|
|
1938
|
+
"trigger",
|
|
1939
|
+
"trigger-dev",
|
|
1940
|
+
"jobs-triggerdev"
|
|
1941
|
+
],
|
|
1942
|
+
defaultKey: "jobs",
|
|
1943
|
+
packageName: "@farm.js/jobs",
|
|
1944
|
+
fileName: "jobs-trigger",
|
|
1945
|
+
exportName: "jobsIntegration",
|
|
1946
|
+
description: "Jobs integration backed by Trigger.dev",
|
|
1947
|
+
env: [
|
|
1948
|
+
"TRIGGER_PROJECT_REF",
|
|
1949
|
+
"TRIGGER_SECRET_KEY",
|
|
1950
|
+
"TRIGGER_WEBHOOK_SECRET"
|
|
1951
|
+
],
|
|
1952
|
+
notes: ["Add tasks to jobTasks before using the generated jobs API."],
|
|
1953
|
+
ui: jobsUIFeature("trigger"),
|
|
1954
|
+
template: () => `import { defineTasks, jobs, trigger } from "@farm.js/jobs";
|
|
1955
|
+
|
|
1956
|
+
export const jobTasks = defineTasks({});
|
|
1957
|
+
|
|
1958
|
+
export const jobsIntegration = jobs({
|
|
1959
|
+
runtime: trigger({
|
|
1960
|
+
projectRef: process.env.TRIGGER_PROJECT_REF,
|
|
1961
|
+
apiKey: process.env.TRIGGER_SECRET_KEY,
|
|
1962
|
+
webhookSecret: process.env.TRIGGER_WEBHOOK_SECRET,
|
|
1963
|
+
}),
|
|
1964
|
+
tasks: jobTasks,
|
|
1965
|
+
log(event) {
|
|
1966
|
+
console.log("[jobs:trigger]", event.phase, event.route?.path || "none");
|
|
1967
|
+
},
|
|
1968
|
+
});
|
|
1969
|
+
`
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
provider: "polar",
|
|
1973
|
+
aliases: ["polar-billing", "billing-polar"],
|
|
1974
|
+
defaultKey: "billing",
|
|
1975
|
+
packageName: "@farm.js/polar",
|
|
1976
|
+
fileName: "polar",
|
|
1977
|
+
exportName: "polarIntegration",
|
|
1978
|
+
description: "Polar billing and checkout routes",
|
|
1979
|
+
env: [
|
|
1980
|
+
"POLAR_ACCESS_TOKEN",
|
|
1981
|
+
"POLAR_WEBHOOK_SECRET",
|
|
1982
|
+
"APP_BASE_URL"
|
|
1983
|
+
],
|
|
1984
|
+
notes: ["Replace resolveBillingOwner with your app user or organization lookup."],
|
|
1985
|
+
ui: polarBillingUIFeature(),
|
|
1986
|
+
template: () => `import type { FarmIntegrationHandlerContext } from "@farm.js/core";
|
|
1987
|
+
import { polar } from "@farm.js/polar";
|
|
1988
|
+
|
|
1989
|
+
function resolveBillingOwner(_context: FarmIntegrationHandlerContext): never {
|
|
1990
|
+
throw new Error("Configure Polar billing owner resolution for your app.");
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
export const polarIntegration = polar({
|
|
1994
|
+
accessToken: process.env.POLAR_ACCESS_TOKEN,
|
|
1995
|
+
server: (process.env.POLAR_SERVER as "sandbox" | "production" | undefined) ?? "sandbox",
|
|
1996
|
+
appBaseUrl: process.env.APP_BASE_URL,
|
|
1997
|
+
webhooks: process.env.POLAR_WEBHOOK_SECRET
|
|
1998
|
+
? {
|
|
1999
|
+
secret: process.env.POLAR_WEBHOOK_SECRET,
|
|
2000
|
+
}
|
|
2001
|
+
: undefined,
|
|
2002
|
+
billing: {
|
|
2003
|
+
resolveOwner: resolveBillingOwner,
|
|
2004
|
+
plans: {},
|
|
2005
|
+
products: {},
|
|
2006
|
+
},
|
|
2007
|
+
log(event) {
|
|
2008
|
+
console.log("[polar]", event.phase, event.route?.path || "none");
|
|
2009
|
+
},
|
|
2010
|
+
});
|
|
2011
|
+
`
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
provider: "autumn",
|
|
2015
|
+
aliases: ["autumn-billing", "billing-autumn"],
|
|
2016
|
+
defaultKey: "billing",
|
|
2017
|
+
packageName: "@farm.js/autumn",
|
|
2018
|
+
fileName: "autumn",
|
|
2019
|
+
exportName: "autumnIntegration",
|
|
2020
|
+
description: "Autumn billing and checkout routes",
|
|
2021
|
+
env: [
|
|
2022
|
+
"AUTUMN_SECRET_KEY",
|
|
2023
|
+
"AUTUMN_WEBHOOK_SECRET",
|
|
2024
|
+
"APP_BASE_URL"
|
|
2025
|
+
],
|
|
2026
|
+
notes: ["Replace resolveBillingOwner with your app user or organization lookup."],
|
|
2027
|
+
ui: autumnBillingUIFeature(),
|
|
2028
|
+
template: () => `import type { FarmIntegrationHandlerContext } from "@farm.js/core";
|
|
2029
|
+
import { autumn } from "@farm.js/autumn";
|
|
2030
|
+
|
|
2031
|
+
function resolveBillingOwner(_context: FarmIntegrationHandlerContext): never {
|
|
2032
|
+
throw new Error("Configure Autumn billing owner resolution for your app.");
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
export const autumnIntegration = autumn({
|
|
2036
|
+
secretKey: process.env.AUTUMN_SECRET_KEY,
|
|
2037
|
+
appBaseUrl: process.env.APP_BASE_URL,
|
|
2038
|
+
webhooks: process.env.AUTUMN_WEBHOOK_SECRET
|
|
2039
|
+
? {
|
|
2040
|
+
secret: process.env.AUTUMN_WEBHOOK_SECRET,
|
|
2041
|
+
}
|
|
2042
|
+
: undefined,
|
|
2043
|
+
billing: {
|
|
2044
|
+
resolveOwner: resolveBillingOwner,
|
|
2045
|
+
plans: {},
|
|
2046
|
+
products: {},
|
|
2047
|
+
},
|
|
2048
|
+
log(event) {
|
|
2049
|
+
console.log("[autumn]", event.phase, event.route?.path || "none");
|
|
2050
|
+
},
|
|
2051
|
+
});
|
|
2052
|
+
`
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
provider: "better-auth",
|
|
2056
|
+
aliases: ["betterauth", "auth-better-auth"],
|
|
2057
|
+
defaultKey: "auth",
|
|
2058
|
+
packageName: "@farm.js/better-auth",
|
|
2059
|
+
fileName: "better-auth",
|
|
2060
|
+
exportName: "betterAuthIntegration",
|
|
2061
|
+
description: "Better Auth route adapter",
|
|
2062
|
+
env: [
|
|
2063
|
+
"BETTER_AUTH_SECRET",
|
|
2064
|
+
"BETTER_AUTH_URL",
|
|
2065
|
+
"BETTER_AUTH_DATABASE_PATH"
|
|
2066
|
+
],
|
|
2067
|
+
dependencies: {
|
|
2068
|
+
"better-auth": "^1.5.5",
|
|
2069
|
+
"better-sqlite3": "^12.6.2"
|
|
2070
|
+
},
|
|
2071
|
+
devDependencies: { "@types/better-sqlite3": "^7.6.13" },
|
|
2072
|
+
notes: ["The generated SQLite database is intended for local development. Configure a persistent production database before deploying.", "Set BETTER_AUTH_SECRET to a high-entropy value of at least 32 characters."],
|
|
2073
|
+
setupFiles: [
|
|
2074
|
+
{
|
|
2075
|
+
path: "src/lib/auth.ts",
|
|
2076
|
+
source: () => `import Database from "better-sqlite3";
|
|
2077
|
+
import { betterAuth as createBetterAuth } from "better-auth";
|
|
2078
|
+
import { getMigrations } from "better-auth/db/migration";
|
|
2079
|
+
|
|
2080
|
+
const baseURL = process.env.BETTER_AUTH_URL || "http://localhost:3000";
|
|
2081
|
+
export const auth = createBetterAuth({
|
|
2082
|
+
database: new Database(process.env.BETTER_AUTH_DATABASE_PATH || "better-auth.sqlite"),
|
|
2083
|
+
secret: process.env.BETTER_AUTH_SECRET,
|
|
2084
|
+
baseURL,
|
|
2085
|
+
trustedOrigins: [baseURL],
|
|
2086
|
+
emailAndPassword: {
|
|
2087
|
+
enabled: true,
|
|
2088
|
+
},
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
const migrations = await getMigrations(auth.options);
|
|
2092
|
+
await migrations.runMigrations();
|
|
2093
|
+
`
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
path: ".env.example",
|
|
2097
|
+
merge: "lines",
|
|
2098
|
+
source: () => `BETTER_AUTH_SECRET=replace-with-at-least-32-random-characters
|
|
2099
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
2100
|
+
BETTER_AUTH_DATABASE_PATH=better-auth.sqlite
|
|
2101
|
+
`
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
path: ".gitignore",
|
|
2105
|
+
merge: "lines",
|
|
2106
|
+
source: () => `better-auth.sqlite
|
|
2107
|
+
better-auth.sqlite-shm
|
|
2108
|
+
better-auth.sqlite-wal
|
|
2109
|
+
`
|
|
2110
|
+
}
|
|
2111
|
+
],
|
|
2112
|
+
ui: betterAuthUIFeature(),
|
|
2113
|
+
template: () => `import { betterAuth } from "@farm.js/better-auth";
|
|
2114
|
+
import { auth } from "../auth.ts";
|
|
2115
|
+
|
|
2116
|
+
export const betterAuthIntegration = betterAuth({
|
|
2117
|
+
instance: auth,
|
|
2118
|
+
log(event) {
|
|
2119
|
+
console.log("[better-auth]", event.phase, event.route?.path || "none");
|
|
2120
|
+
},
|
|
2121
|
+
});
|
|
2122
|
+
`
|
|
2123
|
+
},
|
|
2124
|
+
{
|
|
2125
|
+
provider: "authjs",
|
|
2126
|
+
aliases: [
|
|
2127
|
+
"auth-js",
|
|
2128
|
+
"nextauth",
|
|
2129
|
+
"next-auth"
|
|
2130
|
+
],
|
|
2131
|
+
defaultKey: "auth",
|
|
2132
|
+
packageName: "@farm.js/authjs",
|
|
2133
|
+
fileName: "authjs",
|
|
2134
|
+
exportName: "authjsIntegration",
|
|
2135
|
+
description: "Auth.js route adapter",
|
|
2136
|
+
env: [
|
|
2137
|
+
"AUTH_SECRET",
|
|
2138
|
+
"AUTH_GITHUB_ID",
|
|
2139
|
+
"AUTH_GITHUB_SECRET"
|
|
2140
|
+
],
|
|
2141
|
+
dependencies: { "@auth/core": "0.34.3" },
|
|
2142
|
+
setupFiles: [{
|
|
2143
|
+
path: "src/lib/auth.ts",
|
|
2144
|
+
source: () => `import { Auth } from "@auth/core";
|
|
2145
|
+
import GitHub from "@auth/core/providers/github";
|
|
2146
|
+
|
|
2147
|
+
const config = {
|
|
2148
|
+
providers: [
|
|
2149
|
+
GitHub({
|
|
2150
|
+
clientId: process.env.AUTH_GITHUB_ID!,
|
|
2151
|
+
clientSecret: process.env.AUTH_GITHUB_SECRET!,
|
|
2152
|
+
}),
|
|
2153
|
+
],
|
|
2154
|
+
secret: process.env.AUTH_SECRET,
|
|
2155
|
+
trustHost: true,
|
|
2156
|
+
};
|
|
2157
|
+
|
|
2158
|
+
const handler = (request: Request) => Auth(request, config);
|
|
2159
|
+
|
|
2160
|
+
export const auth = {
|
|
2161
|
+
handlers: {
|
|
2162
|
+
GET: handler,
|
|
2163
|
+
POST: handler,
|
|
2164
|
+
},
|
|
2165
|
+
};
|
|
2166
|
+
`
|
|
2167
|
+
}],
|
|
2168
|
+
notes: ["The generated Auth.js instance uses GitHub OAuth through @auth/core; add or replace providers in src/lib/auth.ts."],
|
|
2169
|
+
ui: authjsUIFeature(),
|
|
2170
|
+
template: () => `import { authjs } from "@farm.js/authjs";
|
|
2171
|
+
import { auth } from "../auth.ts";
|
|
2172
|
+
|
|
2173
|
+
export const authjsIntegration = authjs({
|
|
2174
|
+
instance: auth,
|
|
2175
|
+
log(event) {
|
|
2176
|
+
console.log("[authjs]", event.phase, event.route?.path || "none");
|
|
2177
|
+
},
|
|
2178
|
+
});
|
|
2179
|
+
`
|
|
2180
|
+
},
|
|
2181
|
+
{
|
|
2182
|
+
provider: "unkey",
|
|
2183
|
+
aliases: [
|
|
2184
|
+
"api-keys",
|
|
2185
|
+
"apikeys",
|
|
2186
|
+
"keys",
|
|
2187
|
+
"unkey-api-keys"
|
|
2188
|
+
],
|
|
2189
|
+
defaultKey: "apiKeys",
|
|
2190
|
+
packageName: "@farm.js/unkey",
|
|
2191
|
+
fileName: "unkey",
|
|
2192
|
+
exportName: "unkeyIntegration",
|
|
2193
|
+
description: "Unkey API key creation, verification, and route protection",
|
|
2194
|
+
env: [
|
|
2195
|
+
"UNKEY_ROOT_KEY",
|
|
2196
|
+
"UNKEY_API_ID",
|
|
2197
|
+
"UNKEY_BASE_URL"
|
|
2198
|
+
],
|
|
2199
|
+
ui: unkeyApiKeysUIFeature(),
|
|
2200
|
+
template: () => `import { unkey } from "@farm.js/unkey";
|
|
2201
|
+
|
|
2202
|
+
export const unkeyIntegration = unkey({
|
|
2203
|
+
rootKey: process.env.UNKEY_ROOT_KEY,
|
|
2204
|
+
apiId: process.env.UNKEY_API_ID,
|
|
2205
|
+
baseUrl: process.env.UNKEY_BASE_URL,
|
|
2206
|
+
protectedRoutes: ["/api/protected(.*)"],
|
|
2207
|
+
log(event) {
|
|
2208
|
+
console.log("[unkey]", event.phase, event.route?.path || "none");
|
|
2209
|
+
},
|
|
2210
|
+
});
|
|
2211
|
+
`
|
|
2212
|
+
}
|
|
2213
|
+
];
|
|
2214
|
+
async function addFarmIntegration(options) {
|
|
2215
|
+
const root = node_path.default.resolve(options.root || process.cwd());
|
|
2216
|
+
const definition = resolveProvider(options.provider);
|
|
2217
|
+
if (definition.provider === "ai") return addAIRouteIntegration({
|
|
2218
|
+
root,
|
|
2219
|
+
definition,
|
|
2220
|
+
routeFile: options.routeFile,
|
|
2221
|
+
ui: options.ui,
|
|
2222
|
+
skipPackageJson: options.skipPackageJson,
|
|
2223
|
+
dryRun: options.dryRun,
|
|
2224
|
+
force: options.force
|
|
2225
|
+
});
|
|
2226
|
+
const key = options.key || definition.defaultKey;
|
|
2227
|
+
assertValidIntegrationKey(key);
|
|
2228
|
+
const registryFile = node_path.default.resolve(root, options.integrationsFile || node_path.default.join("src", "lib", "integrations.ts"));
|
|
2229
|
+
const integrationFile = node_path.default.join(node_path.default.dirname(registryFile), "integrations", `${definition.fileName}.ts`);
|
|
2230
|
+
const result = {
|
|
2231
|
+
provider: definition.provider,
|
|
2232
|
+
key,
|
|
2233
|
+
mode: "integration",
|
|
2234
|
+
integrationFile,
|
|
2235
|
+
registryFile,
|
|
2236
|
+
created: [],
|
|
2237
|
+
updated: [],
|
|
2238
|
+
skipped: [],
|
|
2239
|
+
env: [...definition.env],
|
|
2240
|
+
notes: [...definition.notes || []]
|
|
2241
|
+
};
|
|
2242
|
+
await writeIntegrationComponent({
|
|
2243
|
+
path: integrationFile,
|
|
2244
|
+
definition,
|
|
2245
|
+
force: options.force,
|
|
2246
|
+
dryRun: options.dryRun,
|
|
2247
|
+
result
|
|
2248
|
+
});
|
|
2249
|
+
await writeIntegrationSetupFiles({
|
|
2250
|
+
root,
|
|
2251
|
+
definition,
|
|
2252
|
+
force: options.force,
|
|
2253
|
+
dryRun: options.dryRun,
|
|
2254
|
+
result
|
|
2255
|
+
});
|
|
2256
|
+
await writeIntegrationRegistry({
|
|
2257
|
+
path: registryFile,
|
|
2258
|
+
integrationFile,
|
|
2259
|
+
definition,
|
|
2260
|
+
key,
|
|
2261
|
+
dryRun: options.dryRun,
|
|
2262
|
+
result
|
|
2263
|
+
});
|
|
2264
|
+
if (!options.skipPackageJson) await updatePackageJson$1({
|
|
2265
|
+
root,
|
|
2266
|
+
definition,
|
|
2267
|
+
dryRun: options.dryRun,
|
|
2268
|
+
result
|
|
2269
|
+
});
|
|
2270
|
+
if (!options.skipConfig) await updateFarmConfig({
|
|
2271
|
+
root,
|
|
2272
|
+
registryFile,
|
|
2273
|
+
dryRun: options.dryRun,
|
|
2274
|
+
result
|
|
2275
|
+
});
|
|
2276
|
+
if (options.ui) await installUIFeature({
|
|
2277
|
+
root,
|
|
2278
|
+
definition,
|
|
2279
|
+
key,
|
|
2280
|
+
dryRun: options.dryRun,
|
|
2281
|
+
force: options.force,
|
|
2282
|
+
skipPackageJson: options.skipPackageJson,
|
|
2283
|
+
result
|
|
2284
|
+
});
|
|
2285
|
+
return result;
|
|
2286
|
+
}
|
|
2287
|
+
async function addAIRouteIntegration(input) {
|
|
2288
|
+
const routeFile = node_path.default.resolve(input.root, input.routeFile || node_path.default.join("src", "app", "api", "chat", "route.ts"));
|
|
2289
|
+
const result = {
|
|
2290
|
+
provider: "ai",
|
|
2291
|
+
key: input.definition.defaultKey,
|
|
2292
|
+
mode: "route",
|
|
2293
|
+
integrationFile: routeFile,
|
|
2294
|
+
registryFile: "",
|
|
2295
|
+
routeFile,
|
|
2296
|
+
routePath: "/api/chat",
|
|
2297
|
+
created: [],
|
|
2298
|
+
updated: [],
|
|
2299
|
+
skipped: [],
|
|
2300
|
+
env: [...input.definition.env],
|
|
2301
|
+
notes: [...input.definition.notes || []]
|
|
2302
|
+
};
|
|
2303
|
+
await writeIntegrationComponent({
|
|
2304
|
+
path: routeFile,
|
|
2305
|
+
definition: input.definition,
|
|
2306
|
+
force: input.force,
|
|
2307
|
+
dryRun: input.dryRun,
|
|
2308
|
+
result
|
|
2309
|
+
});
|
|
2310
|
+
if (!input.skipPackageJson) await updatePackageJson$1({
|
|
2311
|
+
root: input.root,
|
|
2312
|
+
definition: input.definition,
|
|
2313
|
+
dryRun: input.dryRun,
|
|
2314
|
+
result
|
|
2315
|
+
});
|
|
2316
|
+
if (input.ui) await installUIFeature({
|
|
2317
|
+
root: input.root,
|
|
2318
|
+
definition: input.definition,
|
|
2319
|
+
key: input.definition.defaultKey,
|
|
2320
|
+
dryRun: input.dryRun,
|
|
2321
|
+
force: input.force,
|
|
2322
|
+
skipPackageJson: input.skipPackageJson,
|
|
2323
|
+
result
|
|
2324
|
+
});
|
|
2325
|
+
return result;
|
|
2326
|
+
}
|
|
2327
|
+
function resolveProvider(input) {
|
|
2328
|
+
const normalized = input.trim().toLowerCase();
|
|
2329
|
+
const match = PROVIDERS.find((provider) => provider.provider === normalized || provider.aliases.includes(normalized));
|
|
2330
|
+
if (!match) {
|
|
2331
|
+
const supported = PROVIDERS.map((provider) => provider.provider).join(", ");
|
|
2332
|
+
throw new Error(`Unknown integration "${input}". Supported integrations: ${supported}.`);
|
|
2333
|
+
}
|
|
2334
|
+
return match;
|
|
2335
|
+
}
|
|
2336
|
+
function assertValidIntegrationKey(key) {
|
|
2337
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key)) throw new Error(`Integration key "${key}" must be a valid JavaScript object property name.`);
|
|
2338
|
+
}
|
|
2339
|
+
async function writeIntegrationComponent(input) {
|
|
2340
|
+
const exists = (0, node_fs.existsSync)(input.path);
|
|
2341
|
+
if (exists && !input.force) {
|
|
2342
|
+
input.result.skipped.push(input.path);
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
if (!input.dryRun) {
|
|
2346
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(input.path), { recursive: true });
|
|
2347
|
+
await (0, node_fs_promises.writeFile)(input.path, input.definition.template(), "utf8");
|
|
2348
|
+
}
|
|
2349
|
+
if (exists) input.result.updated.push(input.path);
|
|
2350
|
+
else input.result.created.push(input.path);
|
|
2351
|
+
}
|
|
2352
|
+
async function writeIntegrationRegistry(input) {
|
|
2353
|
+
const importPath = toImportPath(node_path.default.relative(node_path.default.dirname(input.path), input.integrationFile));
|
|
2354
|
+
const importLine = `import { ${input.definition.exportName} } from "${importPath}";`;
|
|
2355
|
+
const propertyLine = ` ${input.key}: ${input.definition.exportName},`;
|
|
2356
|
+
let nextSource;
|
|
2357
|
+
const exists = (0, node_fs.existsSync)(input.path);
|
|
2358
|
+
if (exists) nextSource = ensureRegistryEntry(await (0, node_fs_promises.readFile)(input.path, "utf8"), {
|
|
2359
|
+
importLine,
|
|
2360
|
+
propertyLine,
|
|
2361
|
+
key: input.key,
|
|
2362
|
+
exportName: input.definition.exportName
|
|
2363
|
+
});
|
|
2364
|
+
else nextSource = `${importLine}
|
|
2365
|
+
|
|
2366
|
+
export const appIntegrations = {
|
|
2367
|
+
${propertyLine}
|
|
2368
|
+
} as const;
|
|
2369
|
+
|
|
2370
|
+
export type AppIntegrations = typeof appIntegrations;
|
|
2371
|
+
`;
|
|
2372
|
+
if (!input.dryRun) {
|
|
2373
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(input.path), { recursive: true });
|
|
2374
|
+
await (0, node_fs_promises.writeFile)(input.path, nextSource, "utf8");
|
|
2375
|
+
}
|
|
2376
|
+
input.result[exists ? "updated" : "created"].push(input.path);
|
|
2377
|
+
}
|
|
2378
|
+
function ensureRegistryEntry(source, input) {
|
|
2379
|
+
if (new RegExp(`(^|\\n)\\s*${escapeRegExp(input.key)}\\s*:`, "m").test(source)) {
|
|
2380
|
+
if (source.includes(`${input.key}: ${input.exportName}`)) return source.includes(input.importLine) ? source : `${input.importLine}\n${source}`;
|
|
2381
|
+
throw new Error(`Integration key "${input.key}" already exists in the app integrations registry. Pass --key to use a different key.`);
|
|
2382
|
+
}
|
|
2383
|
+
const sourceWithImport = source.includes(input.importLine) ? source : `${input.importLine}\n${source}`;
|
|
2384
|
+
const appIntegrationsPattern = /export\s+const\s+appIntegrations\s*=\s*\{([\s\S]*?)\}\s*as\s+const;/m;
|
|
2385
|
+
const match = sourceWithImport.match(appIntegrationsPattern);
|
|
2386
|
+
if (!match) return `${sourceWithImport.trimEnd()}
|
|
2387
|
+
|
|
2388
|
+
export const appIntegrations = {
|
|
2389
|
+
${input.propertyLine}
|
|
2390
|
+
} as const;
|
|
2391
|
+
|
|
2392
|
+
export type AppIntegrations = typeof appIntegrations;
|
|
2393
|
+
`;
|
|
2394
|
+
const body = match[1] || "";
|
|
2395
|
+
const nextBody = body.trim().length ? `${body.trimEnd()}\n${input.propertyLine}\n` : `\n${input.propertyLine}\n`;
|
|
2396
|
+
return sourceWithImport.replace(appIntegrationsPattern, () => {
|
|
2397
|
+
return `export const appIntegrations = {${nextBody}} as const;`;
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
async function writeIntegrationSetupFiles(input) {
|
|
2401
|
+
for (const file of input.definition.setupFiles || []) {
|
|
2402
|
+
const absolutePath = node_path.default.join(input.root, file.path);
|
|
2403
|
+
const exists = (0, node_fs.existsSync)(absolutePath);
|
|
2404
|
+
if (exists && !input.force) {
|
|
2405
|
+
if (file.merge === "lines") {
|
|
2406
|
+
const source = await (0, node_fs_promises.readFile)(absolutePath, "utf8");
|
|
2407
|
+
const additions = file.source().split(/\r?\n/).filter((line) => line && !source.split(/\r?\n/).includes(line));
|
|
2408
|
+
if (!additions.length) {
|
|
2409
|
+
input.result.skipped.push(absolutePath);
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(absolutePath, `${source.trimEnd()}\n${additions.join("\n")}\n`, "utf8");
|
|
2413
|
+
input.result.updated.push(absolutePath);
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
input.result.skipped.push(absolutePath);
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
if (!input.dryRun) {
|
|
2420
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(absolutePath), { recursive: true });
|
|
2421
|
+
await (0, node_fs_promises.writeFile)(absolutePath, file.source(), "utf8");
|
|
2422
|
+
}
|
|
2423
|
+
input.result[exists ? "updated" : "created"].push(absolutePath);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
async function updatePackageJson$1(input) {
|
|
2427
|
+
const packageJsonPath = node_path.default.join(input.root, "package.json");
|
|
2428
|
+
if (!(0, node_fs.existsSync)(packageJsonPath)) {
|
|
2429
|
+
input.result.skipped.push(packageJsonPath);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const source = await (0, node_fs_promises.readFile)(packageJsonPath, "utf8");
|
|
2433
|
+
const manifest = JSON.parse(source);
|
|
2434
|
+
const dependencies = missingDependencies(manifest, input.definition.dependencies);
|
|
2435
|
+
const devDependencies = missingDependencies(manifest, input.definition.devDependencies);
|
|
2436
|
+
manifest.dependencies = {
|
|
2437
|
+
...manifest.dependencies,
|
|
2438
|
+
...hasPackageDependency(manifest, input.definition.packageName) ? {} : { [input.definition.packageName]: getFarmIntegrationVersion(manifest) },
|
|
2439
|
+
...dependencies
|
|
2440
|
+
};
|
|
2441
|
+
if (Object.keys(devDependencies).length) manifest.devDependencies = {
|
|
2442
|
+
...manifest.devDependencies,
|
|
2443
|
+
...devDependencies
|
|
2444
|
+
};
|
|
2445
|
+
const nextSource = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
2446
|
+
if (source === nextSource) {
|
|
2447
|
+
input.result.packageJson = packageJsonPath;
|
|
2448
|
+
input.result.skipped.push(packageJsonPath);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(packageJsonPath, nextSource, "utf8");
|
|
2452
|
+
input.result.packageJson = packageJsonPath;
|
|
2453
|
+
input.result.updated.push(packageJsonPath);
|
|
2454
|
+
}
|
|
2455
|
+
function missingDependencies(manifest, dependencies) {
|
|
2456
|
+
return Object.fromEntries(Object.entries(dependencies || {}).filter(([name]) => !hasPackageDependency(manifest, name)));
|
|
2457
|
+
}
|
|
2458
|
+
async function updateFarmConfig(input) {
|
|
2459
|
+
const configFile = findFarmConfig(input.root);
|
|
2460
|
+
if (!configFile) {
|
|
2461
|
+
const newConfigFile = node_path.default.join(input.root, "farm.config.ts");
|
|
2462
|
+
const source = `import { defineConfig } from "@farm.js/core";
|
|
2463
|
+
import { appIntegrations } from "${toImportPath(node_path.default.relative(input.root, input.registryFile))}";
|
|
2464
|
+
|
|
2465
|
+
export default defineConfig({
|
|
2466
|
+
integrations: appIntegrations,
|
|
2467
|
+
});
|
|
2468
|
+
`;
|
|
2469
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(newConfigFile, source, "utf8");
|
|
2470
|
+
input.result.configFile = newConfigFile;
|
|
2471
|
+
input.result.created.push(newConfigFile);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
input.result.configFile = configFile;
|
|
2475
|
+
const source = await (0, node_fs_promises.readFile)(configFile, "utf8");
|
|
2476
|
+
if (/\bintegrations\s*:/.test(source)) {
|
|
2477
|
+
input.result.skipped.push(configFile);
|
|
2478
|
+
input.result.notes.push(`farm.config already has an integrations field. Confirm it includes appIntegrations from ${node_path.default.relative(input.root, input.registryFile)}.`);
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
const importLine = `import { appIntegrations } from "${toImportPath(node_path.default.relative(node_path.default.dirname(configFile), input.registryFile))}";`;
|
|
2482
|
+
const sourceWithImport = source.includes(importLine) ? source : `${importLine}\n${source}`;
|
|
2483
|
+
const nextSource = insertIntegrationsConfig(sourceWithImport);
|
|
2484
|
+
if (nextSource === sourceWithImport) {
|
|
2485
|
+
input.result.skipped.push(configFile);
|
|
2486
|
+
input.result.notes.push(`Could not safely update ${node_path.default.relative(input.root, configFile)}. Add integrations: appIntegrations manually.`);
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
if (!input.dryRun) await (0, node_fs_promises.writeFile)(configFile, nextSource, "utf8");
|
|
2490
|
+
input.result.updated.push(configFile);
|
|
2491
|
+
}
|
|
2492
|
+
function insertIntegrationsConfig(source) {
|
|
2493
|
+
const defineConfigCall = /\bdefine(?:Farm)?Config\s*\(\s*\{/;
|
|
2494
|
+
if (defineConfigCall.test(source)) return source.replace(defineConfigCall, (match) => {
|
|
2495
|
+
return `${match}\n integrations: appIntegrations,`;
|
|
2496
|
+
});
|
|
2497
|
+
if (/export\s+default\s+\{/.test(source)) return source.replace(/export\s+default\s+\{/, (match) => {
|
|
2498
|
+
return `${match}\n integrations: appIntegrations,`;
|
|
2499
|
+
});
|
|
2500
|
+
return source;
|
|
2501
|
+
}
|
|
2502
|
+
function findFarmConfig(root) {
|
|
2503
|
+
for (const candidate of [
|
|
2504
|
+
"farm.config.ts",
|
|
2505
|
+
"farm.config.mts",
|
|
2506
|
+
"farm.config.js",
|
|
2507
|
+
"farm.config.mjs",
|
|
2508
|
+
"config.ts",
|
|
2509
|
+
"config.mts",
|
|
2510
|
+
"config.js",
|
|
2511
|
+
"config.mjs"
|
|
2512
|
+
]) {
|
|
2513
|
+
const absolutePath = node_path.default.join(root, candidate);
|
|
2514
|
+
if ((0, node_fs.existsSync)(absolutePath)) return absolutePath;
|
|
2515
|
+
}
|
|
2516
|
+
return null;
|
|
2517
|
+
}
|
|
2518
|
+
function hasPackageDependency(manifest, dependency) {
|
|
2519
|
+
return dependency in (manifest.dependencies || {}) || dependency in (manifest.devDependencies || {}) || dependency in (manifest.peerDependencies || {}) || dependency in (manifest.optionalDependencies || {});
|
|
2520
|
+
}
|
|
2521
|
+
function getFarmIntegrationVersion(manifest) {
|
|
2522
|
+
const farmCoreVersion = manifest.dependencies?.["@farm.js/core"] ?? manifest.devDependencies?.["@farm.js/core"] ?? manifest.peerDependencies?.["@farm.js/core"] ?? manifest.optionalDependencies?.["@farm.js/core"];
|
|
2523
|
+
if (!farmCoreVersion) return "latest";
|
|
2524
|
+
return farmCoreVersion.startsWith("workspace:") ? "workspace:*" : farmCoreVersion;
|
|
2525
|
+
}
|
|
2526
|
+
function toImportPath(relativePath) {
|
|
2527
|
+
const normalized = relativePath.split(node_path.default.sep).join("/");
|
|
2528
|
+
return (normalized.startsWith(".") ? normalized : `./${normalized}`).replace(/\.tsx?$/, ".ts");
|
|
2529
|
+
}
|
|
2530
|
+
function escapeRegExp(input) {
|
|
2531
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2532
|
+
}
|
|
2533
|
+
//#endregion
|
|
2534
|
+
//#region ../farm-cli/src/telemetry-contract.ts
|
|
2535
|
+
const FARM_CREATE_APP_TELEMETRY_COMMANDS = ["create", "list-templates"];
|
|
2536
|
+
const FARM_TELEMETRY_TEMPLATES = [
|
|
2537
|
+
"basic",
|
|
2538
|
+
"react-compiler",
|
|
2539
|
+
"auth",
|
|
2540
|
+
"better-auth",
|
|
2541
|
+
"ai",
|
|
2542
|
+
"auth0",
|
|
2543
|
+
"authjs",
|
|
2544
|
+
"autumn",
|
|
2545
|
+
"clerk",
|
|
2546
|
+
"jobs-inngest",
|
|
2547
|
+
"jobs-trigger",
|
|
2548
|
+
"polar",
|
|
2549
|
+
"resend",
|
|
2550
|
+
"stripe",
|
|
2551
|
+
"supabase",
|
|
2552
|
+
"unkey",
|
|
2553
|
+
"workos"
|
|
2554
|
+
];
|
|
2555
|
+
const FARM_TELEMETRY_RENDERERS = [
|
|
2556
|
+
"react",
|
|
2557
|
+
"preact",
|
|
2558
|
+
"solid",
|
|
2559
|
+
"vue",
|
|
2560
|
+
"svelte"
|
|
2561
|
+
];
|
|
2562
|
+
const FARM_TELEMETRY_PACKAGE_MANAGERS = [
|
|
2563
|
+
"npm",
|
|
2564
|
+
"pnpm",
|
|
2565
|
+
"yarn",
|
|
2566
|
+
"bun"
|
|
2567
|
+
];
|
|
2568
|
+
//#endregion
|
|
2569
|
+
//#region ../farm-cli/src/telemetry.ts
|
|
2570
|
+
const TELEMETRY_SCHEMA_VERSION = 1;
|
|
2571
|
+
const DEFAULT_TELEMETRY_ENDPOINT = "https://farmjs.dev/api/telemetry/v1/events";
|
|
2572
|
+
const TELEMETRY_NOTICE_URL = "https://farmjs.dev/docs/telemetry";
|
|
2573
|
+
const REQUEST_TIMEOUT_MS = 3e3;
|
|
2574
|
+
const RETRY_DELAYS_MS = [250, 750];
|
|
2575
|
+
const pendingDeliveries = /* @__PURE__ */ new Set();
|
|
2576
|
+
const retryTimers = /* @__PURE__ */ new Set();
|
|
2577
|
+
function defaultConfig() {
|
|
2578
|
+
return {
|
|
2579
|
+
version: TELEMETRY_SCHEMA_VERSION,
|
|
2580
|
+
enabled: true,
|
|
2581
|
+
noticeShown: false
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
function configDirectory() {
|
|
2585
|
+
if (process.env.FARM_TELEMETRY_CONFIG_DIR) return node_path.default.resolve(process.env.FARM_TELEMETRY_CONFIG_DIR);
|
|
2586
|
+
if (process.platform === "win32") return node_path.default.join(process.env.APPDATA || node_path.default.join(node_os.default.homedir(), "AppData", "Roaming"), "farmjs");
|
|
2587
|
+
if (process.platform === "darwin") return node_path.default.join(node_os.default.homedir(), "Library", "Application Support", "farmjs");
|
|
2588
|
+
return node_path.default.join(process.env.XDG_CONFIG_HOME || node_path.default.join(node_os.default.homedir(), ".config"), "farmjs");
|
|
2589
|
+
}
|
|
2590
|
+
function getFarmTelemetryConfigFile() {
|
|
2591
|
+
return node_path.default.join(configDirectory(), "telemetry.json");
|
|
2592
|
+
}
|
|
2593
|
+
async function readConfig() {
|
|
2594
|
+
try {
|
|
2595
|
+
const parsed = JSON.parse(await (0, node_fs_promises.readFile)(getFarmTelemetryConfigFile(), "utf8"));
|
|
2596
|
+
if (parsed.version !== TELEMETRY_SCHEMA_VERSION) return {
|
|
2597
|
+
config: defaultConfig(),
|
|
2598
|
+
stored: false
|
|
2599
|
+
};
|
|
2600
|
+
return {
|
|
2601
|
+
config: {
|
|
2602
|
+
version: TELEMETRY_SCHEMA_VERSION,
|
|
2603
|
+
enabled: parsed.enabled === true,
|
|
2604
|
+
noticeShown: parsed.noticeShown === true,
|
|
2605
|
+
anonymousId: isUuid(parsed.anonymousId) ? parsed.anonymousId : void 0
|
|
2606
|
+
},
|
|
2607
|
+
stored: true
|
|
2608
|
+
};
|
|
2609
|
+
} catch {
|
|
2610
|
+
return {
|
|
2611
|
+
config: defaultConfig(),
|
|
2612
|
+
stored: false
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
async function writeConfig(config) {
|
|
2617
|
+
const file = getFarmTelemetryConfigFile();
|
|
2618
|
+
const directory = node_path.default.dirname(file);
|
|
2619
|
+
const temporaryFile = `${file}.${process.pid}.${(0, node_crypto.randomUUID)()}.tmp`;
|
|
2620
|
+
try {
|
|
2621
|
+
await (0, node_fs_promises.mkdir)(directory, {
|
|
2622
|
+
recursive: true,
|
|
2623
|
+
mode: 448
|
|
2624
|
+
});
|
|
2625
|
+
await (0, node_fs_promises.writeFile)(temporaryFile, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
2626
|
+
await (0, node_fs_promises.rename)(temporaryFile, file);
|
|
2627
|
+
await (0, node_fs_promises.chmod)(file, 384).catch(() => void 0);
|
|
2628
|
+
} catch {
|
|
2629
|
+
await (0, node_fs_promises.unlink)(temporaryFile).catch(() => void 0);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
function isUuid(value) {
|
|
2633
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
2634
|
+
}
|
|
2635
|
+
function isTrue(value) {
|
|
2636
|
+
return value !== void 0 && [
|
|
2637
|
+
"1",
|
|
2638
|
+
"true",
|
|
2639
|
+
"yes",
|
|
2640
|
+
"on"
|
|
2641
|
+
].includes(value.toLowerCase());
|
|
2642
|
+
}
|
|
2643
|
+
function isFalse(value) {
|
|
2644
|
+
return value !== void 0 && [
|
|
2645
|
+
"0",
|
|
2646
|
+
"false",
|
|
2647
|
+
"no",
|
|
2648
|
+
"off"
|
|
2649
|
+
].includes(value.toLowerCase());
|
|
2650
|
+
}
|
|
2651
|
+
function environmentDecision() {
|
|
2652
|
+
if (process.env.DO_NOT_TRACK !== void 0 && !isFalse(process.env.DO_NOT_TRACK)) return {
|
|
2653
|
+
enabled: false,
|
|
2654
|
+
reason: "DO_NOT_TRACK is set"
|
|
2655
|
+
};
|
|
2656
|
+
if (isTrue(process.env.FARM_TELEMETRY_DISABLED)) return {
|
|
2657
|
+
enabled: false,
|
|
2658
|
+
reason: "FARM_TELEMETRY_DISABLED is set"
|
|
2659
|
+
};
|
|
2660
|
+
if (isTrue(process.env.FARM_TELEMETRY)) return { enabled: true };
|
|
2661
|
+
if (isFalse(process.env.FARM_TELEMETRY)) return {
|
|
2662
|
+
enabled: false,
|
|
2663
|
+
reason: "FARM_TELEMETRY disables collection"
|
|
2664
|
+
};
|
|
2665
|
+
return {};
|
|
2666
|
+
}
|
|
2667
|
+
function isContinuousIntegration() {
|
|
2668
|
+
return isTrue(process.env.CI) || isTrue(process.env.GITHUB_ACTIONS) || isTrue(process.env.BUILDKITE) || isTrue(process.env.CIRCLECI);
|
|
2669
|
+
}
|
|
2670
|
+
function isInteractive() {
|
|
2671
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2672
|
+
}
|
|
2673
|
+
function getEndpoint() {
|
|
2674
|
+
const candidate = process.env.FARM_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_ENDPOINT;
|
|
2675
|
+
try {
|
|
2676
|
+
const url = new URL(candidate);
|
|
2677
|
+
const isLocal = [
|
|
2678
|
+
"localhost",
|
|
2679
|
+
"127.0.0.1",
|
|
2680
|
+
"::1"
|
|
2681
|
+
].includes(url.hostname);
|
|
2682
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal)) return DEFAULT_TELEMETRY_ENDPOINT;
|
|
2683
|
+
return url.toString();
|
|
2684
|
+
} catch {
|
|
2685
|
+
return DEFAULT_TELEMETRY_ENDPOINT;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
async function resolveState() {
|
|
2689
|
+
const { config, stored } = await readConfig();
|
|
2690
|
+
const environment = environmentDecision();
|
|
2691
|
+
const enabled = environment.enabled ?? config.enabled;
|
|
2692
|
+
const source = environment.enabled !== void 0 ? "environment" : stored ? "configuration" : "default";
|
|
2693
|
+
if (!enabled) return {
|
|
2694
|
+
config,
|
|
2695
|
+
enabled,
|
|
2696
|
+
active: false,
|
|
2697
|
+
source,
|
|
2698
|
+
reason: environment.reason
|
|
2699
|
+
};
|
|
2700
|
+
if (environment.enabled === true) return {
|
|
2701
|
+
config,
|
|
2702
|
+
enabled,
|
|
2703
|
+
active: true,
|
|
2704
|
+
source
|
|
2705
|
+
};
|
|
2706
|
+
if (process.env.NODE_ENV === "test") return {
|
|
2707
|
+
config,
|
|
2708
|
+
enabled,
|
|
2709
|
+
active: false,
|
|
2710
|
+
source,
|
|
2711
|
+
reason: "test environments are skipped"
|
|
2712
|
+
};
|
|
2713
|
+
if (isContinuousIntegration()) return {
|
|
2714
|
+
config,
|
|
2715
|
+
enabled,
|
|
2716
|
+
active: false,
|
|
2717
|
+
source,
|
|
2718
|
+
reason: "CI environments are skipped"
|
|
2719
|
+
};
|
|
2720
|
+
if (!isInteractive()) return {
|
|
2721
|
+
config,
|
|
2722
|
+
enabled,
|
|
2723
|
+
active: false,
|
|
2724
|
+
source,
|
|
2725
|
+
reason: "non-interactive commands are skipped"
|
|
2726
|
+
};
|
|
2727
|
+
return {
|
|
2728
|
+
config,
|
|
2729
|
+
enabled,
|
|
2730
|
+
active: true,
|
|
2731
|
+
source
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
async function showFarmTelemetryNotice() {
|
|
2735
|
+
if (!isInteractive() || isContinuousIntegration() || process.env.NODE_ENV === "test") return;
|
|
2736
|
+
if (environmentDecision().enabled !== void 0) return;
|
|
2737
|
+
const { config } = await readConfig();
|
|
2738
|
+
if (config.noticeShown) return;
|
|
2739
|
+
process.stderr.write(`Farm.js collects anonymous CLI telemetry by default. Run "farm telemetry disable" to opt out.\nLearn more: ${TELEMETRY_NOTICE_URL}\n`);
|
|
2740
|
+
await writeConfig({
|
|
2741
|
+
...config,
|
|
2742
|
+
noticeShown: true
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
function trackFarmCreateAppCommand(input) {
|
|
2746
|
+
return schedule(async () => {
|
|
2747
|
+
await showFarmTelemetryNotice();
|
|
2748
|
+
const command = allowlisted(input.command, FARM_CREATE_APP_TELEMETRY_COMMANDS);
|
|
2749
|
+
if (!command) return;
|
|
2750
|
+
await track({
|
|
2751
|
+
eventType: "command_invoked",
|
|
2752
|
+
source: "create-app",
|
|
2753
|
+
packageName: "@farm.js/create-app",
|
|
2754
|
+
packageVersion: sanitizeVersion(input.packageVersion),
|
|
2755
|
+
command
|
|
2756
|
+
});
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
function trackFarmProjectCreated(input) {
|
|
2760
|
+
return schedule(async () => {
|
|
2761
|
+
const template = allowlisted(input.template, FARM_TELEMETRY_TEMPLATES);
|
|
2762
|
+
const renderer = allowlisted(input.renderer, FARM_TELEMETRY_RENDERERS);
|
|
2763
|
+
const packageManager = allowlisted(input.packageManager, FARM_TELEMETRY_PACKAGE_MANAGERS);
|
|
2764
|
+
await track({
|
|
2765
|
+
eventType: "project_created",
|
|
2766
|
+
source: "create-app",
|
|
2767
|
+
packageName: "@farm.js/create-app",
|
|
2768
|
+
packageVersion: sanitizeVersion(input.packageVersion),
|
|
2769
|
+
...template ? { template } : {},
|
|
2770
|
+
...renderer ? { renderer } : {},
|
|
2771
|
+
...packageManager ? { packageManager } : {},
|
|
2772
|
+
...typeof input.typescript === "boolean" ? { typescript: input.typescript } : {},
|
|
2773
|
+
...typeof input.installedDependencies === "boolean" ? { installedDependencies: input.installedDependencies } : {}
|
|
2774
|
+
});
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
function schedule(delivery) {
|
|
2778
|
+
const pending = Promise.resolve().then(delivery).catch(() => {});
|
|
2779
|
+
pendingDeliveries.add(pending);
|
|
2780
|
+
pending.finally(() => pendingDeliveries.delete(pending));
|
|
2781
|
+
return Promise.resolve();
|
|
2782
|
+
}
|
|
2783
|
+
async function track(event) {
|
|
2784
|
+
try {
|
|
2785
|
+
const state = await resolveState();
|
|
2786
|
+
if (!state.active) return;
|
|
2787
|
+
const anonymousId = state.config.anonymousId || (0, node_crypto.randomUUID)();
|
|
2788
|
+
if (!state.config.anonymousId) await writeConfig({
|
|
2789
|
+
...state.config,
|
|
2790
|
+
anonymousId
|
|
2791
|
+
});
|
|
2792
|
+
await send({
|
|
2793
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
2794
|
+
eventId: (0, node_crypto.randomUUID)(),
|
|
2795
|
+
anonymousId,
|
|
2796
|
+
nodeMajor: Number.parseInt(process.versions.node.split(".")[0] || "0", 10),
|
|
2797
|
+
platform: normalizePlatform(process.platform),
|
|
2798
|
+
architecture: normalizeArchitecture(process.arch),
|
|
2799
|
+
...event
|
|
2800
|
+
});
|
|
2801
|
+
} catch {}
|
|
2802
|
+
}
|
|
2803
|
+
async function send(payload) {
|
|
2804
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
2805
|
+
const result = await sendOnce(payload);
|
|
2806
|
+
if (result === "delivered" || result === "rejected") return;
|
|
2807
|
+
const delay = RETRY_DELAYS_MS[attempt];
|
|
2808
|
+
if (delay !== void 0) await wait(delay);
|
|
2809
|
+
}
|
|
2810
|
+
debug("delivery failed after retries");
|
|
2811
|
+
}
|
|
2812
|
+
async function sendOnce(payload) {
|
|
2813
|
+
let endpoint;
|
|
2814
|
+
try {
|
|
2815
|
+
endpoint = new URL(getEndpoint());
|
|
2816
|
+
} catch {
|
|
2817
|
+
debug("invalid endpoint URL");
|
|
2818
|
+
return "rejected";
|
|
2819
|
+
}
|
|
2820
|
+
const requestTransport = endpoint.protocol === "http:" ? node_http.request : node_https.request;
|
|
2821
|
+
if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:") {
|
|
2822
|
+
debug(`unsupported endpoint protocol ${endpoint.protocol}`);
|
|
2823
|
+
return "rejected";
|
|
2824
|
+
}
|
|
2825
|
+
const body = JSON.stringify(payload);
|
|
2826
|
+
return new Promise((resolve) => {
|
|
2827
|
+
let settled = false;
|
|
2828
|
+
const finish = (result) => {
|
|
2829
|
+
if (settled) return;
|
|
2830
|
+
settled = true;
|
|
2831
|
+
clearTimeout(timeout);
|
|
2832
|
+
resolve(result);
|
|
2833
|
+
};
|
|
2834
|
+
const request = requestTransport(endpoint, {
|
|
2835
|
+
method: "POST",
|
|
2836
|
+
headers: {
|
|
2837
|
+
"content-type": "application/json",
|
|
2838
|
+
"content-length": Buffer.byteLength(body)
|
|
2839
|
+
}
|
|
2840
|
+
}, (response) => {
|
|
2841
|
+
response.on("error", () => {});
|
|
2842
|
+
response.resume();
|
|
2843
|
+
const status = response.statusCode ?? 0;
|
|
2844
|
+
if (status >= 200 && status < 300) return finish("delivered");
|
|
2845
|
+
if (status === 408 || status === 425 || status === 429) {
|
|
2846
|
+
debug(`temporary HTTP ${status}; retrying`);
|
|
2847
|
+
return finish("retry");
|
|
2848
|
+
}
|
|
2849
|
+
if (status >= 500) {
|
|
2850
|
+
debug(`server HTTP ${status}; retrying`);
|
|
2851
|
+
return finish("retry");
|
|
2852
|
+
}
|
|
2853
|
+
debug(`event rejected with HTTP ${status}`);
|
|
2854
|
+
return finish("rejected");
|
|
2855
|
+
});
|
|
2856
|
+
const timeout = setTimeout(() => {
|
|
2857
|
+
request.destroy();
|
|
2858
|
+
debug("network request timed out; retrying");
|
|
2859
|
+
finish("retry");
|
|
2860
|
+
}, REQUEST_TIMEOUT_MS);
|
|
2861
|
+
timeout.unref?.();
|
|
2862
|
+
request.once("socket", (socket) => socket.unref());
|
|
2863
|
+
request.once("error", () => {
|
|
2864
|
+
debug("network request failed; retrying");
|
|
2865
|
+
finish("retry");
|
|
2866
|
+
});
|
|
2867
|
+
request.end(body);
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
function wait(delay) {
|
|
2871
|
+
return new Promise((resolve) => {
|
|
2872
|
+
const timeout = setTimeout(() => {
|
|
2873
|
+
retryTimers.delete(timeout);
|
|
2874
|
+
resolve();
|
|
2875
|
+
}, delay);
|
|
2876
|
+
retryTimers.add(timeout);
|
|
2877
|
+
timeout.unref?.();
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
function debug(message) {
|
|
2881
|
+
if (!isTrue(process.env.FARM_TELEMETRY_DEBUG)) return;
|
|
2882
|
+
process.stderr.write(`[farm.telemetry] ${message}\n`);
|
|
2883
|
+
}
|
|
2884
|
+
function sanitizeVersion(value) {
|
|
2885
|
+
return /^[0-9A-Za-z.+_-]{1,64}$/.test(value) ? value : "unknown";
|
|
2886
|
+
}
|
|
2887
|
+
function allowlisted(value, values) {
|
|
2888
|
+
return value && values.includes(value) ? value : void 0;
|
|
2889
|
+
}
|
|
2890
|
+
function normalizePlatform(value) {
|
|
2891
|
+
if (value === "darwin" || value === "linux") return value;
|
|
2892
|
+
if (value === "win32") return "windows";
|
|
2893
|
+
return "other";
|
|
2894
|
+
}
|
|
2895
|
+
function normalizeArchitecture(value) {
|
|
2896
|
+
return value === "arm64" || value === "x64" ? value : "other";
|
|
2897
|
+
}
|
|
2898
|
+
//#endregion
|
|
11
2899
|
//#region src/index.ts
|
|
2900
|
+
const templateDetails = {
|
|
2901
|
+
basic: {
|
|
2902
|
+
title: "Basic starter",
|
|
2903
|
+
description: "A minimal Farm.js app with built-in Tailwind support",
|
|
2904
|
+
instructions: ["Tailwind is enabled by default. You only need postcss config for custom plugins."]
|
|
2905
|
+
},
|
|
2906
|
+
"react-compiler": {
|
|
2907
|
+
title: "React Compiler starter (experimental)",
|
|
2908
|
+
description: "React AOT compiler with a focused live comparison",
|
|
2909
|
+
instructions: ["The React AOT compiler is experimental; unsupported components safely stay on React.", "Run pnpm experiment after installing Playwright Chromium to verify the compiled update path."]
|
|
2910
|
+
},
|
|
2911
|
+
auth: {
|
|
2912
|
+
title: "Auth starter",
|
|
2913
|
+
description: "Farm.js-native auth with local SQLite, secure sessions, and protected routes",
|
|
2914
|
+
instructions: ["FARMJS Auth uses local SQLite automatically; no auth environment variables are needed locally.", "For production, set FARM_AUTH_URL, FARM_AUTH_SECRET, and DATABASE_URL, then run the auth:migrate script."]
|
|
2915
|
+
},
|
|
2916
|
+
"better-auth": {
|
|
2917
|
+
title: "Better Auth starter",
|
|
2918
|
+
description: "Better Auth with Postgres, secure sessions, and protected routes",
|
|
2919
|
+
instructions: ["Before starting, copy .env.example to .env.local and set DATABASE_URL and BETTER_AUTH_SECRET.", "Better Auth migrations run automatically when the auth instance starts."]
|
|
2920
|
+
},
|
|
2921
|
+
ai: integrationTemplate({
|
|
2922
|
+
title: "AI starter",
|
|
2923
|
+
description: "AI SDK chat route with a ready-to-use chat interface",
|
|
2924
|
+
provider: "ai",
|
|
2925
|
+
label: "AI",
|
|
2926
|
+
route: "/integrations/ai",
|
|
2927
|
+
docsPath: "/docs/integrations"
|
|
2928
|
+
}),
|
|
2929
|
+
auth0: integrationTemplate({
|
|
2930
|
+
title: "Auth0 starter",
|
|
2931
|
+
description: "Auth0 login, sessions, protected routes, and account controls",
|
|
2932
|
+
provider: "auth0",
|
|
2933
|
+
label: "Auth0",
|
|
2934
|
+
route: "/integrations/auth0",
|
|
2935
|
+
docsPath: "/docs/integrations/auth/auth0"
|
|
2936
|
+
}),
|
|
2937
|
+
authjs: integrationTemplate({
|
|
2938
|
+
title: "Auth.js starter",
|
|
2939
|
+
description: "Auth.js with GitHub OAuth and Farm-owned route mounting",
|
|
2940
|
+
provider: "authjs",
|
|
2941
|
+
label: "Auth.js",
|
|
2942
|
+
route: "/integrations/authjs",
|
|
2943
|
+
docsPath: "/docs/integrations/auth/authjs"
|
|
2944
|
+
}),
|
|
2945
|
+
autumn: integrationTemplate({
|
|
2946
|
+
title: "Autumn starter",
|
|
2947
|
+
description: "Autumn products, checkout, billing state, and customer portal",
|
|
2948
|
+
provider: "autumn",
|
|
2949
|
+
label: "Autumn",
|
|
2950
|
+
route: "/integrations/autumn",
|
|
2951
|
+
docsPath: "/docs/integrations/autumn"
|
|
2952
|
+
}),
|
|
2953
|
+
clerk: integrationTemplate({
|
|
2954
|
+
title: "Clerk starter",
|
|
2955
|
+
description: "Clerk authentication, account entry points, and protected routes",
|
|
2956
|
+
provider: "clerk",
|
|
2957
|
+
label: "Clerk",
|
|
2958
|
+
route: "/integrations/clerk",
|
|
2959
|
+
docsPath: "/docs/integrations/auth/clerk"
|
|
2960
|
+
}),
|
|
2961
|
+
"jobs-inngest": integrationTemplate({
|
|
2962
|
+
title: "Inngest starter",
|
|
2963
|
+
description: "Typed background jobs backed by Inngest",
|
|
2964
|
+
provider: "jobs-inngest",
|
|
2965
|
+
label: "Inngest",
|
|
2966
|
+
route: "/integrations/jobs-inngest",
|
|
2967
|
+
docsPath: "/docs/integrations/inngest"
|
|
2968
|
+
}),
|
|
2969
|
+
"jobs-trigger": integrationTemplate({
|
|
2970
|
+
title: "Trigger.dev starter",
|
|
2971
|
+
description: "Typed background jobs backed by Trigger.dev",
|
|
2972
|
+
provider: "jobs-trigger",
|
|
2973
|
+
label: "Trigger.dev",
|
|
2974
|
+
route: "/integrations/jobs-trigger",
|
|
2975
|
+
docsPath: "/docs/integrations/trigger"
|
|
2976
|
+
}),
|
|
2977
|
+
polar: integrationTemplate({
|
|
2978
|
+
title: "Polar starter",
|
|
2979
|
+
description: "Polar products, checkout, billing state, and customer portal",
|
|
2980
|
+
provider: "polar",
|
|
2981
|
+
label: "Polar",
|
|
2982
|
+
route: "/integrations/polar",
|
|
2983
|
+
docsPath: "/docs/integrations/polar"
|
|
2984
|
+
}),
|
|
2985
|
+
resend: integrationTemplate({
|
|
2986
|
+
title: "Resend starter",
|
|
2987
|
+
description: "Resend templates, delivery, scheduling, and webhooks",
|
|
2988
|
+
provider: "resend",
|
|
2989
|
+
label: "Resend",
|
|
2990
|
+
route: "/integrations/resend",
|
|
2991
|
+
docsPath: "/docs/integrations/email"
|
|
2992
|
+
}),
|
|
2993
|
+
stripe: integrationTemplate({
|
|
2994
|
+
title: "Stripe starter",
|
|
2995
|
+
description: "Stripe products, checkout, billing portal, and webhooks",
|
|
2996
|
+
provider: "stripe",
|
|
2997
|
+
label: "Stripe",
|
|
2998
|
+
route: "/integrations/stripe",
|
|
2999
|
+
docsPath: "/docs/integrations/stripe"
|
|
3000
|
+
}),
|
|
3001
|
+
supabase: integrationTemplate({
|
|
3002
|
+
title: "Supabase starter",
|
|
3003
|
+
description: "Supabase authentication, sessions, OAuth, and protected routes",
|
|
3004
|
+
provider: "supabase",
|
|
3005
|
+
label: "Supabase",
|
|
3006
|
+
route: "/integrations/supabase",
|
|
3007
|
+
docsPath: "/docs/integrations/auth/supabase"
|
|
3008
|
+
}),
|
|
3009
|
+
unkey: integrationTemplate({
|
|
3010
|
+
title: "Unkey starter",
|
|
3011
|
+
description: "Unkey API key creation, verification, and route protection",
|
|
3012
|
+
provider: "unkey",
|
|
3013
|
+
label: "Unkey",
|
|
3014
|
+
route: "/integrations/unkey",
|
|
3015
|
+
docsPath: "/docs/integrations/unkey"
|
|
3016
|
+
}),
|
|
3017
|
+
workos: integrationTemplate({
|
|
3018
|
+
title: "WorkOS starter",
|
|
3019
|
+
description: "WorkOS AuthKit, organization sessions, and protected routes",
|
|
3020
|
+
provider: "workos",
|
|
3021
|
+
label: "WorkOS",
|
|
3022
|
+
route: "/integrations/workos",
|
|
3023
|
+
docsPath: "/docs/integrations/auth/workos"
|
|
3024
|
+
})
|
|
3025
|
+
};
|
|
3026
|
+
function integrationTemplate(input) {
|
|
3027
|
+
return {
|
|
3028
|
+
title: input.title,
|
|
3029
|
+
description: input.description,
|
|
3030
|
+
instructions: [],
|
|
3031
|
+
integration: {
|
|
3032
|
+
provider: input.provider,
|
|
3033
|
+
label: input.label,
|
|
3034
|
+
route: input.route,
|
|
3035
|
+
docsPath: input.docsPath
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
12
3039
|
async function createApp(projectName, options = {}) {
|
|
13
3040
|
require_utils.showBanner();
|
|
3041
|
+
trackFarmCreateAppCommand({
|
|
3042
|
+
command: options.listTemplates ? "list-templates" : "create",
|
|
3043
|
+
packageVersion: options.telemetryPackageVersion ?? "unknown"
|
|
3044
|
+
});
|
|
14
3045
|
const templates = await getAvailableTemplates();
|
|
15
3046
|
if (templates.length === 0) {
|
|
16
3047
|
require_utils.logger.error("No templates are available in this package.");
|
|
17
3048
|
process.exit(1);
|
|
18
3049
|
}
|
|
3050
|
+
if (options.listTemplates) {
|
|
3051
|
+
require_utils.logger.info("Available templates");
|
|
3052
|
+
for (const template of templates) {
|
|
3053
|
+
const details = templateDetails[template];
|
|
3054
|
+
require_utils.logger.info(` ${template.padEnd(14)} ${details?.description ?? ""}`.trimEnd());
|
|
3055
|
+
}
|
|
3056
|
+
return;
|
|
3057
|
+
}
|
|
19
3058
|
if (!projectName) {
|
|
20
3059
|
const response = await (0, prompts.default)({
|
|
21
3060
|
type: "text",
|
|
@@ -42,11 +3081,14 @@ async function createApp(projectName, options = {}) {
|
|
|
42
3081
|
type: "select",
|
|
43
3082
|
name: "template",
|
|
44
3083
|
message: "Which template would you like to use?",
|
|
45
|
-
choices: templates.map((name) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
3084
|
+
choices: templates.map((name) => {
|
|
3085
|
+
const details = templateDetails[name];
|
|
3086
|
+
return {
|
|
3087
|
+
title: details?.title ?? prettifyTemplateName(name),
|
|
3088
|
+
value: name,
|
|
3089
|
+
description: details?.description
|
|
3090
|
+
};
|
|
3091
|
+
}),
|
|
50
3092
|
initial: 0
|
|
51
3093
|
});
|
|
52
3094
|
if (!response.template) {
|
|
@@ -58,6 +3100,56 @@ async function createApp(projectName, options = {}) {
|
|
|
58
3100
|
require_utils.logger.error(`Unknown template "${template}". Available: ${templates.map((t) => `"${t}"`).join(", ")}`);
|
|
59
3101
|
process.exit(1);
|
|
60
3102
|
}
|
|
3103
|
+
let renderer = options.renderer?.toLowerCase();
|
|
3104
|
+
if (renderer && renderer !== "react" && renderer !== "preact" && renderer !== "solid" && renderer !== "vue" && renderer !== "svelte") {
|
|
3105
|
+
require_utils.logger.error(`Unknown renderer "${options.renderer}". Available: "react", "preact", "solid", "vue", "svelte".`);
|
|
3106
|
+
process.exit(1);
|
|
3107
|
+
}
|
|
3108
|
+
if (!renderer && !options.template && (template === "basic" || template === "better-auth")) {
|
|
3109
|
+
const response = await (0, prompts.default)({
|
|
3110
|
+
type: "select",
|
|
3111
|
+
name: "renderer",
|
|
3112
|
+
message: "Which rendering library would you like to use?",
|
|
3113
|
+
choices: [
|
|
3114
|
+
{
|
|
3115
|
+
title: "React",
|
|
3116
|
+
value: "react",
|
|
3117
|
+
description: "The default FARMJS renderer"
|
|
3118
|
+
},
|
|
3119
|
+
{
|
|
3120
|
+
title: "Preact",
|
|
3121
|
+
value: "preact",
|
|
3122
|
+
description: "Small React-compatible runtime with SSR and hydration"
|
|
3123
|
+
},
|
|
3124
|
+
{
|
|
3125
|
+
title: "Solid",
|
|
3126
|
+
value: "solid",
|
|
3127
|
+
description: "Fine-grained reactivity with Solid"
|
|
3128
|
+
},
|
|
3129
|
+
{
|
|
3130
|
+
title: "Vue",
|
|
3131
|
+
value: "vue",
|
|
3132
|
+
description: "Vue SFCs with server rendering and hydration"
|
|
3133
|
+
},
|
|
3134
|
+
{
|
|
3135
|
+
title: "Svelte",
|
|
3136
|
+
value: "svelte",
|
|
3137
|
+
description: "Svelte components with server rendering and hydration"
|
|
3138
|
+
}
|
|
3139
|
+
],
|
|
3140
|
+
initial: 0
|
|
3141
|
+
});
|
|
3142
|
+
if (!response.renderer) {
|
|
3143
|
+
require_utils.logger.error("Operation cancelled.");
|
|
3144
|
+
process.exit(1);
|
|
3145
|
+
}
|
|
3146
|
+
renderer = response.renderer;
|
|
3147
|
+
}
|
|
3148
|
+
renderer ||= "react";
|
|
3149
|
+
if (renderer !== "react" && template !== "basic" && !await hasRendererIntegrationTemplate(template, renderer)) {
|
|
3150
|
+
require_utils.logger.error(`The "${template}" starter currently targets React. Use --template basic or --template better-auth with --renderer ${renderer}.`);
|
|
3151
|
+
process.exit(1);
|
|
3152
|
+
}
|
|
61
3153
|
let useTypeScript = options.typescript;
|
|
62
3154
|
if (useTypeScript === void 0) {
|
|
63
3155
|
const response = await (0, prompts.default)({
|
|
@@ -85,9 +3177,9 @@ async function createApp(projectName, options = {}) {
|
|
|
85
3177
|
process.exit(1);
|
|
86
3178
|
}
|
|
87
3179
|
}
|
|
88
|
-
require_utils.logger.info(`Creating
|
|
3180
|
+
require_utils.logger.info(`Creating FARMJS app in ${projectPath}`);
|
|
89
3181
|
await fs_promises.default.mkdir(projectPath, { recursive: true });
|
|
90
|
-
await copyTemplate(template, projectPath, useTypeScript);
|
|
3182
|
+
const integrationResult = await copyTemplate(template, projectPath, useTypeScript, renderer);
|
|
91
3183
|
await updatePackageJson(projectPath, projectName, packageManager);
|
|
92
3184
|
require_utils.logger.success(`🚜 Created ${projectName}`);
|
|
93
3185
|
if (!options.skipInstall) {
|
|
@@ -101,14 +3193,194 @@ async function createApp(projectName, options = {}) {
|
|
|
101
3193
|
if (options.skipInstall) require_utils.logger.info(` ${packageManager.name} install`);
|
|
102
3194
|
require_utils.logger.info(` ${getDevCommand(packageManager.name)}`);
|
|
103
3195
|
require_utils.logger.info("");
|
|
104
|
-
|
|
3196
|
+
for (const instruction of templateDetails[template]?.instructions ?? []) require_utils.logger.info(instruction);
|
|
3197
|
+
if (integrationResult) {
|
|
3198
|
+
const details = templateDetails[template].integration;
|
|
3199
|
+
require_utils.logger.info(`Open ${details.route} for the ${details.label} starter.`);
|
|
3200
|
+
if (integrationResult.env.length) require_utils.logger.info("Copy .env.example to .env.local and add your provider credentials.");
|
|
3201
|
+
for (const note of integrationResult.notes) require_utils.logger.info(note);
|
|
3202
|
+
}
|
|
3203
|
+
trackFarmProjectCreated({
|
|
3204
|
+
packageVersion: options.telemetryPackageVersion ?? "unknown",
|
|
3205
|
+
template,
|
|
3206
|
+
renderer,
|
|
3207
|
+
packageManager: packageManager.name,
|
|
3208
|
+
typescript: useTypeScript,
|
|
3209
|
+
installedDependencies: !options.skipInstall
|
|
3210
|
+
});
|
|
105
3211
|
}
|
|
106
|
-
async function copyTemplate(template, projectPath, useTypeScript) {
|
|
107
|
-
|
|
3212
|
+
async function copyTemplate(template, projectPath, useTypeScript, renderer) {
|
|
3213
|
+
const details = templateDetails[template];
|
|
3214
|
+
await copyDir(path.default.join(__dirname, "..", "templates", details?.integration ? "basic" : template), projectPath);
|
|
3215
|
+
const basePackageJson = await readPackageJson(projectPath);
|
|
108
3216
|
if (useTypeScript) {
|
|
109
3217
|
const tsTemplatePath = path.default.join(__dirname, "..", "templates", "_typescript");
|
|
110
3218
|
if (await dirExists(tsTemplatePath)) await copyDir(tsTemplatePath, projectPath);
|
|
111
3219
|
}
|
|
3220
|
+
if (renderer !== "react") {
|
|
3221
|
+
await applyRendererTemplate(projectPath, renderer, basePackageJson);
|
|
3222
|
+
const integrationRendererPath = getRendererIntegrationTemplatePath(template, renderer);
|
|
3223
|
+
if (await dirExists(integrationRendererPath)) {
|
|
3224
|
+
await removeRendererIntegrationConflicts(projectPath);
|
|
3225
|
+
await copyDir(integrationRendererPath, projectPath);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
if (!details?.integration) return;
|
|
3229
|
+
const result = await addFarmIntegration({
|
|
3230
|
+
root: projectPath,
|
|
3231
|
+
provider: details.integration.provider,
|
|
3232
|
+
ui: true
|
|
3233
|
+
});
|
|
3234
|
+
await writeEnvironmentExample(projectPath, result.env);
|
|
3235
|
+
await writeIntegrationHomePage(projectPath, details.integration, result.env.length > 0);
|
|
3236
|
+
await writeIntegrationReadme(projectPath, details.integration, result.env);
|
|
3237
|
+
return result;
|
|
3238
|
+
}
|
|
3239
|
+
async function applyRendererTemplate(projectPath, renderer, basePackageJson) {
|
|
3240
|
+
const rendererTemplatePath = path.default.join(__dirname, "..", "templates", "_renderers", renderer);
|
|
3241
|
+
if (renderer === "vue" || renderer === "svelte") await Promise.all([
|
|
3242
|
+
fs_promises.default.rm(path.default.join(projectPath, "src", "app", "page.tsx"), { force: true }),
|
|
3243
|
+
fs_promises.default.rm(path.default.join(projectPath, "src", "app", "layout.tsx"), { force: true }),
|
|
3244
|
+
fs_promises.default.rm(path.default.join(projectPath, "src", "components", "resource-links.tsx"), { force: true })
|
|
3245
|
+
]);
|
|
3246
|
+
await copyDir(rendererTemplatePath, projectPath);
|
|
3247
|
+
await writePackageJson(projectPath, mergeRendererPackageJson(basePackageJson, await readPackageJson(projectPath)));
|
|
3248
|
+
}
|
|
3249
|
+
function mergeRendererPackageJson(base, renderer) {
|
|
3250
|
+
const dependencies = {
|
|
3251
|
+
...base.dependencies,
|
|
3252
|
+
...renderer.dependencies
|
|
3253
|
+
};
|
|
3254
|
+
const devDependencies = {
|
|
3255
|
+
...base.devDependencies,
|
|
3256
|
+
...renderer.devDependencies
|
|
3257
|
+
};
|
|
3258
|
+
for (const name of ["react", "react-dom"]) delete dependencies[name];
|
|
3259
|
+
for (const name of ["@types/react", "@types/react-dom"]) delete devDependencies[name];
|
|
3260
|
+
return {
|
|
3261
|
+
...base,
|
|
3262
|
+
...renderer.type ? { type: renderer.type } : {},
|
|
3263
|
+
scripts: {
|
|
3264
|
+
...base.scripts,
|
|
3265
|
+
...renderer.scripts
|
|
3266
|
+
},
|
|
3267
|
+
dependencies,
|
|
3268
|
+
devDependencies
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
async function readPackageJson(projectPath) {
|
|
3272
|
+
return JSON.parse(await fs_promises.default.readFile(path.default.join(projectPath, "package.json"), "utf8"));
|
|
3273
|
+
}
|
|
3274
|
+
async function writePackageJson(projectPath, packageJson) {
|
|
3275
|
+
await fs_promises.default.writeFile(path.default.join(projectPath, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
|
3276
|
+
}
|
|
3277
|
+
function getRendererIntegrationTemplatePath(template, renderer) {
|
|
3278
|
+
return path.default.join(__dirname, "..", "templates", "_integrations", template, renderer);
|
|
3279
|
+
}
|
|
3280
|
+
async function hasRendererIntegrationTemplate(template, renderer) {
|
|
3281
|
+
return dirExists(getRendererIntegrationTemplatePath(template, renderer));
|
|
3282
|
+
}
|
|
3283
|
+
async function removeRendererIntegrationConflicts(projectPath) {
|
|
3284
|
+
const appFiles = [
|
|
3285
|
+
"page.tsx",
|
|
3286
|
+
"layout.tsx",
|
|
3287
|
+
"loading.tsx",
|
|
3288
|
+
"error.tsx",
|
|
3289
|
+
"not-found.tsx",
|
|
3290
|
+
"sign-in/page.tsx",
|
|
3291
|
+
"sign-up/page.tsx",
|
|
3292
|
+
"dashboard/page.tsx",
|
|
3293
|
+
"preact.css",
|
|
3294
|
+
"solid.css",
|
|
3295
|
+
"vue.css",
|
|
3296
|
+
"svelte.css",
|
|
3297
|
+
"api/greeting/route.ts"
|
|
3298
|
+
];
|
|
3299
|
+
const componentFiles = [
|
|
3300
|
+
"auth-form.tsx",
|
|
3301
|
+
"auth-shell.tsx",
|
|
3302
|
+
"sign-out-button.tsx",
|
|
3303
|
+
"site-header.tsx"
|
|
3304
|
+
];
|
|
3305
|
+
await Promise.all([
|
|
3306
|
+
...appFiles.map((file) => fs_promises.default.rm(path.default.join(projectPath, "src", "app", file), { force: true })),
|
|
3307
|
+
...componentFiles.map((file) => fs_promises.default.rm(path.default.join(projectPath, "src", "components", file), { force: true })),
|
|
3308
|
+
fs_promises.default.rm(path.default.join(projectPath, "src", "lib", "api-client.ts"), { force: true }),
|
|
3309
|
+
fs_promises.default.rm(path.default.join(projectPath, "src", "lib", "api.generated.ts"), { force: true })
|
|
3310
|
+
]);
|
|
3311
|
+
}
|
|
3312
|
+
async function writeEnvironmentExample(projectPath, keys) {
|
|
3313
|
+
if (keys.length === 0) return;
|
|
3314
|
+
const envPath = path.default.join(projectPath, ".env.example");
|
|
3315
|
+
let current = "";
|
|
3316
|
+
try {
|
|
3317
|
+
current = await fs_promises.default.readFile(envPath, "utf8");
|
|
3318
|
+
} catch {}
|
|
3319
|
+
const existingKeys = new Set(current.split(/\r?\n/).map((line) => line.match(/^([A-Z][A-Z0-9_]*)=/)?.[1]).filter((key) => Boolean(key)));
|
|
3320
|
+
const additions = keys.filter((key) => !existingKeys.has(key)).map((key) => `${key}=${environmentExampleValue(key)}`);
|
|
3321
|
+
if (additions.length > 0) await fs_promises.default.writeFile(envPath, `${current.trimEnd()}${current.trim() ? "\n" : ""}${additions.join("\n")}\n`, "utf8");
|
|
3322
|
+
}
|
|
3323
|
+
function environmentExampleValue(key) {
|
|
3324
|
+
if (key === "APP_BASE_URL") return "http://localhost:3000";
|
|
3325
|
+
if (key === "AUTH_SECRET") return "replace-with-at-least-32-random-characters";
|
|
3326
|
+
if (key === "UNKEY_BASE_URL") return "https://api.unkey.com";
|
|
3327
|
+
return "";
|
|
3328
|
+
}
|
|
3329
|
+
async function writeIntegrationHomePage(projectPath, integration, hasEnvironment) {
|
|
3330
|
+
const commandRows = [...hasEnvironment ? ["cp .env.example .env.local"] : [], "pnpm dev"].map((command, index) => ` <div className="command-row">
|
|
3331
|
+
<span>${String(index + 1).padStart(2, "0")}</span>
|
|
3332
|
+
<code>${command}</code>
|
|
3333
|
+
</div>`).join("\n");
|
|
3334
|
+
const source = `import { ResourceLinks } from "../components/resource-links";
|
|
3335
|
+
|
|
3336
|
+
export default function HomePage() {
|
|
3337
|
+
return (
|
|
3338
|
+
<main className="landing-main">
|
|
3339
|
+
<section className="hero-section">
|
|
3340
|
+
<div className="hero-copy">
|
|
3341
|
+
<div className="eyebrow-row">
|
|
3342
|
+
<span>00</span>
|
|
3343
|
+
<span>FARMJS / ${integration.label} starter</span>
|
|
3344
|
+
</div>
|
|
3345
|
+
|
|
3346
|
+
<h1>
|
|
3347
|
+
Start at <code>${integration.route}</code>.
|
|
3348
|
+
</h1>
|
|
3349
|
+
|
|
3350
|
+
<div className="command-list" aria-label="Getting started commands">
|
|
3351
|
+
${commandRows}
|
|
3352
|
+
</div>
|
|
3353
|
+
|
|
3354
|
+
<ResourceLinks
|
|
3355
|
+
className="resource-links"
|
|
3356
|
+
primary={{ href: "${integration.route}", label: "Get started" }}
|
|
3357
|
+
/>
|
|
3358
|
+
</div>
|
|
3359
|
+
</section>
|
|
3360
|
+
</main>
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
`;
|
|
3364
|
+
await fs_promises.default.writeFile(path.default.join(projectPath, "src", "app", "page.tsx"), source, "utf8");
|
|
3365
|
+
}
|
|
3366
|
+
async function writeIntegrationReadme(projectPath, integration, env) {
|
|
3367
|
+
const environmentSetup = env.length ? `cp .env.example .env.local\n# Add values for: ${env.join(", ")}\n` : "";
|
|
3368
|
+
const wiring = integration.provider === "ai" ? "The AI route lives in `src/app/api/chat/route.ts` and its local UI lives under `src/components/farm`." : "The provider wiring lives in `src/lib/integrations` and is registered from `farm.config.ts`.";
|
|
3369
|
+
const source = `# FARMJS ${integration.label} Starter
|
|
3370
|
+
|
|
3371
|
+
## Getting started
|
|
3372
|
+
|
|
3373
|
+
\`\`\`bash
|
|
3374
|
+
pnpm install
|
|
3375
|
+
${environmentSetup}pnpm dev
|
|
3376
|
+
\`\`\`
|
|
3377
|
+
|
|
3378
|
+
Open [${integration.route}](http://localhost:3000${integration.route}) for the integration UI.
|
|
3379
|
+
|
|
3380
|
+
${wiring}
|
|
3381
|
+
See the [${integration.label} integration guide](https://farm.js.dev${integration.docsPath}) for provider setup and production guidance.
|
|
3382
|
+
`;
|
|
3383
|
+
await fs_promises.default.writeFile(path.default.join(projectPath, "README.md"), source, "utf8");
|
|
112
3384
|
}
|
|
113
3385
|
async function copyDir(src, dest) {
|
|
114
3386
|
await fs_promises.default.mkdir(dest, { recursive: true });
|
|
@@ -121,9 +3393,9 @@ async function copyDir(src, dest) {
|
|
|
121
3393
|
else await fs_promises.default.copyFile(srcPath, destPath);
|
|
122
3394
|
}
|
|
123
3395
|
}
|
|
124
|
-
async function dirExists(path$
|
|
3396
|
+
async function dirExists(path$5) {
|
|
125
3397
|
try {
|
|
126
|
-
return (await fs_promises.default.stat(path$
|
|
3398
|
+
return (await fs_promises.default.stat(path$5)).isDirectory();
|
|
127
3399
|
} catch {
|
|
128
3400
|
return false;
|
|
129
3401
|
}
|
|
@@ -152,14 +3424,22 @@ function prettifyTemplateName(name) {
|
|
|
152
3424
|
}
|
|
153
3425
|
async function getAvailableTemplates() {
|
|
154
3426
|
const templatesRoot = path.default.join(__dirname, "..", "templates");
|
|
155
|
-
|
|
3427
|
+
const entries = await fs_promises.default.readdir(templatesRoot, { withFileTypes: true });
|
|
3428
|
+
const templateOrder = Object.keys(templateDetails);
|
|
3429
|
+
const directoryTemplates = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("_")).map((entry) => entry.name);
|
|
3430
|
+
const generatedTemplates = Object.entries(templateDetails).filter(([, details]) => Boolean(details.integration)).map(([name]) => name);
|
|
3431
|
+
return [.../* @__PURE__ */ new Set([...directoryTemplates, ...generatedTemplates])].sort((left, right) => {
|
|
3432
|
+
const leftIndex = templateOrder.indexOf(left);
|
|
3433
|
+
const rightIndex = templateOrder.indexOf(right);
|
|
3434
|
+
return (leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex) - (rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex) || left.localeCompare(right);
|
|
3435
|
+
});
|
|
156
3436
|
}
|
|
157
3437
|
async function updatePackageJson(projectPath, projectName, packageManager) {
|
|
158
3438
|
const packageJsonPath = path.default.join(projectPath, "package.json");
|
|
159
3439
|
try {
|
|
160
3440
|
const content = await fs_promises.default.readFile(packageJsonPath, "utf-8");
|
|
161
3441
|
const packageJson = JSON.parse(content);
|
|
162
|
-
packageJson.name = projectName;
|
|
3442
|
+
packageJson.name = path.default.basename(projectName.replace(/[\\/]+$/, "")) || projectName;
|
|
163
3443
|
if (packageManager.version) packageJson.packageManager = `${packageManager.name}@${packageManager.version}`;
|
|
164
3444
|
await fs_promises.default.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
165
3445
|
} catch {
|
|
@@ -177,11 +3457,13 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
|
|
|
177
3457
|
}
|
|
178
3458
|
function installDependencies(projectPath, packageManager) {
|
|
179
3459
|
return new Promise((resolve, reject) => {
|
|
180
|
-
const
|
|
3460
|
+
const isWindows = process.platform === "win32";
|
|
3461
|
+
const command = isWindows ? `${packageManager.name}.cmd` : packageManager.name;
|
|
181
3462
|
const child = (0, node_child_process.spawn)(command, ["install"], {
|
|
182
3463
|
cwd: projectPath,
|
|
183
3464
|
env: process.env,
|
|
184
|
-
stdio: "inherit"
|
|
3465
|
+
stdio: "inherit",
|
|
3466
|
+
shell: isWindows
|
|
185
3467
|
});
|
|
186
3468
|
child.on("error", reject);
|
|
187
3469
|
child.on("close", (code, signal) => {
|