create-better-t-stack 3.7.3-canary.7cbb05fc → 3.7.3-canary.8e4d5716
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/package.json +19 -23
- package/src/cli.ts +3 -0
- package/src/constants.ts +188 -0
- package/src/helpers/addons/addons-setup.ts +226 -0
- package/src/helpers/addons/examples-setup.ts +104 -0
- package/src/helpers/addons/fumadocs-setup.ts +103 -0
- package/src/helpers/addons/ruler-setup.ts +139 -0
- package/src/helpers/addons/starlight-setup.ts +51 -0
- package/src/helpers/addons/tauri-setup.ts +96 -0
- package/src/helpers/addons/ultracite-setup.ts +232 -0
- package/src/helpers/addons/vite-pwa-setup.ts +59 -0
- package/src/helpers/core/add-addons.ts +85 -0
- package/src/helpers/core/add-deployment.ts +102 -0
- package/src/helpers/core/api-setup.ts +280 -0
- package/src/helpers/core/auth-setup.ts +203 -0
- package/src/helpers/core/backend-setup.ts +73 -0
- package/src/helpers/core/command-handlers.ts +354 -0
- package/src/helpers/core/convex-codegen.ts +14 -0
- package/src/helpers/core/create-project.ts +133 -0
- package/src/helpers/core/create-readme.ts +687 -0
- package/src/helpers/core/db-setup.ts +184 -0
- package/src/helpers/core/detect-project-config.ts +41 -0
- package/src/helpers/core/env-setup.ts +449 -0
- package/src/helpers/core/git.ts +31 -0
- package/src/helpers/core/install-dependencies.ts +32 -0
- package/src/helpers/core/payments-setup.ts +48 -0
- package/src/helpers/core/post-installation.ts +383 -0
- package/src/helpers/core/project-config.ts +246 -0
- package/src/helpers/core/runtime-setup.ts +76 -0
- package/src/helpers/core/template-manager.ts +917 -0
- package/src/helpers/core/workspace-setup.ts +184 -0
- package/src/helpers/database-providers/d1-setup.ts +28 -0
- package/src/helpers/database-providers/docker-compose-setup.ts +50 -0
- package/src/helpers/database-providers/mongodb-atlas-setup.ts +186 -0
- package/src/helpers/database-providers/neon-setup.ts +243 -0
- package/src/helpers/database-providers/planetscale-setup.ts +78 -0
- package/src/helpers/database-providers/prisma-postgres-setup.ts +196 -0
- package/src/helpers/database-providers/supabase-setup.ts +218 -0
- package/src/helpers/database-providers/turso-setup.ts +309 -0
- package/src/helpers/deployment/alchemy/alchemy-combined-setup.ts +80 -0
- package/src/helpers/deployment/alchemy/alchemy-next-setup.ts +51 -0
- package/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts +104 -0
- package/src/helpers/deployment/alchemy/alchemy-react-router-setup.ts +32 -0
- package/src/helpers/deployment/alchemy/alchemy-solid-setup.ts +32 -0
- package/src/helpers/deployment/alchemy/alchemy-svelte-setup.ts +98 -0
- package/src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts +33 -0
- package/src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts +98 -0
- package/src/helpers/deployment/alchemy/env-dts-setup.ts +76 -0
- package/src/helpers/deployment/alchemy/index.ts +7 -0
- package/src/helpers/deployment/server-deploy-setup.ts +55 -0
- package/src/helpers/deployment/web-deploy-setup.ts +58 -0
- package/src/index.ts +253 -0
- package/src/prompts/addons.ts +178 -0
- package/src/prompts/api.ts +49 -0
- package/src/prompts/auth.ts +84 -0
- package/src/prompts/backend.ts +83 -0
- package/src/prompts/config-prompts.ts +138 -0
- package/src/prompts/database-setup.ts +112 -0
- package/src/prompts/database.ts +57 -0
- package/src/prompts/examples.ts +64 -0
- package/src/prompts/frontend.ts +118 -0
- package/src/prompts/git.ts +16 -0
- package/src/prompts/install.ts +16 -0
- package/src/prompts/orm.ts +53 -0
- package/src/prompts/package-manager.ts +32 -0
- package/src/prompts/payments.ts +50 -0
- package/src/prompts/project-name.ts +86 -0
- package/src/prompts/runtime.ts +47 -0
- package/src/prompts/server-deploy.ts +91 -0
- package/src/prompts/web-deploy.ts +107 -0
- package/src/types.ts +2 -0
- package/src/utils/add-package-deps.ts +57 -0
- package/src/utils/analytics.ts +39 -0
- package/src/utils/better-auth-plugin-setup.ts +71 -0
- package/src/utils/biome-formatter.ts +82 -0
- package/src/utils/bts-config.ts +122 -0
- package/src/utils/command-exists.ts +16 -0
- package/src/utils/compatibility-rules.ts +319 -0
- package/src/utils/compatibility.ts +11 -0
- package/src/utils/config-processing.ts +130 -0
- package/src/utils/config-validation.ts +470 -0
- package/src/utils/display-config.ts +96 -0
- package/src/utils/docker-utils.ts +70 -0
- package/src/utils/errors.ts +32 -0
- package/src/utils/generate-reproducible-command.ts +53 -0
- package/src/utils/get-latest-cli-version.ts +11 -0
- package/src/utils/get-package-manager.ts +13 -0
- package/src/utils/open-url.ts +25 -0
- package/src/utils/package-runner.ts +23 -0
- package/src/utils/project-directory.ts +102 -0
- package/src/utils/project-name-validation.ts +43 -0
- package/src/utils/render-title.ts +48 -0
- package/src/utils/setup-catalogs.ts +192 -0
- package/src/utils/sponsors.ts +101 -0
- package/src/utils/telemetry.ts +19 -0
- package/src/utils/template-processor.ts +64 -0
- package/src/utils/templates.ts +94 -0
- package/src/utils/ts-morph.ts +26 -0
- package/src/validation.ts +117 -0
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -8
- package/dist/index.d.mts +0 -347
- package/dist/index.mjs +0 -4
- package/dist/src-CxVxLS85.mjs +0 -7077
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import type { AvailableDependencies } from "../../constants";
|
|
6
|
+
import type { ProjectConfig } from "../../types";
|
|
7
|
+
import { addPackageDependency } from "../../utils/add-package-deps";
|
|
8
|
+
import { setupCloudflareD1 } from "../database-providers/d1-setup";
|
|
9
|
+
import { setupDockerCompose } from "../database-providers/docker-compose-setup";
|
|
10
|
+
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
|
|
11
|
+
import { setupNeonPostgres } from "../database-providers/neon-setup";
|
|
12
|
+
import { setupPlanetScale } from "../database-providers/planetscale-setup";
|
|
13
|
+
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
|
|
14
|
+
import { setupSupabase } from "../database-providers/supabase-setup";
|
|
15
|
+
import { setupTurso } from "../database-providers/turso-setup";
|
|
16
|
+
|
|
17
|
+
export async function setupDatabase(config: ProjectConfig, cliInput?: { manualDb?: boolean }) {
|
|
18
|
+
const { database, orm, dbSetup, backend, projectDir } = config;
|
|
19
|
+
|
|
20
|
+
if (backend === "convex" || database === "none") {
|
|
21
|
+
if (backend !== "convex") {
|
|
22
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
23
|
+
const serverDbDir = path.join(serverDir, "src/db");
|
|
24
|
+
if (await fs.pathExists(serverDbDir)) {
|
|
25
|
+
await fs.remove(serverDbDir);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const dbPackageDir = path.join(projectDir, "packages/db");
|
|
32
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
33
|
+
const webDirExists = await fs.pathExists(webDir);
|
|
34
|
+
|
|
35
|
+
if (!(await fs.pathExists(dbPackageDir))) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (orm === "prisma") {
|
|
41
|
+
if (database === "mongodb") {
|
|
42
|
+
await addPackageDependency({
|
|
43
|
+
customDependencies: {
|
|
44
|
+
"@prisma/client": "6.19.0",
|
|
45
|
+
},
|
|
46
|
+
customDevDependencies: {
|
|
47
|
+
prisma: "6.19.0",
|
|
48
|
+
},
|
|
49
|
+
projectDir: dbPackageDir,
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
const prismaDependencies: AvailableDependencies[] = ["@prisma/client"];
|
|
53
|
+
const prismaDevDependencies: AvailableDependencies[] = ["prisma"];
|
|
54
|
+
|
|
55
|
+
if (database === "mysql" && dbSetup === "planetscale") {
|
|
56
|
+
prismaDependencies.push("@prisma/adapter-planetscale", "@planetscale/database");
|
|
57
|
+
} else if (database === "mysql") {
|
|
58
|
+
prismaDependencies.push("@prisma/adapter-mariadb");
|
|
59
|
+
} else if (database === "sqlite") {
|
|
60
|
+
if (dbSetup === "d1") {
|
|
61
|
+
prismaDependencies.push("@prisma/adapter-d1");
|
|
62
|
+
} else {
|
|
63
|
+
prismaDependencies.push("@prisma/adapter-libsql");
|
|
64
|
+
}
|
|
65
|
+
} else if (database === "postgres") {
|
|
66
|
+
if (dbSetup === "neon") {
|
|
67
|
+
prismaDependencies.push("@prisma/adapter-neon", "@neondatabase/serverless", "ws");
|
|
68
|
+
prismaDevDependencies.push("@types/ws");
|
|
69
|
+
} else if (dbSetup === "prisma-postgres") {
|
|
70
|
+
prismaDependencies.push("@prisma/adapter-pg");
|
|
71
|
+
} else {
|
|
72
|
+
prismaDependencies.push("@prisma/adapter-pg");
|
|
73
|
+
prismaDependencies.push("pg");
|
|
74
|
+
prismaDevDependencies.push("@types/pg");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await addPackageDependency({
|
|
79
|
+
dependencies: prismaDependencies,
|
|
80
|
+
devDependencies: prismaDevDependencies,
|
|
81
|
+
projectDir: dbPackageDir,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (await fs.pathExists(webDir)) {
|
|
86
|
+
if (database === "mongodb") {
|
|
87
|
+
await addPackageDependency({
|
|
88
|
+
customDependencies: {
|
|
89
|
+
"@prisma/client": "6.19.0",
|
|
90
|
+
},
|
|
91
|
+
projectDir: webDir,
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
await addPackageDependency({
|
|
95
|
+
dependencies: ["@prisma/client"],
|
|
96
|
+
projectDir: webDir,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else if (orm === "drizzle") {
|
|
101
|
+
if (database === "sqlite") {
|
|
102
|
+
await addPackageDependency({
|
|
103
|
+
dependencies: ["drizzle-orm", "@libsql/client", "libsql"],
|
|
104
|
+
devDependencies: ["drizzle-kit"],
|
|
105
|
+
projectDir: dbPackageDir,
|
|
106
|
+
});
|
|
107
|
+
if (webDirExists) {
|
|
108
|
+
await addPackageDependency({
|
|
109
|
+
dependencies: ["@libsql/client", "libsql"],
|
|
110
|
+
projectDir: webDir,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} else if (database === "postgres") {
|
|
114
|
+
if (dbSetup === "neon") {
|
|
115
|
+
await addPackageDependency({
|
|
116
|
+
dependencies: ["drizzle-orm", "@neondatabase/serverless", "ws"],
|
|
117
|
+
devDependencies: ["drizzle-kit", "@types/ws"],
|
|
118
|
+
projectDir: dbPackageDir,
|
|
119
|
+
});
|
|
120
|
+
} else if (dbSetup === "planetscale") {
|
|
121
|
+
await addPackageDependency({
|
|
122
|
+
dependencies: ["drizzle-orm", "pg"],
|
|
123
|
+
devDependencies: ["drizzle-kit", "@types/pg"],
|
|
124
|
+
projectDir: dbPackageDir,
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
await addPackageDependency({
|
|
128
|
+
dependencies: ["drizzle-orm", "pg"],
|
|
129
|
+
devDependencies: ["drizzle-kit", "@types/pg"],
|
|
130
|
+
projectDir: dbPackageDir,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} else if (database === "mysql") {
|
|
134
|
+
if (dbSetup === "planetscale") {
|
|
135
|
+
await addPackageDependency({
|
|
136
|
+
dependencies: ["drizzle-orm", "@planetscale/database"],
|
|
137
|
+
devDependencies: ["drizzle-kit"],
|
|
138
|
+
projectDir: dbPackageDir,
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
await addPackageDependency({
|
|
142
|
+
dependencies: ["drizzle-orm", "mysql2"],
|
|
143
|
+
devDependencies: ["drizzle-kit"],
|
|
144
|
+
projectDir: dbPackageDir,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else if (orm === "mongoose") {
|
|
149
|
+
await addPackageDependency({
|
|
150
|
+
dependencies: ["mongoose"],
|
|
151
|
+
devDependencies: [],
|
|
152
|
+
projectDir: dbPackageDir,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (dbSetup === "docker") {
|
|
157
|
+
await setupDockerCompose(config);
|
|
158
|
+
} else if (database === "sqlite" && dbSetup === "turso") {
|
|
159
|
+
await setupTurso(config, cliInput);
|
|
160
|
+
} else if (database === "sqlite" && dbSetup === "d1") {
|
|
161
|
+
await setupCloudflareD1(config);
|
|
162
|
+
} else if (database === "postgres") {
|
|
163
|
+
if (dbSetup === "prisma-postgres") {
|
|
164
|
+
await setupPrismaPostgres(config, cliInput);
|
|
165
|
+
} else if (dbSetup === "neon") {
|
|
166
|
+
await setupNeonPostgres(config, cliInput);
|
|
167
|
+
} else if (dbSetup === "planetscale") {
|
|
168
|
+
await setupPlanetScale(config);
|
|
169
|
+
} else if (dbSetup === "supabase") {
|
|
170
|
+
await setupSupabase(config, cliInput);
|
|
171
|
+
}
|
|
172
|
+
} else if (database === "mysql") {
|
|
173
|
+
if (dbSetup === "planetscale") {
|
|
174
|
+
await setupPlanetScale(config);
|
|
175
|
+
}
|
|
176
|
+
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
|
|
177
|
+
await setupMongoDBAtlas(config, cliInput);
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof Error) {
|
|
181
|
+
consola.error(pc.red(error.message));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { readBtsConfig } from "../../utils/bts-config";
|
|
4
|
+
|
|
5
|
+
export async function detectProjectConfig(projectDir: string) {
|
|
6
|
+
try {
|
|
7
|
+
const btsConfig = await readBtsConfig(projectDir);
|
|
8
|
+
if (btsConfig) {
|
|
9
|
+
return {
|
|
10
|
+
projectDir,
|
|
11
|
+
projectName: path.basename(projectDir),
|
|
12
|
+
database: btsConfig.database,
|
|
13
|
+
orm: btsConfig.orm,
|
|
14
|
+
backend: btsConfig.backend,
|
|
15
|
+
runtime: btsConfig.runtime,
|
|
16
|
+
frontend: btsConfig.frontend,
|
|
17
|
+
addons: btsConfig.addons,
|
|
18
|
+
examples: btsConfig.examples,
|
|
19
|
+
auth: btsConfig.auth,
|
|
20
|
+
payments: btsConfig.payments,
|
|
21
|
+
packageManager: btsConfig.packageManager,
|
|
22
|
+
dbSetup: btsConfig.dbSetup,
|
|
23
|
+
api: btsConfig.api,
|
|
24
|
+
webDeploy: btsConfig.webDeploy,
|
|
25
|
+
serverDeploy: btsConfig.serverDeploy,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function isBetterTStackProject(projectDir: string) {
|
|
36
|
+
try {
|
|
37
|
+
return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import type { ProjectConfig } from "../../types";
|
|
4
|
+
import { generateAuthSecret } from "./auth-setup";
|
|
5
|
+
|
|
6
|
+
function getClientServerVar(frontend: string[], backend: ProjectConfig["backend"]) {
|
|
7
|
+
const hasNextJs = frontend.includes("next");
|
|
8
|
+
const hasNuxt = frontend.includes("nuxt");
|
|
9
|
+
const hasSvelte = frontend.includes("svelte");
|
|
10
|
+
const hasTanstackStart = frontend.includes("tanstack-start");
|
|
11
|
+
|
|
12
|
+
// For fullstack self, no base URL is needed (same-origin)
|
|
13
|
+
if (backend === "self") {
|
|
14
|
+
return { key: "", value: "", write: false } as const;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let key = "VITE_SERVER_URL";
|
|
18
|
+
if (hasNextJs) key = "NEXT_PUBLIC_SERVER_URL";
|
|
19
|
+
else if (hasNuxt) key = "NUXT_PUBLIC_SERVER_URL";
|
|
20
|
+
else if (hasSvelte) key = "PUBLIC_SERVER_URL";
|
|
21
|
+
else if (hasTanstackStart) key = "VITE_SERVER_URL";
|
|
22
|
+
|
|
23
|
+
return { key, value: "http://localhost:3000", write: true } as const;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getConvexVar(frontend: string[]) {
|
|
27
|
+
const hasNextJs = frontend.includes("next");
|
|
28
|
+
const hasNuxt = frontend.includes("nuxt");
|
|
29
|
+
const hasSvelte = frontend.includes("svelte");
|
|
30
|
+
const hasTanstackStart = frontend.includes("tanstack-start");
|
|
31
|
+
if (hasNextJs) return "NEXT_PUBLIC_CONVEX_URL";
|
|
32
|
+
if (hasNuxt) return "NUXT_PUBLIC_CONVEX_URL";
|
|
33
|
+
if (hasSvelte) return "PUBLIC_CONVEX_URL";
|
|
34
|
+
if (hasTanstackStart) return "VITE_CONVEX_URL";
|
|
35
|
+
return "VITE_CONVEX_URL";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface EnvVariable {
|
|
39
|
+
key: string;
|
|
40
|
+
value: string | null | undefined;
|
|
41
|
+
condition: boolean;
|
|
42
|
+
comment?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function addEnvVariablesToFile(filePath: string, variables: EnvVariable[]) {
|
|
46
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
47
|
+
|
|
48
|
+
let envContent = "";
|
|
49
|
+
if (await fs.pathExists(filePath)) {
|
|
50
|
+
envContent = await fs.readFile(filePath, "utf8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let modified = false;
|
|
54
|
+
let contentToAdd = "";
|
|
55
|
+
const exampleVariables: string[] = [];
|
|
56
|
+
|
|
57
|
+
for (const { key, value, condition, comment } of variables) {
|
|
58
|
+
if (condition) {
|
|
59
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
60
|
+
const valueToWrite = value ?? "";
|
|
61
|
+
exampleVariables.push(`${key}=`);
|
|
62
|
+
|
|
63
|
+
if (regex.test(envContent)) {
|
|
64
|
+
const existingMatch = envContent.match(regex);
|
|
65
|
+
if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) {
|
|
66
|
+
envContent = envContent.replace(regex, `${key}=${valueToWrite}`);
|
|
67
|
+
modified = true;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
if (comment) {
|
|
71
|
+
contentToAdd += `# ${comment}\n`;
|
|
72
|
+
}
|
|
73
|
+
contentToAdd += `${key}=${valueToWrite}\n`;
|
|
74
|
+
modified = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (contentToAdd) {
|
|
80
|
+
if (envContent.length > 0 && !envContent.endsWith("\n")) {
|
|
81
|
+
envContent += "\n";
|
|
82
|
+
}
|
|
83
|
+
envContent += contentToAdd;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (modified) {
|
|
87
|
+
await fs.writeFile(filePath, envContent.trimEnd());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
|
|
91
|
+
let exampleEnvContent = "";
|
|
92
|
+
if (await fs.pathExists(exampleFilePath)) {
|
|
93
|
+
exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let exampleModified = false;
|
|
97
|
+
let exampleContentToAdd = "";
|
|
98
|
+
|
|
99
|
+
for (const exampleVar of exampleVariables) {
|
|
100
|
+
const key = exampleVar.split("=")[0];
|
|
101
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
102
|
+
if (!regex.test(exampleEnvContent)) {
|
|
103
|
+
exampleContentToAdd += `${exampleVar}\n`;
|
|
104
|
+
exampleModified = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (exampleContentToAdd) {
|
|
109
|
+
if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) {
|
|
110
|
+
exampleEnvContent += "\n";
|
|
111
|
+
}
|
|
112
|
+
exampleEnvContent += exampleContentToAdd;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (exampleModified || !(await fs.pathExists(exampleFilePath))) {
|
|
116
|
+
await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function setupEnvironmentVariables(config: ProjectConfig) {
|
|
121
|
+
const {
|
|
122
|
+
backend,
|
|
123
|
+
frontend,
|
|
124
|
+
database,
|
|
125
|
+
auth,
|
|
126
|
+
examples,
|
|
127
|
+
dbSetup,
|
|
128
|
+
projectDir,
|
|
129
|
+
webDeploy,
|
|
130
|
+
serverDeploy,
|
|
131
|
+
} = config;
|
|
132
|
+
|
|
133
|
+
const hasReactRouter = frontend.includes("react-router");
|
|
134
|
+
const hasTanStackRouter = frontend.includes("tanstack-router");
|
|
135
|
+
const hasTanStackStart = frontend.includes("tanstack-start");
|
|
136
|
+
const hasNextJs = frontend.includes("next");
|
|
137
|
+
const hasNuxt = frontend.includes("nuxt");
|
|
138
|
+
const hasSvelte = frontend.includes("svelte");
|
|
139
|
+
const hasSolid = frontend.includes("solid");
|
|
140
|
+
const hasWebFrontend =
|
|
141
|
+
hasReactRouter ||
|
|
142
|
+
hasTanStackRouter ||
|
|
143
|
+
hasTanStackStart ||
|
|
144
|
+
hasNextJs ||
|
|
145
|
+
hasNuxt ||
|
|
146
|
+
hasSolid ||
|
|
147
|
+
hasSvelte;
|
|
148
|
+
|
|
149
|
+
if (hasWebFrontend) {
|
|
150
|
+
const clientDir = path.join(projectDir, "apps/web");
|
|
151
|
+
if (await fs.pathExists(clientDir)) {
|
|
152
|
+
const baseVar = getClientServerVar(frontend, backend);
|
|
153
|
+
const envVarName = backend === "convex" ? getConvexVar(frontend) : baseVar.key;
|
|
154
|
+
const serverUrl = backend === "convex" ? "https://<YOUR_CONVEX_URL>" : baseVar.value;
|
|
155
|
+
|
|
156
|
+
const clientVars: EnvVariable[] = [
|
|
157
|
+
{
|
|
158
|
+
key: envVarName,
|
|
159
|
+
value: serverUrl,
|
|
160
|
+
condition: backend === "convex" ? true : baseVar.write,
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
if (backend === "convex" && auth === "clerk") {
|
|
165
|
+
if (hasNextJs) {
|
|
166
|
+
clientVars.push(
|
|
167
|
+
{
|
|
168
|
+
key: "NEXT_PUBLIC_CLERK_FRONTEND_API_URL",
|
|
169
|
+
value: "",
|
|
170
|
+
condition: true,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
|
174
|
+
value: "",
|
|
175
|
+
condition: true,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
key: "CLERK_SECRET_KEY",
|
|
179
|
+
value: "",
|
|
180
|
+
condition: true,
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
} else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) {
|
|
184
|
+
clientVars.push({
|
|
185
|
+
key: "VITE_CLERK_PUBLISHABLE_KEY",
|
|
186
|
+
value: "",
|
|
187
|
+
condition: true,
|
|
188
|
+
});
|
|
189
|
+
if (hasTanStackStart) {
|
|
190
|
+
clientVars.push({
|
|
191
|
+
key: "CLERK_SECRET_KEY",
|
|
192
|
+
value: "",
|
|
193
|
+
condition: true,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (backend === "convex" && auth === "better-auth") {
|
|
200
|
+
if (hasNextJs) {
|
|
201
|
+
clientVars.push({
|
|
202
|
+
key: "NEXT_PUBLIC_CONVEX_SITE_URL",
|
|
203
|
+
value: "https://<YOUR_CONVEX_URL>",
|
|
204
|
+
condition: true,
|
|
205
|
+
});
|
|
206
|
+
} else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) {
|
|
207
|
+
clientVars.push({
|
|
208
|
+
key: "VITE_CONVEX_SITE_URL",
|
|
209
|
+
value: "https://<YOUR_CONVEX_URL>",
|
|
210
|
+
condition: true,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
frontend.includes("native-bare") ||
|
|
221
|
+
frontend.includes("native-uniwind") ||
|
|
222
|
+
frontend.includes("native-unistyles")
|
|
223
|
+
) {
|
|
224
|
+
const nativeDir = path.join(projectDir, "apps/native");
|
|
225
|
+
if (await fs.pathExists(nativeDir)) {
|
|
226
|
+
let envVarName = "EXPO_PUBLIC_SERVER_URL";
|
|
227
|
+
let serverUrl = "http://localhost:3000";
|
|
228
|
+
|
|
229
|
+
if (backend === "self") {
|
|
230
|
+
// Both TanStack Start and Next.js use port 3001 for fullstack
|
|
231
|
+
serverUrl = "http://localhost:3001";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (backend === "convex") {
|
|
235
|
+
envVarName = "EXPO_PUBLIC_CONVEX_URL";
|
|
236
|
+
serverUrl = "https://<YOUR_CONVEX_URL>";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const nativeVars: EnvVariable[] = [
|
|
240
|
+
{
|
|
241
|
+
key: envVarName,
|
|
242
|
+
value: serverUrl,
|
|
243
|
+
condition: true,
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
if (backend === "convex" && auth === "clerk") {
|
|
248
|
+
nativeVars.push({
|
|
249
|
+
key: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
|
250
|
+
value: "",
|
|
251
|
+
condition: true,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (backend === "convex" && auth === "better-auth") {
|
|
256
|
+
nativeVars.push({
|
|
257
|
+
key: "EXPO_PUBLIC_CONVEX_SITE_URL",
|
|
258
|
+
value: "https://<YOUR_CONVEX_URL>",
|
|
259
|
+
condition: true,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (backend === "convex") {
|
|
267
|
+
if (auth === "better-auth") {
|
|
268
|
+
const convexBackendDir = path.join(projectDir, "packages/backend");
|
|
269
|
+
if (await fs.pathExists(convexBackendDir)) {
|
|
270
|
+
const envLocalPath = path.join(convexBackendDir, ".env.local");
|
|
271
|
+
|
|
272
|
+
const hasNative =
|
|
273
|
+
frontend.includes("native-bare") ||
|
|
274
|
+
frontend.includes("native-uniwind") ||
|
|
275
|
+
frontend.includes("native-unistyles");
|
|
276
|
+
const hasWeb = hasWebFrontend;
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
!(await fs.pathExists(envLocalPath)) ||
|
|
280
|
+
!(await fs.readFile(envLocalPath, "utf8")).includes("npx convex env set")
|
|
281
|
+
) {
|
|
282
|
+
const convexCommands = `# Set Convex environment variables
|
|
283
|
+
# npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
|
|
284
|
+
${hasWeb ? "# npx convex env set SITE_URL http://localhost:3001\n" : ""}
|
|
285
|
+
`;
|
|
286
|
+
await fs.appendFile(envLocalPath, convexCommands);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const convexBackendVars: EnvVariable[] = [];
|
|
290
|
+
|
|
291
|
+
if (hasNative) {
|
|
292
|
+
convexBackendVars.push({
|
|
293
|
+
key: "EXPO_PUBLIC_CONVEX_SITE_URL",
|
|
294
|
+
value: "",
|
|
295
|
+
condition: true,
|
|
296
|
+
comment: "Same as CONVEX_URL but ends in .site",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (hasWeb) {
|
|
301
|
+
convexBackendVars.push(
|
|
302
|
+
{
|
|
303
|
+
key: hasNextJs ? "NEXT_PUBLIC_CONVEX_SITE_URL" : "VITE_CONVEX_SITE_URL",
|
|
304
|
+
value: "",
|
|
305
|
+
condition: true,
|
|
306
|
+
comment: "Same as CONVEX_URL but ends in .site",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
key: "SITE_URL",
|
|
310
|
+
value: "http://localhost:3001",
|
|
311
|
+
condition: true,
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await addEnvVariablesToFile(envLocalPath, convexBackendVars);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
323
|
+
|
|
324
|
+
let corsOrigin = "http://localhost:3001";
|
|
325
|
+
if (backend === "self") {
|
|
326
|
+
corsOrigin = "http://localhost:3001";
|
|
327
|
+
} else if (hasReactRouter || hasSvelte) {
|
|
328
|
+
corsOrigin = "http://localhost:5173";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let databaseUrl: string | null = null;
|
|
332
|
+
|
|
333
|
+
if (database !== "none" && dbSetup === "none") {
|
|
334
|
+
switch (database) {
|
|
335
|
+
case "postgres":
|
|
336
|
+
databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
|
|
337
|
+
break;
|
|
338
|
+
case "mysql":
|
|
339
|
+
databaseUrl = "mysql://root:password@localhost:3306/mydb";
|
|
340
|
+
break;
|
|
341
|
+
case "mongodb":
|
|
342
|
+
databaseUrl = "mongodb://localhost:27017/mydatabase";
|
|
343
|
+
break;
|
|
344
|
+
case "sqlite":
|
|
345
|
+
if (config.runtime === "workers" || webDeploy === "alchemy" || serverDeploy === "alchemy") {
|
|
346
|
+
databaseUrl = "http://127.0.0.1:8080";
|
|
347
|
+
} else {
|
|
348
|
+
const dbAppDir = backend === "self" ? "apps/web" : "apps/server";
|
|
349
|
+
databaseUrl = `file:${path.join(config.projectDir, dbAppDir, "local.db")}`;
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const serverVars: EnvVariable[] = [
|
|
356
|
+
{
|
|
357
|
+
key: "BETTER_AUTH_SECRET",
|
|
358
|
+
value: generateAuthSecret(),
|
|
359
|
+
condition: !!auth,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
key: "BETTER_AUTH_URL",
|
|
363
|
+
value: backend === "self" ? "http://localhost:3001" : "http://localhost:3000",
|
|
364
|
+
condition: !!auth,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
key: "POLAR_ACCESS_TOKEN",
|
|
368
|
+
value: "",
|
|
369
|
+
condition: config.payments === "polar",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
key: "POLAR_SUCCESS_URL",
|
|
373
|
+
value: `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`,
|
|
374
|
+
condition: config.payments === "polar",
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
key: "CORS_ORIGIN",
|
|
378
|
+
value: corsOrigin,
|
|
379
|
+
condition: true,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
key: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
383
|
+
value: "",
|
|
384
|
+
condition: examples?.includes("ai") || false,
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
key: "DATABASE_URL",
|
|
388
|
+
value: databaseUrl,
|
|
389
|
+
condition: database !== "none" && dbSetup === "none",
|
|
390
|
+
},
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
if (backend === "self") {
|
|
394
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
395
|
+
if (await fs.pathExists(webDir)) {
|
|
396
|
+
await addEnvVariablesToFile(path.join(webDir, ".env"), serverVars);
|
|
397
|
+
}
|
|
398
|
+
} else if (await fs.pathExists(serverDir)) {
|
|
399
|
+
await addEnvVariablesToFile(path.join(serverDir, ".env"), serverVars);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const isUnifiedAlchemy = webDeploy === "alchemy" && serverDeploy === "alchemy";
|
|
403
|
+
const isIndividualAlchemy = webDeploy === "alchemy" || serverDeploy === "alchemy";
|
|
404
|
+
|
|
405
|
+
if (isUnifiedAlchemy) {
|
|
406
|
+
const rootEnvPath = path.join(projectDir, ".env");
|
|
407
|
+
const rootAlchemyVars: EnvVariable[] = [
|
|
408
|
+
{
|
|
409
|
+
key: "ALCHEMY_PASSWORD",
|
|
410
|
+
value: "please-change-this",
|
|
411
|
+
condition: true,
|
|
412
|
+
},
|
|
413
|
+
];
|
|
414
|
+
await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
|
|
415
|
+
} else if (isIndividualAlchemy) {
|
|
416
|
+
if (webDeploy === "alchemy") {
|
|
417
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
418
|
+
if (await fs.pathExists(webDir)) {
|
|
419
|
+
const webAlchemyVars: EnvVariable[] = [
|
|
420
|
+
{
|
|
421
|
+
key: "ALCHEMY_PASSWORD",
|
|
422
|
+
value: "please-change-this",
|
|
423
|
+
condition: true,
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (serverDeploy === "alchemy") {
|
|
431
|
+
const serverAlchemyVars: EnvVariable[] = [
|
|
432
|
+
{
|
|
433
|
+
key: "ALCHEMY_PASSWORD",
|
|
434
|
+
value: "please-change-this",
|
|
435
|
+
condition: true,
|
|
436
|
+
},
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
if (backend === "self") {
|
|
440
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
441
|
+
if (await fs.pathExists(webDir)) {
|
|
442
|
+
await addEnvVariablesToFile(path.join(webDir, ".env"), serverAlchemyVars);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
await addEnvVariablesToFile(path.join(serverDir, ".env"), serverAlchemyVars);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { log } from "@clack/prompts";
|
|
2
|
+
import { $ } from "execa";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
|
|
5
|
+
export async function initializeGit(projectDir: string, useGit: boolean) {
|
|
6
|
+
if (!useGit) return;
|
|
7
|
+
|
|
8
|
+
const gitVersionResult = await $({
|
|
9
|
+
cwd: projectDir,
|
|
10
|
+
reject: false,
|
|
11
|
+
stderr: "pipe",
|
|
12
|
+
})`git --version`;
|
|
13
|
+
|
|
14
|
+
if (gitVersionResult.exitCode !== 0) {
|
|
15
|
+
log.warn(pc.yellow("Git is not installed"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = await $({
|
|
20
|
+
cwd: projectDir,
|
|
21
|
+
reject: false,
|
|
22
|
+
stderr: "pipe",
|
|
23
|
+
})`git init`;
|
|
24
|
+
|
|
25
|
+
if (result.exitCode !== 0) {
|
|
26
|
+
throw new Error(`Git initialization failed: ${result.stderr}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await $({ cwd: projectDir })`git add -A`;
|
|
30
|
+
await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
|
|
31
|
+
}
|