create-better-t-stack 2.33.9 → 2.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +1 -1
- package/dist/{src-BSUXyw1T.js → src-CqC0HMY7.js} +1477 -561
- package/package.json +7 -4
- package/templates/addons/biome/biome.json.hbs +1 -0
- package/templates/addons/ruler/.ruler/mcp.json.hbs +1 -1
- package/templates/addons/ultracite/biome.json.hbs +1 -0
- package/templates/auth/server/base/src/lib/auth.ts.hbs +37 -4
- package/templates/backend/server/server-base/_gitignore +1 -0
- package/templates/backend/server/server-base/tsconfig.json.hbs +1 -1
- package/templates/base/_gitignore +2 -0
- package/templates/db/drizzle/sqlite/drizzle.config.ts.hbs +2 -0
- package/templates/deploy/alchemy/alchemy.run.ts.hbs +208 -0
- package/templates/deploy/alchemy/env.d.ts.hbs +20 -0
- package/templates/deploy/alchemy/wrangler.jsonc.hbs +11 -0
- package/templates/deploy/{web → wrangler/web}/nuxt/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/react/next/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/react/react-router/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/react/tanstack-router/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/react/tanstack-start/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/solid/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/svelte/wrangler.jsonc.hbs +1 -1
- package/templates/frontend/nuxt/_gitignore +3 -0
- package/templates/frontend/nuxt/tsconfig.json.hbs +1 -3
- package/templates/frontend/react/web-base/_gitignore +1 -0
- package/templates/frontend/react/web-base/src/components/header.tsx.hbs +0 -1
- package/templates/frontend/solid/_gitignore +1 -0
- package/templates/frontend/solid/package.json.hbs +0 -1
- package/templates/frontend/svelte/_gitignore +1 -0
- package/templates/frontend/svelte/package.json.hbs +11 -13
- /package/templates/{runtime/workers/apps → deploy/wrangler}/server/wrangler.jsonc.hbs +0 -0
- /package/templates/deploy/{web → wrangler/web}/react/next/open-next.config.ts +0 -0
|
@@ -10,9 +10,10 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import gradient from "gradient-string";
|
|
11
11
|
import * as JSONC from "jsonc-parser";
|
|
12
12
|
import { $, execa } from "execa";
|
|
13
|
-
import {
|
|
13
|
+
import { glob } from "tinyglobby";
|
|
14
14
|
import handlebars from "handlebars";
|
|
15
15
|
import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
|
|
16
|
+
import { Biome } from "@biomejs/js-api/nodejs";
|
|
16
17
|
import os from "node:os";
|
|
17
18
|
|
|
18
19
|
//#region src/utils/get-package-manager.ts
|
|
@@ -28,9 +29,8 @@ const getUserPkgManager = () => {
|
|
|
28
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
30
|
const distPath = path.dirname(__filename);
|
|
30
31
|
const PKG_ROOT = path.join(distPath, "../");
|
|
31
|
-
const
|
|
32
|
+
const DEFAULT_CONFIG_BASE = {
|
|
32
33
|
projectName: "my-better-t-app",
|
|
33
|
-
projectDir: path.resolve(process.cwd(), "my-better-t-app"),
|
|
34
34
|
relativePath: "my-better-t-app",
|
|
35
35
|
frontend: ["tanstack-router"],
|
|
36
36
|
database: "sqlite",
|
|
@@ -39,14 +39,25 @@ const DEFAULT_CONFIG = {
|
|
|
39
39
|
addons: ["turborepo"],
|
|
40
40
|
examples: [],
|
|
41
41
|
git: true,
|
|
42
|
-
packageManager: getUserPkgManager(),
|
|
43
42
|
install: true,
|
|
44
43
|
dbSetup: "none",
|
|
45
44
|
backend: "hono",
|
|
46
45
|
runtime: "bun",
|
|
47
46
|
api: "trpc",
|
|
48
|
-
webDeploy: "none"
|
|
47
|
+
webDeploy: "none",
|
|
48
|
+
serverDeploy: "none"
|
|
49
49
|
};
|
|
50
|
+
function getDefaultConfig() {
|
|
51
|
+
return {
|
|
52
|
+
...DEFAULT_CONFIG_BASE,
|
|
53
|
+
projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
|
|
54
|
+
packageManager: getUserPkgManager(),
|
|
55
|
+
frontend: [...DEFAULT_CONFIG_BASE.frontend],
|
|
56
|
+
addons: [...DEFAULT_CONFIG_BASE.addons],
|
|
57
|
+
examples: [...DEFAULT_CONFIG_BASE.examples]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const DEFAULT_CONFIG = getDefaultConfig();
|
|
50
61
|
const dependencyVersionMap = {
|
|
51
62
|
"better-auth": "^1.3.4",
|
|
52
63
|
"@better-auth/expo": "^1.3.4",
|
|
@@ -103,18 +114,24 @@ const dependencyVersionMap = {
|
|
|
103
114
|
"convex-svelte": "^0.0.11",
|
|
104
115
|
"convex-nuxt": "0.1.5",
|
|
105
116
|
"convex-vue": "^0.1.5",
|
|
106
|
-
"@tanstack/svelte-query": "^5.
|
|
117
|
+
"@tanstack/svelte-query": "^5.85.3",
|
|
118
|
+
"@tanstack/svelte-query-devtools": "^5.85.3",
|
|
107
119
|
"@tanstack/vue-query-devtools": "^5.83.0",
|
|
108
120
|
"@tanstack/vue-query": "^5.83.0",
|
|
109
121
|
"@tanstack/react-query-devtools": "^5.80.5",
|
|
110
122
|
"@tanstack/react-query": "^5.80.5",
|
|
111
123
|
"@tanstack/solid-query": "^5.75.0",
|
|
112
124
|
"@tanstack/solid-query-devtools": "^5.75.0",
|
|
125
|
+
"@tanstack/solid-router-devtools": "^1.131.25",
|
|
113
126
|
wrangler: "^4.23.0",
|
|
114
127
|
"@cloudflare/vite-plugin": "^1.9.0",
|
|
115
128
|
"@opennextjs/cloudflare": "^1.3.0",
|
|
116
129
|
"nitro-cloudflare-dev": "^0.2.2",
|
|
117
|
-
"@sveltejs/adapter-cloudflare": "^7.
|
|
130
|
+
"@sveltejs/adapter-cloudflare": "^7.2.1",
|
|
131
|
+
"@cloudflare/workers-types": "^4.20250813.0",
|
|
132
|
+
alchemy: "^0.62.1",
|
|
133
|
+
nitropack: "^2.12.4",
|
|
134
|
+
dotenv: "^17.2.1"
|
|
118
135
|
};
|
|
119
136
|
const ADDON_COMPATIBILITY = {
|
|
120
137
|
pwa: [
|
|
@@ -128,7 +145,8 @@ const ADDON_COMPATIBILITY = {
|
|
|
128
145
|
"react-router",
|
|
129
146
|
"nuxt",
|
|
130
147
|
"svelte",
|
|
131
|
-
"solid"
|
|
148
|
+
"solid",
|
|
149
|
+
"next"
|
|
132
150
|
],
|
|
133
151
|
biome: [],
|
|
134
152
|
husky: [],
|
|
@@ -233,7 +251,16 @@ const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(
|
|
|
233
251
|
];
|
|
234
252
|
return !invalidChars.some((char) => name.includes(char));
|
|
235
253
|
}, "Project name contains invalid characters").refine((name) => name.toLowerCase() !== "node_modules", "Project name is reserved").describe("Project name or path");
|
|
236
|
-
const WebDeploySchema = z.enum([
|
|
254
|
+
const WebDeploySchema = z.enum([
|
|
255
|
+
"wrangler",
|
|
256
|
+
"alchemy",
|
|
257
|
+
"none"
|
|
258
|
+
]).describe("Web deployment");
|
|
259
|
+
const ServerDeploySchema = z.enum([
|
|
260
|
+
"wrangler",
|
|
261
|
+
"alchemy",
|
|
262
|
+
"none"
|
|
263
|
+
]).describe("Server deployment");
|
|
237
264
|
const DirectoryConflictSchema = z.enum([
|
|
238
265
|
"merge",
|
|
239
266
|
"overwrite",
|
|
@@ -544,6 +571,34 @@ function isExampleAIAllowed(backend, frontends = []) {
|
|
|
544
571
|
function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
|
|
545
572
|
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
546
573
|
}
|
|
574
|
+
function validateServerDeployRequiresBackend(serverDeploy, backend) {
|
|
575
|
+
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
|
|
576
|
+
}
|
|
577
|
+
function validateAddonsAgainstFrontends(addons = [], frontends = []) {
|
|
578
|
+
for (const addon of addons) {
|
|
579
|
+
if (addon === "none") continue;
|
|
580
|
+
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends);
|
|
581
|
+
if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function validateExamplesCompatibility(examples, backend, database, frontend) {
|
|
585
|
+
const examplesArr = examples ?? [];
|
|
586
|
+
if (examplesArr.length === 0 || examplesArr.includes("none")) return;
|
|
587
|
+
if (examplesArr.includes("todo") && backend !== "convex" && backend !== "none" && database === "none") exitWithError("The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.");
|
|
588
|
+
if (examplesArr.includes("ai") && backend === "elysia") exitWithError("The 'ai' example is not compatible with the Elysia backend.");
|
|
589
|
+
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
|
|
590
|
+
}
|
|
591
|
+
function validateAlchemyCompatibility(webDeploy, serverDeploy, frontends = []) {
|
|
592
|
+
const isAlchemyWebDeploy = webDeploy === "alchemy";
|
|
593
|
+
const isAlchemyServerDeploy = serverDeploy === "alchemy";
|
|
594
|
+
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
|
|
595
|
+
const incompatibleFrontends = frontends.filter((f) => f === "next" || f === "react-router");
|
|
596
|
+
if (incompatibleFrontends.length > 0) {
|
|
597
|
+
const deployType = isAlchemyWebDeploy && isAlchemyServerDeploy ? "web and server deployment" : isAlchemyWebDeploy ? "web deployment" : "server deployment";
|
|
598
|
+
exitWithError(`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")} frontend(s). Please choose a different frontend or deployment option.`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
547
602
|
|
|
548
603
|
//#endregion
|
|
549
604
|
//#region src/prompts/api.ts
|
|
@@ -1004,16 +1059,97 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
1004
1059
|
return response;
|
|
1005
1060
|
}
|
|
1006
1061
|
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/prompts/server-deploy.ts
|
|
1064
|
+
function getDeploymentDisplay$1(deployment) {
|
|
1065
|
+
if (deployment === "wrangler") return {
|
|
1066
|
+
label: "Wrangler",
|
|
1067
|
+
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
1068
|
+
};
|
|
1069
|
+
if (deployment === "alchemy") return {
|
|
1070
|
+
label: "Alchemy",
|
|
1071
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
1072
|
+
};
|
|
1073
|
+
return {
|
|
1074
|
+
label: deployment,
|
|
1075
|
+
hint: `Add ${deployment} deployment`
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
async function getServerDeploymentChoice(deployment, runtime, backend, webDeploy) {
|
|
1079
|
+
if (deployment !== void 0) return deployment;
|
|
1080
|
+
if (backend === "none" || backend === "convex") return "none";
|
|
1081
|
+
const options = [];
|
|
1082
|
+
if (runtime === "workers") ["alchemy", "wrangler"].forEach((deploy) => {
|
|
1083
|
+
const { label, hint } = getDeploymentDisplay$1(deploy);
|
|
1084
|
+
options.unshift({
|
|
1085
|
+
value: deploy,
|
|
1086
|
+
label,
|
|
1087
|
+
hint
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
else options.push({
|
|
1091
|
+
value: "none",
|
|
1092
|
+
label: "None",
|
|
1093
|
+
hint: "Manual setup"
|
|
1094
|
+
});
|
|
1095
|
+
const response = await select({
|
|
1096
|
+
message: "Select server deployment",
|
|
1097
|
+
options,
|
|
1098
|
+
initialValue: webDeploy === "alchemy" ? "alchemy" : runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
|
|
1099
|
+
});
|
|
1100
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1101
|
+
return response;
|
|
1102
|
+
}
|
|
1103
|
+
async function getServerDeploymentToAdd(runtime, existingDeployment) {
|
|
1104
|
+
const options = [];
|
|
1105
|
+
if (runtime === "workers") {
|
|
1106
|
+
if (existingDeployment !== "wrangler") {
|
|
1107
|
+
const { label, hint } = getDeploymentDisplay$1("wrangler");
|
|
1108
|
+
options.push({
|
|
1109
|
+
value: "wrangler",
|
|
1110
|
+
label,
|
|
1111
|
+
hint
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
if (existingDeployment !== "alchemy") {
|
|
1115
|
+
const { label, hint } = getDeploymentDisplay$1("alchemy");
|
|
1116
|
+
options.push({
|
|
1117
|
+
value: "alchemy",
|
|
1118
|
+
label,
|
|
1119
|
+
hint
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (existingDeployment && existingDeployment !== "none") return "none";
|
|
1124
|
+
if (options.length > 0) options.push({
|
|
1125
|
+
value: "none",
|
|
1126
|
+
label: "None",
|
|
1127
|
+
hint: "Skip deployment setup"
|
|
1128
|
+
});
|
|
1129
|
+
if (options.length === 0) return "none";
|
|
1130
|
+
const response = await select({
|
|
1131
|
+
message: "Select server deployment",
|
|
1132
|
+
options,
|
|
1133
|
+
initialValue: runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
|
|
1134
|
+
});
|
|
1135
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1136
|
+
return response;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1007
1139
|
//#endregion
|
|
1008
1140
|
//#region src/prompts/web-deploy.ts
|
|
1009
1141
|
function hasWebFrontend(frontends) {
|
|
1010
1142
|
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
|
|
1011
1143
|
}
|
|
1012
1144
|
function getDeploymentDisplay(deployment) {
|
|
1013
|
-
if (deployment === "
|
|
1014
|
-
label: "
|
|
1145
|
+
if (deployment === "wrangler") return {
|
|
1146
|
+
label: "Wrangler",
|
|
1015
1147
|
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
1016
1148
|
};
|
|
1149
|
+
if (deployment === "alchemy") return {
|
|
1150
|
+
label: "Alchemy",
|
|
1151
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
1152
|
+
};
|
|
1017
1153
|
return {
|
|
1018
1154
|
label: deployment,
|
|
1019
1155
|
hint: `Add ${deployment} deployment`
|
|
@@ -1022,15 +1158,18 @@ function getDeploymentDisplay(deployment) {
|
|
|
1022
1158
|
async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
|
|
1023
1159
|
if (deployment !== void 0) return deployment;
|
|
1024
1160
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1025
|
-
const options = [
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1161
|
+
const options = [
|
|
1162
|
+
"wrangler",
|
|
1163
|
+
"alchemy",
|
|
1164
|
+
"none"
|
|
1165
|
+
].map((deploy) => {
|
|
1166
|
+
const { label, hint } = getDeploymentDisplay(deploy);
|
|
1167
|
+
return {
|
|
1168
|
+
value: deploy,
|
|
1169
|
+
label,
|
|
1170
|
+
hint
|
|
1171
|
+
};
|
|
1172
|
+
});
|
|
1034
1173
|
const response = await select({
|
|
1035
1174
|
message: "Select web deployment",
|
|
1036
1175
|
options,
|
|
@@ -1042,10 +1181,18 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
|
|
|
1042
1181
|
async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
1043
1182
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1044
1183
|
const options = [];
|
|
1045
|
-
if (existingDeployment !== "
|
|
1046
|
-
const { label, hint } = getDeploymentDisplay("
|
|
1184
|
+
if (existingDeployment !== "wrangler") {
|
|
1185
|
+
const { label, hint } = getDeploymentDisplay("wrangler");
|
|
1186
|
+
options.push({
|
|
1187
|
+
value: "wrangler",
|
|
1188
|
+
label,
|
|
1189
|
+
hint
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
if (existingDeployment !== "alchemy") {
|
|
1193
|
+
const { label, hint } = getDeploymentDisplay("alchemy");
|
|
1047
1194
|
options.push({
|
|
1048
|
-
value: "
|
|
1195
|
+
value: "alchemy",
|
|
1049
1196
|
label,
|
|
1050
1197
|
hint
|
|
1051
1198
|
});
|
|
@@ -1081,6 +1228,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1081
1228
|
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
|
|
1082
1229
|
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
|
|
1083
1230
|
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend),
|
|
1231
|
+
serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
|
|
1084
1232
|
git: () => getGitChoice(flags.git),
|
|
1085
1233
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
|
1086
1234
|
install: () => getinstallChoice(flags.install)
|
|
@@ -1120,7 +1268,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1120
1268
|
install: result.install,
|
|
1121
1269
|
dbSetup: result.dbSetup,
|
|
1122
1270
|
api: result.api,
|
|
1123
|
-
webDeploy: result.webDeploy
|
|
1271
|
+
webDeploy: result.webDeploy,
|
|
1272
|
+
serverDeploy: result.serverDeploy
|
|
1124
1273
|
};
|
|
1125
1274
|
}
|
|
1126
1275
|
|
|
@@ -1152,7 +1301,7 @@ async function getProjectName(initialName) {
|
|
|
1152
1301
|
let projectPath = "";
|
|
1153
1302
|
let defaultName = DEFAULT_CONFIG.projectName;
|
|
1154
1303
|
let counter = 1;
|
|
1155
|
-
while (fs.
|
|
1304
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
1156
1305
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
|
1157
1306
|
counter++;
|
|
1158
1307
|
}
|
|
@@ -1199,7 +1348,7 @@ const getLatestCLIVersion = () => {
|
|
|
1199
1348
|
*/
|
|
1200
1349
|
function isTelemetryEnabled() {
|
|
1201
1350
|
const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
|
|
1202
|
-
const BTS_TELEMETRY = "
|
|
1351
|
+
const BTS_TELEMETRY = "0";
|
|
1203
1352
|
if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
|
|
1204
1353
|
if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
|
|
1205
1354
|
return true;
|
|
@@ -1207,11 +1356,16 @@ function isTelemetryEnabled() {
|
|
|
1207
1356
|
|
|
1208
1357
|
//#endregion
|
|
1209
1358
|
//#region src/utils/analytics.ts
|
|
1210
|
-
const POSTHOG_API_KEY = "
|
|
1211
|
-
const POSTHOG_HOST = "
|
|
1359
|
+
const POSTHOG_API_KEY = "random";
|
|
1360
|
+
const POSTHOG_HOST = "random";
|
|
1361
|
+
function generateSessionId() {
|
|
1362
|
+
const rand = Math.random().toString(36).slice(2);
|
|
1363
|
+
const now = Date.now().toString(36);
|
|
1364
|
+
return `cli_${now}${rand}`;
|
|
1365
|
+
}
|
|
1212
1366
|
async function trackProjectCreation(config, disableAnalytics = false) {
|
|
1213
1367
|
if (!isTelemetryEnabled() || disableAnalytics) return;
|
|
1214
|
-
const sessionId =
|
|
1368
|
+
const sessionId = generateSessionId();
|
|
1215
1369
|
const { projectName, projectDir, relativePath,...safeConfig } = config;
|
|
1216
1370
|
const payload = {
|
|
1217
1371
|
api_key: POSTHOG_API_KEY,
|
|
@@ -1219,8 +1373,8 @@ async function trackProjectCreation(config, disableAnalytics = false) {
|
|
|
1219
1373
|
properties: {
|
|
1220
1374
|
...safeConfig,
|
|
1221
1375
|
cli_version: getLatestCLIVersion(),
|
|
1222
|
-
node_version: process.version,
|
|
1223
|
-
platform: process.platform,
|
|
1376
|
+
node_version: typeof process !== "undefined" ? process.version : "",
|
|
1377
|
+
platform: typeof process !== "undefined" ? process.platform : "",
|
|
1224
1378
|
$ip: null
|
|
1225
1379
|
},
|
|
1226
1380
|
distinct_id: sessionId
|
|
@@ -1274,6 +1428,7 @@ function displayConfig(config) {
|
|
|
1274
1428
|
}
|
|
1275
1429
|
if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
|
|
1276
1430
|
if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
|
|
1431
|
+
if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
|
|
1277
1432
|
if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
|
|
1278
1433
|
return configDisplay.join("\n");
|
|
1279
1434
|
}
|
|
@@ -1296,6 +1451,7 @@ function generateReproducibleCommand(config) {
|
|
|
1296
1451
|
else flags.push("--examples none");
|
|
1297
1452
|
flags.push(`--db-setup ${config.dbSetup}`);
|
|
1298
1453
|
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
1454
|
+
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
1299
1455
|
flags.push(config.git ? "--git" : "--no-git");
|
|
1300
1456
|
flags.push(`--package-manager ${config.packageManager}`);
|
|
1301
1457
|
flags.push(config.install ? "--install" : "--no-install");
|
|
@@ -1313,8 +1469,8 @@ function generateReproducibleCommand(config) {
|
|
|
1313
1469
|
async function handleDirectoryConflict(currentPathInput, silent = false) {
|
|
1314
1470
|
while (true) {
|
|
1315
1471
|
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
|
1316
|
-
const dirExists = fs.
|
|
1317
|
-
const dirIsNotEmpty = dirExists && fs.
|
|
1472
|
+
const dirExists = await fs.pathExists(resolvedPath);
|
|
1473
|
+
const dirIsNotEmpty = dirExists && (await fs.readdir(resolvedPath)).length > 0;
|
|
1318
1474
|
if (!dirIsNotEmpty) return {
|
|
1319
1475
|
finalPathInput: currentPathInput,
|
|
1320
1476
|
shouldClearDirectory: false
|
|
@@ -1440,7 +1596,7 @@ const renderTitle = () => {
|
|
|
1440
1596
|
};
|
|
1441
1597
|
|
|
1442
1598
|
//#endregion
|
|
1443
|
-
//#region src/
|
|
1599
|
+
//#region src/utils/config-processing.ts
|
|
1444
1600
|
function processArrayOption(options) {
|
|
1445
1601
|
if (!options || options.length === 0) return [];
|
|
1446
1602
|
if (options.includes("none")) return [];
|
|
@@ -1451,22 +1607,10 @@ function deriveProjectName(projectName, projectDirectory) {
|
|
|
1451
1607
|
if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
|
|
1452
1608
|
return "";
|
|
1453
1609
|
}
|
|
1454
|
-
function
|
|
1455
|
-
const result = ProjectNameSchema.safeParse(name);
|
|
1456
|
-
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1457
|
-
}
|
|
1458
|
-
function processAndValidateFlags(options, providedFlags, projectName) {
|
|
1610
|
+
function processFlags(options, projectName) {
|
|
1459
1611
|
const config = {};
|
|
1460
|
-
if (options.api)
|
|
1461
|
-
config.api = options.api;
|
|
1462
|
-
if (options.api === "none") {
|
|
1463
|
-
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1612
|
+
if (options.api) config.api = options.api;
|
|
1466
1613
|
if (options.backend) config.backend = options.backend;
|
|
1467
|
-
if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
|
|
1468
|
-
if (providedFlags.has("runtime") && options.runtime === "none") exitWithError(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
|
|
1469
|
-
}
|
|
1470
1614
|
if (options.database) config.database = options.database;
|
|
1471
1615
|
if (options.orm) config.orm = options.orm;
|
|
1472
1616
|
if (options.auth !== void 0) config.auth = options.auth;
|
|
@@ -1476,70 +1620,190 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1476
1620
|
if (options.dbSetup) config.dbSetup = options.dbSetup;
|
|
1477
1621
|
if (options.packageManager) config.packageManager = options.packageManager;
|
|
1478
1622
|
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1623
|
+
if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
|
|
1479
1624
|
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
|
1480
|
-
if (derivedName)
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1625
|
+
if (derivedName) config.projectName = projectName || derivedName;
|
|
1626
|
+
if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
|
|
1627
|
+
if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
|
|
1628
|
+
if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
|
|
1629
|
+
return config;
|
|
1630
|
+
}
|
|
1631
|
+
function getProvidedFlags(options) {
|
|
1632
|
+
return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
|
|
1633
|
+
}
|
|
1634
|
+
function validateNoneExclusivity(options, optionName) {
|
|
1635
|
+
if (!options || options.length === 0) return;
|
|
1636
|
+
if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
|
|
1637
|
+
}
|
|
1638
|
+
function validateArrayOptions(options) {
|
|
1639
|
+
validateNoneExclusivity(options.frontend, "frontend options");
|
|
1640
|
+
validateNoneExclusivity(options.addons, "addons");
|
|
1641
|
+
validateNoneExclusivity(options.examples, "examples");
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
//#endregion
|
|
1645
|
+
//#region src/utils/config-validation.ts
|
|
1646
|
+
function validateDatabaseOrmAuth(cfg, flags) {
|
|
1647
|
+
const db = cfg.database;
|
|
1648
|
+
const orm = cfg.orm;
|
|
1649
|
+
const has = (k) => flags ? flags.has(k) : true;
|
|
1650
|
+
if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
|
|
1651
|
+
if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1652
|
+
if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1653
|
+
if (has("database") && has("orm") && db && db !== "none" && orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
|
|
1654
|
+
if (has("orm") && has("database") && orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1655
|
+
if (has("auth") && has("database") && cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1656
|
+
if (cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1657
|
+
if (orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1658
|
+
}
|
|
1659
|
+
function validateDatabaseSetup(config, providedFlags) {
|
|
1660
|
+
const { dbSetup, database, runtime } = config;
|
|
1661
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("database") && dbSetup && dbSetup !== "none" && database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
|
|
1662
|
+
const setupValidations = {
|
|
1663
|
+
turso: {
|
|
1664
|
+
database: "sqlite",
|
|
1665
|
+
errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
|
|
1666
|
+
},
|
|
1667
|
+
neon: {
|
|
1668
|
+
database: "postgres",
|
|
1669
|
+
errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1670
|
+
},
|
|
1671
|
+
"prisma-postgres": {
|
|
1672
|
+
database: "postgres",
|
|
1673
|
+
errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1674
|
+
},
|
|
1675
|
+
"mongodb-atlas": {
|
|
1676
|
+
database: "mongodb",
|
|
1677
|
+
errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
|
|
1678
|
+
},
|
|
1679
|
+
supabase: {
|
|
1680
|
+
database: "postgres",
|
|
1681
|
+
errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1682
|
+
},
|
|
1683
|
+
d1: {
|
|
1684
|
+
database: "sqlite",
|
|
1685
|
+
runtime: "workers",
|
|
1686
|
+
errorMessage: "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime."
|
|
1687
|
+
},
|
|
1688
|
+
docker: { errorMessage: "Docker setup is not compatible with SQLite database or Cloudflare Workers runtime." },
|
|
1689
|
+
none: { errorMessage: "" }
|
|
1690
|
+
};
|
|
1691
|
+
if (dbSetup && dbSetup !== "none") {
|
|
1692
|
+
const validation = setupValidations[dbSetup];
|
|
1693
|
+
if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
|
|
1694
|
+
if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
|
|
1695
|
+
if (dbSetup === "docker") {
|
|
1696
|
+
if (database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
|
|
1697
|
+
if (runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function validateBackendConstraints(config, providedFlags, options) {
|
|
1702
|
+
const { backend } = config;
|
|
1703
|
+
if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none") {
|
|
1704
|
+
if (providedFlags.has("runtime") && options.runtime === "none") exitWithError("'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.");
|
|
1504
1705
|
}
|
|
1505
|
-
if (
|
|
1506
|
-
const incompatibleFlags = incompatibleFlagsForBackend(
|
|
1507
|
-
if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${
|
|
1508
|
-
if (
|
|
1706
|
+
if (backend === "convex" || backend === "none") {
|
|
1707
|
+
const incompatibleFlags = incompatibleFlagsForBackend(backend, providedFlags, options);
|
|
1708
|
+
if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1709
|
+
if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
|
|
1509
1710
|
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
|
1510
1711
|
if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1511
1712
|
}
|
|
1512
1713
|
coerceBackendPresets(config);
|
|
1513
1714
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
if (
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup && config.dbSetup !== "none" && config.database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
|
|
1521
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "turso" && config.database !== "sqlite") exitWithError("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1522
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") exitWithError("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1523
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") exitWithError("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1524
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") exitWithError("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
|
|
1525
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") exitWithError("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1526
|
-
if (config.dbSetup === "d1") {
|
|
1527
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
|
|
1528
|
-
if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1529
|
-
}
|
|
1530
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
|
|
1531
|
-
if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
|
|
1532
|
-
}
|
|
1715
|
+
}
|
|
1716
|
+
function validateFrontendConstraints(config, providedFlags) {
|
|
1717
|
+
const { frontend } = config;
|
|
1718
|
+
if (frontend && frontend.length > 0) {
|
|
1719
|
+
ensureSingleWebAndNative(frontend);
|
|
1720
|
+
if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend);
|
|
1533
1721
|
}
|
|
1534
|
-
|
|
1535
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") && config.dbSetup === "docker" && config.runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
1536
|
-
validateWorkersCompatibility(providedFlags, options, config);
|
|
1537
|
-
const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
|
|
1722
|
+
const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
|
|
1538
1723
|
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
1724
|
+
}
|
|
1725
|
+
function validateApiConstraints(config, options) {
|
|
1726
|
+
if (config.api === "none") {
|
|
1727
|
+
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function validateFullConfig(config, providedFlags, options) {
|
|
1731
|
+
validateDatabaseOrmAuth(config, providedFlags);
|
|
1732
|
+
validateDatabaseSetup(config, providedFlags);
|
|
1733
|
+
validateBackendConstraints(config, providedFlags, options);
|
|
1734
|
+
validateFrontendConstraints(config, providedFlags);
|
|
1735
|
+
validateApiConstraints(config, options);
|
|
1736
|
+
validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
|
1737
|
+
validateWorkersCompatibility(providedFlags, options, config);
|
|
1738
|
+
if (config.addons && config.addons.length > 0) {
|
|
1739
|
+
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
|
1740
|
+
config.addons = [...new Set(config.addons)];
|
|
1741
|
+
}
|
|
1742
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
1743
|
+
validateAlchemyCompatibility(config.webDeploy, config.serverDeploy, config.frontend ?? []);
|
|
1744
|
+
}
|
|
1745
|
+
function validateConfigForProgrammaticUse(config) {
|
|
1746
|
+
try {
|
|
1747
|
+
validateDatabaseOrmAuth(config);
|
|
1748
|
+
if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
|
|
1749
|
+
validateApiFrontendCompatibility(config.api, config.frontend);
|
|
1750
|
+
if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend);
|
|
1751
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
if (error instanceof Error) throw error;
|
|
1754
|
+
throw new Error(String(error));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
//#endregion
|
|
1759
|
+
//#region src/utils/project-name-validation.ts
|
|
1760
|
+
function validateProjectName(name) {
|
|
1761
|
+
const result = ProjectNameSchema.safeParse(name);
|
|
1762
|
+
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1763
|
+
}
|
|
1764
|
+
function validateProjectNameThrow(name) {
|
|
1765
|
+
const result = ProjectNameSchema.safeParse(name);
|
|
1766
|
+
if (!result.success) throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
|
|
1767
|
+
}
|
|
1768
|
+
function extractAndValidateProjectName(projectName, projectDirectory, throwOnError = false) {
|
|
1769
|
+
const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
|
|
1770
|
+
if (!derivedName) return "";
|
|
1771
|
+
const nameToValidate = projectName ? path.basename(projectName) : derivedName;
|
|
1772
|
+
if (throwOnError) validateProjectNameThrow(nameToValidate);
|
|
1773
|
+
else validateProjectName(nameToValidate);
|
|
1774
|
+
return projectName || derivedName;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
//#endregion
|
|
1778
|
+
//#region src/validation.ts
|
|
1779
|
+
function processAndValidateFlags(options, providedFlags, projectName) {
|
|
1780
|
+
if (options.yolo) {
|
|
1781
|
+
const cfg = processFlags(options, projectName);
|
|
1782
|
+
const validatedProjectName$1 = extractAndValidateProjectName(projectName, options.projectDirectory, true);
|
|
1783
|
+
if (validatedProjectName$1) cfg.projectName = validatedProjectName$1;
|
|
1784
|
+
return cfg;
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
validateArrayOptions(options);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
exitWithError(error instanceof Error ? error.message : String(error));
|
|
1790
|
+
}
|
|
1791
|
+
const config = processFlags(options, projectName);
|
|
1792
|
+
const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, false);
|
|
1793
|
+
if (validatedProjectName) config.projectName = validatedProjectName;
|
|
1794
|
+
validateFullConfig(config, providedFlags, options);
|
|
1539
1795
|
return config;
|
|
1540
1796
|
}
|
|
1541
|
-
function
|
|
1542
|
-
|
|
1797
|
+
function processProvidedFlagsWithoutValidation(options, projectName) {
|
|
1798
|
+
const config = processFlags(options, projectName);
|
|
1799
|
+
const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, true);
|
|
1800
|
+
if (validatedProjectName) config.projectName = validatedProjectName;
|
|
1801
|
+
return config;
|
|
1802
|
+
}
|
|
1803
|
+
function validateConfigCompatibility(config, providedFlags, options) {
|
|
1804
|
+
if (options?.yolo) return;
|
|
1805
|
+
if (options && providedFlags) validateFullConfig(config, providedFlags, options);
|
|
1806
|
+
else validateConfigForProgrammaticUse(config);
|
|
1543
1807
|
}
|
|
1544
1808
|
|
|
1545
1809
|
//#endregion
|
|
@@ -1560,7 +1824,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1560
1824
|
packageManager: projectConfig.packageManager,
|
|
1561
1825
|
dbSetup: projectConfig.dbSetup,
|
|
1562
1826
|
api: projectConfig.api,
|
|
1563
|
-
webDeploy: projectConfig.webDeploy
|
|
1827
|
+
webDeploy: projectConfig.webDeploy,
|
|
1828
|
+
serverDeploy: projectConfig.serverDeploy
|
|
1564
1829
|
};
|
|
1565
1830
|
const baseContent = {
|
|
1566
1831
|
$schema: "https://r2.better-t-stack.dev/schema.json",
|
|
@@ -1577,7 +1842,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1577
1842
|
packageManager: btsConfig.packageManager,
|
|
1578
1843
|
dbSetup: btsConfig.dbSetup,
|
|
1579
1844
|
api: btsConfig.api,
|
|
1580
|
-
webDeploy: btsConfig.webDeploy
|
|
1845
|
+
webDeploy: btsConfig.webDeploy,
|
|
1846
|
+
serverDeploy: btsConfig.serverDeploy
|
|
1581
1847
|
};
|
|
1582
1848
|
let configContent = JSON.stringify(baseContent);
|
|
1583
1849
|
const formatResult = JSONC.format(configContent, void 0, {
|
|
@@ -1670,7 +1936,7 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
|
|
|
1670
1936
|
}
|
|
1671
1937
|
|
|
1672
1938
|
//#endregion
|
|
1673
|
-
//#region src/helpers/
|
|
1939
|
+
//#region src/helpers/addons/fumadocs-setup.ts
|
|
1674
1940
|
const TEMPLATES = {
|
|
1675
1941
|
"next-mdx": {
|
|
1676
1942
|
label: "Next.js: Fumadocs MDX",
|
|
@@ -1757,9 +2023,9 @@ handlebars.registerHelper("or", (a, b) => a || b);
|
|
|
1757
2023
|
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
1758
2024
|
|
|
1759
2025
|
//#endregion
|
|
1760
|
-
//#region src/helpers/
|
|
2026
|
+
//#region src/helpers/core/template-manager.ts
|
|
1761
2027
|
async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
|
|
1762
|
-
const sourceFiles = await
|
|
2028
|
+
const sourceFiles = await glob(sourcePattern, {
|
|
1763
2029
|
cwd: baseSourceDir,
|
|
1764
2030
|
dot: true,
|
|
1765
2031
|
onlyFiles: true,
|
|
@@ -2073,10 +2339,6 @@ async function handleExtras(projectDir, context) {
|
|
|
2073
2339
|
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
|
|
2074
2340
|
if (await fs.pathExists(npmrcTemplateSrc)) await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
|
|
2075
2341
|
}
|
|
2076
|
-
if (context.runtime === "workers") {
|
|
2077
|
-
const runtimeWorkersDir = path.join(PKG_ROOT, "templates/runtime/workers");
|
|
2078
|
-
if (await fs.pathExists(runtimeWorkersDir)) await processAndCopyFiles("**/*", runtimeWorkersDir, projectDir, context, false);
|
|
2079
|
-
}
|
|
2080
2342
|
}
|
|
2081
2343
|
async function setupDockerComposeTemplates(projectDir, context) {
|
|
2082
2344
|
if (context.dbSetup !== "docker" || context.database === "none") return;
|
|
@@ -2085,29 +2347,62 @@ async function setupDockerComposeTemplates(projectDir, context) {
|
|
|
2085
2347
|
if (await fs.pathExists(dockerSrcDir)) await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
|
|
2086
2348
|
}
|
|
2087
2349
|
async function setupDeploymentTemplates(projectDir, context) {
|
|
2088
|
-
if (context.webDeploy === "
|
|
2089
|
-
|
|
2350
|
+
if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
|
|
2351
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2352
|
+
if (await fs.pathExists(alchemyTemplateSrc)) {
|
|
2353
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, projectDir, context);
|
|
2354
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2355
|
+
if (await fs.pathExists(serverAppDir)) {
|
|
2356
|
+
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2357
|
+
await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
} else {
|
|
2361
|
+
if (context.webDeploy === "alchemy") {
|
|
2362
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2363
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2364
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
|
|
2365
|
+
}
|
|
2366
|
+
if (context.serverDeploy === "alchemy") {
|
|
2367
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2368
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2369
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
|
|
2370
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2371
|
+
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2372
|
+
await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
|
|
2090
2377
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2378
|
+
if (await fs.pathExists(webAppDir)) {
|
|
2379
|
+
const frontends = context.frontend;
|
|
2380
|
+
const templateMap = {
|
|
2381
|
+
"tanstack-router": "react/tanstack-router",
|
|
2382
|
+
"tanstack-start": "react/tanstack-start",
|
|
2383
|
+
"react-router": "react/react-router",
|
|
2384
|
+
solid: "solid",
|
|
2385
|
+
next: "react/next",
|
|
2386
|
+
nuxt: "nuxt",
|
|
2387
|
+
svelte: "svelte"
|
|
2388
|
+
};
|
|
2389
|
+
for (const f of frontends) if (templateMap[f]) {
|
|
2390
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
|
|
2391
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
|
|
2396
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2397
|
+
if (await fs.pathExists(serverAppDir)) {
|
|
2398
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
|
|
2399
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
|
|
2105
2400
|
}
|
|
2106
2401
|
}
|
|
2107
2402
|
}
|
|
2108
2403
|
|
|
2109
2404
|
//#endregion
|
|
2110
|
-
//#region src/helpers/
|
|
2405
|
+
//#region src/helpers/addons/ruler-setup.ts
|
|
2111
2406
|
async function setupVibeRules(config) {
|
|
2112
2407
|
const { packageManager, projectDir } = config;
|
|
2113
2408
|
try {
|
|
@@ -2189,7 +2484,7 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
|
|
|
2189
2484
|
}
|
|
2190
2485
|
|
|
2191
2486
|
//#endregion
|
|
2192
|
-
//#region src/helpers/
|
|
2487
|
+
//#region src/helpers/addons/starlight-setup.ts
|
|
2193
2488
|
async function setupStarlight(config) {
|
|
2194
2489
|
const { packageManager, projectDir } = config;
|
|
2195
2490
|
const s = spinner();
|
|
@@ -2221,7 +2516,7 @@ async function setupStarlight(config) {
|
|
|
2221
2516
|
}
|
|
2222
2517
|
|
|
2223
2518
|
//#endregion
|
|
2224
|
-
//#region src/helpers/
|
|
2519
|
+
//#region src/helpers/addons/tauri-setup.ts
|
|
2225
2520
|
async function setupTauri(config) {
|
|
2226
2521
|
const { packageManager, frontend, projectDir } = config;
|
|
2227
2522
|
const s = spinner();
|
|
@@ -2258,8 +2553,8 @@ async function setupTauri(config) {
|
|
|
2258
2553
|
`--window-title=${path.basename(projectDir)}`,
|
|
2259
2554
|
`--frontend-dist=${frontendDist}`,
|
|
2260
2555
|
`--dev-url=${devUrl}`,
|
|
2261
|
-
`--before-dev-command
|
|
2262
|
-
`--before-build-command
|
|
2556
|
+
`--before-dev-command="${packageManager} run dev"`,
|
|
2557
|
+
`--before-build-command="${packageManager} run build"`
|
|
2263
2558
|
];
|
|
2264
2559
|
const tauriArgsString = tauriArgs.join(" ");
|
|
2265
2560
|
const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
|
|
@@ -2277,7 +2572,7 @@ async function setupTauri(config) {
|
|
|
2277
2572
|
}
|
|
2278
2573
|
|
|
2279
2574
|
//#endregion
|
|
2280
|
-
//#region src/helpers/
|
|
2575
|
+
//#region src/helpers/addons/ultracite-setup.ts
|
|
2281
2576
|
const EDITORS = {
|
|
2282
2577
|
vscode: {
|
|
2283
2578
|
label: "VSCode / Cursor / Windsurf",
|
|
@@ -2384,7 +2679,7 @@ function ensureArrayProperty(obj, name) {
|
|
|
2384
2679
|
}
|
|
2385
2680
|
|
|
2386
2681
|
//#endregion
|
|
2387
|
-
//#region src/helpers/
|
|
2682
|
+
//#region src/helpers/addons/vite-pwa-setup.ts
|
|
2388
2683
|
async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
2389
2684
|
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
|
2390
2685
|
if (!sourceFile) throw new Error("vite config not found");
|
|
@@ -2418,7 +2713,7 @@ async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
|
2418
2713
|
}
|
|
2419
2714
|
|
|
2420
2715
|
//#endregion
|
|
2421
|
-
//#region src/helpers/
|
|
2716
|
+
//#region src/helpers/addons/addons-setup.ts
|
|
2422
2717
|
async function setupAddons(config, isAddCommand = false) {
|
|
2423
2718
|
const { addons, frontend, projectDir, packageManager } = config;
|
|
2424
2719
|
const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
|
|
@@ -2552,7 +2847,7 @@ async function setupOxlint(projectDir, packageManager) {
|
|
|
2552
2847
|
}
|
|
2553
2848
|
|
|
2554
2849
|
//#endregion
|
|
2555
|
-
//#region src/helpers/
|
|
2850
|
+
//#region src/helpers/core/detect-project-config.ts
|
|
2556
2851
|
async function detectProjectConfig(projectDir) {
|
|
2557
2852
|
try {
|
|
2558
2853
|
const btsConfig = await readBtsConfig(projectDir);
|
|
@@ -2570,7 +2865,8 @@ async function detectProjectConfig(projectDir) {
|
|
|
2570
2865
|
packageManager: btsConfig.packageManager,
|
|
2571
2866
|
dbSetup: btsConfig.dbSetup,
|
|
2572
2867
|
api: btsConfig.api,
|
|
2573
|
-
webDeploy: btsConfig.webDeploy
|
|
2868
|
+
webDeploy: btsConfig.webDeploy,
|
|
2869
|
+
serverDeploy: btsConfig.serverDeploy
|
|
2574
2870
|
};
|
|
2575
2871
|
return null;
|
|
2576
2872
|
} catch (_error) {
|
|
@@ -2586,7 +2882,7 @@ async function isBetterTStackProject(projectDir) {
|
|
|
2586
2882
|
}
|
|
2587
2883
|
|
|
2588
2884
|
//#endregion
|
|
2589
|
-
//#region src/helpers/
|
|
2885
|
+
//#region src/helpers/core/install-dependencies.ts
|
|
2590
2886
|
async function installDependencies({ projectDir, packageManager }) {
|
|
2591
2887
|
const s = spinner();
|
|
2592
2888
|
try {
|
|
@@ -2603,7 +2899,7 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
2603
2899
|
}
|
|
2604
2900
|
|
|
2605
2901
|
//#endregion
|
|
2606
|
-
//#region src/helpers/
|
|
2902
|
+
//#region src/helpers/core/add-addons.ts
|
|
2607
2903
|
async function addAddonsToProject(input) {
|
|
2608
2904
|
try {
|
|
2609
2905
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -2628,7 +2924,8 @@ async function addAddonsToProject(input) {
|
|
|
2628
2924
|
install: input.install || false,
|
|
2629
2925
|
dbSetup: detectedConfig.dbSetup || "none",
|
|
2630
2926
|
api: detectedConfig.api || "none",
|
|
2631
|
-
webDeploy: detectedConfig.webDeploy || "none"
|
|
2927
|
+
webDeploy: detectedConfig.webDeploy || "none",
|
|
2928
|
+
serverDeploy: detectedConfig.serverDeploy || "none"
|
|
2632
2929
|
};
|
|
2633
2930
|
for (const addon of input.addons) {
|
|
2634
2931
|
const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
|
|
@@ -2651,12 +2948,92 @@ async function addAddonsToProject(input) {
|
|
|
2651
2948
|
}
|
|
2652
2949
|
|
|
2653
2950
|
//#endregion
|
|
2654
|
-
//#region src/helpers/
|
|
2655
|
-
async function
|
|
2951
|
+
//#region src/helpers/deployment/server-deploy-setup.ts
|
|
2952
|
+
async function setupServerDeploy(config) {
|
|
2953
|
+
const { serverDeploy, webDeploy, projectDir } = config;
|
|
2954
|
+
const { packageManager } = config;
|
|
2955
|
+
if (serverDeploy === "none") return;
|
|
2956
|
+
if (serverDeploy === "alchemy" && webDeploy === "alchemy") return;
|
|
2957
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
2958
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2959
|
+
if (serverDeploy === "wrangler") {
|
|
2960
|
+
await setupWorkersServerDeploy(serverDir, packageManager);
|
|
2961
|
+
await generateCloudflareWorkerTypes({
|
|
2962
|
+
serverDir,
|
|
2963
|
+
packageManager
|
|
2964
|
+
});
|
|
2965
|
+
} else if (serverDeploy === "alchemy") await setupAlchemyServerDeploy(serverDir, packageManager);
|
|
2966
|
+
}
|
|
2967
|
+
async function setupWorkersServerDeploy(serverDir, _packageManager) {
|
|
2968
|
+
const packageJsonPath = path.join(serverDir, "package.json");
|
|
2969
|
+
if (!await fs.pathExists(packageJsonPath)) return;
|
|
2970
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
2971
|
+
packageJson.scripts = {
|
|
2972
|
+
...packageJson.scripts,
|
|
2973
|
+
dev: "wrangler dev --port=3000",
|
|
2974
|
+
start: "wrangler dev",
|
|
2975
|
+
deploy: "wrangler deploy",
|
|
2976
|
+
build: "wrangler deploy --dry-run",
|
|
2977
|
+
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
|
|
2978
|
+
};
|
|
2979
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
2980
|
+
await addPackageDependency({
|
|
2981
|
+
devDependencies: [
|
|
2982
|
+
"wrangler",
|
|
2983
|
+
"@types/node",
|
|
2984
|
+
"@cloudflare/workers-types"
|
|
2985
|
+
],
|
|
2986
|
+
projectDir: serverDir
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
async function generateCloudflareWorkerTypes({ serverDir, packageManager }) {
|
|
2990
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2991
|
+
const s = spinner();
|
|
2992
|
+
try {
|
|
2993
|
+
s.start("Generating Cloudflare Workers types...");
|
|
2994
|
+
const runCmd = packageManager === "npm" ? "npm" : packageManager;
|
|
2995
|
+
await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
|
|
2996
|
+
s.stop("Cloudflare Workers types generated successfully!");
|
|
2997
|
+
} catch {
|
|
2998
|
+
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
|
2999
|
+
const managerCmd = `${packageManager} run`;
|
|
3000
|
+
log.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
async function setupAlchemyServerDeploy(serverDir, _packageManager) {
|
|
3004
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
3005
|
+
await addPackageDependency({
|
|
3006
|
+
devDependencies: [
|
|
3007
|
+
"alchemy",
|
|
3008
|
+
"wrangler",
|
|
3009
|
+
"@types/node",
|
|
3010
|
+
"@cloudflare/workers-types",
|
|
3011
|
+
"dotenv"
|
|
3012
|
+
],
|
|
3013
|
+
projectDir: serverDir
|
|
3014
|
+
});
|
|
3015
|
+
const packageJsonPath = path.join(serverDir, "package.json");
|
|
3016
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
3017
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
3018
|
+
packageJson.scripts = {
|
|
3019
|
+
...packageJson.scripts,
|
|
3020
|
+
dev: "wrangler dev --port=3000",
|
|
3021
|
+
build: "wrangler deploy --dry-run",
|
|
3022
|
+
deploy: "alchemy deploy",
|
|
3023
|
+
destroy: "alchemy destroy",
|
|
3024
|
+
"alchemy:dev": "alchemy dev"
|
|
3025
|
+
};
|
|
3026
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
//#endregion
|
|
3031
|
+
//#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
|
|
3032
|
+
async function setupNextAlchemyDeploy(projectDir, _packageManager) {
|
|
2656
3033
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2657
3034
|
if (!await fs.pathExists(webAppDir)) return;
|
|
2658
3035
|
await addPackageDependency({
|
|
2659
|
-
devDependencies: ["
|
|
3036
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
2660
3037
|
projectDir: webAppDir
|
|
2661
3038
|
});
|
|
2662
3039
|
const pkgPath = path.join(webAppDir, "package.json");
|
|
@@ -2664,43 +3041,518 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
|
2664
3041
|
const pkg = await fs.readJson(pkgPath);
|
|
2665
3042
|
pkg.scripts = {
|
|
2666
3043
|
...pkg.scripts,
|
|
2667
|
-
deploy:
|
|
2668
|
-
|
|
3044
|
+
deploy: "alchemy deploy",
|
|
3045
|
+
destroy: "alchemy destroy",
|
|
3046
|
+
"alchemy:dev": "alchemy dev"
|
|
3047
|
+
};
|
|
3048
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
//#endregion
|
|
3053
|
+
//#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
|
|
3054
|
+
async function setupNuxtAlchemyDeploy(projectDir, _packageManager) {
|
|
3055
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3056
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3057
|
+
await addPackageDependency({
|
|
3058
|
+
devDependencies: [
|
|
3059
|
+
"alchemy",
|
|
3060
|
+
"nitro-cloudflare-dev",
|
|
3061
|
+
"dotenv"
|
|
3062
|
+
],
|
|
3063
|
+
projectDir: webAppDir
|
|
3064
|
+
});
|
|
3065
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3066
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3067
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3068
|
+
pkg.scripts = {
|
|
3069
|
+
...pkg.scripts,
|
|
3070
|
+
deploy: "alchemy deploy",
|
|
3071
|
+
destroy: "alchemy destroy",
|
|
3072
|
+
"alchemy:dev": "alchemy dev"
|
|
2669
3073
|
};
|
|
2670
3074
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2671
3075
|
}
|
|
2672
3076
|
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
2673
3077
|
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
3078
|
+
try {
|
|
3079
|
+
const project = new Project({ manipulationSettings: {
|
|
3080
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3081
|
+
quoteKind: QuoteKind.Double
|
|
3082
|
+
} });
|
|
3083
|
+
project.addSourceFileAtPath(nuxtConfigPath);
|
|
3084
|
+
const sourceFile = project.getSourceFileOrThrow(nuxtConfigPath);
|
|
3085
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3086
|
+
if (!exportAssignment) return;
|
|
3087
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3088
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
|
|
3089
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3090
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3091
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3092
|
+
if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
|
|
3093
|
+
name: "nitro",
|
|
3094
|
+
initializer: `{
|
|
2691
3095
|
preset: "cloudflare_module",
|
|
2692
3096
|
cloudflare: {
|
|
2693
3097
|
deployConfig: true,
|
|
2694
3098
|
nodeCompat: true
|
|
2695
3099
|
}
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3100
|
+
}`
|
|
3101
|
+
});
|
|
3102
|
+
const modulesProperty = configObject.getProperty("modules");
|
|
3103
|
+
if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
|
|
3104
|
+
const initializer = modulesProperty.getInitializer();
|
|
3105
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3106
|
+
const hasModule = initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'");
|
|
3107
|
+
if (!hasModule) initializer.addElement("\"nitro-cloudflare-dev\"");
|
|
3108
|
+
}
|
|
3109
|
+
} else if (!modulesProperty) configObject.addPropertyAssignment({
|
|
3110
|
+
name: "modules",
|
|
3111
|
+
initializer: "[\"nitro-cloudflare-dev\"]"
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
await project.save();
|
|
3115
|
+
} catch (error) {
|
|
3116
|
+
console.warn("Failed to update nuxt.config.ts:", error);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
//#endregion
|
|
3121
|
+
//#region src/helpers/deployment/alchemy/alchemy-react-router-setup.ts
|
|
3122
|
+
async function setupReactRouterAlchemyDeploy(projectDir, _packageManager) {
|
|
3123
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3124
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3125
|
+
await addPackageDependency({
|
|
3126
|
+
devDependencies: [
|
|
3127
|
+
"alchemy",
|
|
3128
|
+
"@cloudflare/vite-plugin",
|
|
3129
|
+
"dotenv"
|
|
3130
|
+
],
|
|
3131
|
+
projectDir: webAppDir
|
|
3132
|
+
});
|
|
3133
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3134
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3135
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3136
|
+
pkg.scripts = {
|
|
3137
|
+
...pkg.scripts,
|
|
3138
|
+
deploy: "alchemy deploy",
|
|
3139
|
+
destroy: "alchemy destroy",
|
|
3140
|
+
"alchemy:dev": "alchemy dev"
|
|
3141
|
+
};
|
|
3142
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3143
|
+
}
|
|
3144
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3145
|
+
if (await fs.pathExists(viteConfigPath)) try {
|
|
3146
|
+
const project = new Project({ manipulationSettings: {
|
|
3147
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3148
|
+
quoteKind: QuoteKind.Double
|
|
3149
|
+
} });
|
|
3150
|
+
project.addSourceFileAtPath(viteConfigPath);
|
|
3151
|
+
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
|
3152
|
+
const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/react-router");
|
|
3153
|
+
if (!alchemyImport) sourceFile.addImportDeclaration({
|
|
3154
|
+
moduleSpecifier: "alchemy/cloudflare/react-router",
|
|
3155
|
+
defaultImport: "alchemy"
|
|
3156
|
+
});
|
|
3157
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3158
|
+
if (!exportAssignment) return;
|
|
3159
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3160
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
|
|
3161
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3162
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3163
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3164
|
+
const pluginsProperty = configObject.getProperty("plugins");
|
|
3165
|
+
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
|
3166
|
+
const initializer = pluginsProperty.getInitializer();
|
|
3167
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3168
|
+
const hasCloudflarePlugin = initializer.getElements().some((el) => el.getText().includes("cloudflare("));
|
|
3169
|
+
if (!hasCloudflarePlugin) initializer.addElement("alchemy()");
|
|
3170
|
+
}
|
|
3171
|
+
} else if (!pluginsProperty) configObject.addPropertyAssignment({
|
|
3172
|
+
name: "plugins",
|
|
3173
|
+
initializer: "[alchemy()]"
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
await project.save();
|
|
3177
|
+
} catch (error) {
|
|
3178
|
+
console.warn("Failed to update vite.config.ts:", error);
|
|
3179
|
+
}
|
|
3180
|
+
const reactRouterConfigPath = path.join(webAppDir, "react-router.config.ts");
|
|
3181
|
+
if (await fs.pathExists(reactRouterConfigPath)) try {
|
|
3182
|
+
const project = new Project({ manipulationSettings: {
|
|
3183
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3184
|
+
quoteKind: QuoteKind.Double
|
|
3185
|
+
} });
|
|
3186
|
+
project.addSourceFileAtPath(reactRouterConfigPath);
|
|
3187
|
+
const sourceFile = project.getSourceFileOrThrow(reactRouterConfigPath);
|
|
3188
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3189
|
+
if (!exportAssignment) return;
|
|
3190
|
+
const configExpression = exportAssignment.getExpression();
|
|
3191
|
+
let configObject;
|
|
3192
|
+
if (Node.isObjectLiteralExpression(configExpression)) configObject = configExpression;
|
|
3193
|
+
else if (Node.isSatisfiesExpression(configExpression)) {
|
|
3194
|
+
const expression = configExpression.getExpression();
|
|
3195
|
+
if (Node.isObjectLiteralExpression(expression)) configObject = expression;
|
|
3196
|
+
}
|
|
3197
|
+
if (!configObject || !Node.isObjectLiteralExpression(configObject)) return;
|
|
3198
|
+
const futureProperty = configObject.getProperty("future");
|
|
3199
|
+
if (!futureProperty) configObject.addPropertyAssignment({
|
|
3200
|
+
name: "future",
|
|
3201
|
+
initializer: `{
|
|
3202
|
+
unstable_viteEnvironmentApi: true,
|
|
3203
|
+
}`
|
|
3204
|
+
});
|
|
3205
|
+
else if (Node.isPropertyAssignment(futureProperty)) {
|
|
3206
|
+
const futureInitializer = futureProperty.getInitializer();
|
|
3207
|
+
if (Node.isObjectLiteralExpression(futureInitializer)) {
|
|
3208
|
+
const viteEnvApiProp = futureInitializer.getProperty("unstable_viteEnvironmentApi");
|
|
3209
|
+
if (!viteEnvApiProp) futureInitializer.addPropertyAssignment({
|
|
3210
|
+
name: "unstable_viteEnvironmentApi",
|
|
3211
|
+
initializer: "true"
|
|
3212
|
+
});
|
|
3213
|
+
else if (Node.isPropertyAssignment(viteEnvApiProp)) {
|
|
3214
|
+
const value = viteEnvApiProp.getInitializer()?.getText();
|
|
3215
|
+
if (value === "false") viteEnvApiProp.setInitializer("true");
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
await project.save();
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
console.warn("Failed to update react-router.config.ts:", error);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
//#endregion
|
|
3226
|
+
//#region src/helpers/deployment/alchemy/alchemy-solid-setup.ts
|
|
3227
|
+
async function setupSolidAlchemyDeploy(projectDir, _packageManager) {
|
|
3228
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3229
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3230
|
+
await addPackageDependency({
|
|
3231
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3232
|
+
projectDir: webAppDir
|
|
3233
|
+
});
|
|
3234
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3235
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3236
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3237
|
+
pkg.scripts = {
|
|
3238
|
+
...pkg.scripts,
|
|
3239
|
+
deploy: "alchemy deploy",
|
|
3240
|
+
destroy: "alchemy destroy",
|
|
3241
|
+
"alchemy:dev": "alchemy dev"
|
|
3242
|
+
};
|
|
3243
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
//#endregion
|
|
3248
|
+
//#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
|
|
3249
|
+
async function setupSvelteAlchemyDeploy(projectDir, _packageManager) {
|
|
3250
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3251
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3252
|
+
await addPackageDependency({
|
|
3253
|
+
devDependencies: [
|
|
3254
|
+
"alchemy",
|
|
3255
|
+
"@sveltejs/adapter-cloudflare",
|
|
3256
|
+
"dotenv"
|
|
3257
|
+
],
|
|
3258
|
+
projectDir: webAppDir
|
|
3259
|
+
});
|
|
3260
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3261
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3262
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3263
|
+
pkg.scripts = {
|
|
3264
|
+
...pkg.scripts,
|
|
3265
|
+
deploy: "alchemy deploy",
|
|
3266
|
+
destroy: "alchemy destroy",
|
|
3267
|
+
"alchemy:dev": "alchemy dev"
|
|
3268
|
+
};
|
|
3269
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3270
|
+
}
|
|
3271
|
+
const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
|
|
3272
|
+
if (!await fs.pathExists(svelteConfigPath)) return;
|
|
3273
|
+
try {
|
|
3274
|
+
const project = new Project({ manipulationSettings: {
|
|
3275
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3276
|
+
quoteKind: QuoteKind.Single
|
|
3277
|
+
} });
|
|
3278
|
+
project.addSourceFileAtPath(svelteConfigPath);
|
|
3279
|
+
const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
|
|
3280
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
3281
|
+
const adapterImport = importDeclarations.find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
|
|
3282
|
+
if (adapterImport) {
|
|
3283
|
+
adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
|
|
3284
|
+
adapterImport.removeDefaultImport();
|
|
3285
|
+
adapterImport.setDefaultImport("alchemy");
|
|
3286
|
+
} else sourceFile.insertImportDeclaration(0, {
|
|
3287
|
+
moduleSpecifier: "alchemy/cloudflare/sveltekit",
|
|
3288
|
+
defaultImport: "alchemy"
|
|
3289
|
+
});
|
|
3290
|
+
const configVariable = sourceFile.getVariableDeclaration("config");
|
|
3291
|
+
if (configVariable) {
|
|
3292
|
+
const initializer = configVariable.getInitializer();
|
|
3293
|
+
if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
|
|
3294
|
+
}
|
|
3295
|
+
await project.save();
|
|
3296
|
+
} catch (error) {
|
|
3297
|
+
console.warn("Failed to update svelte.config.js:", error);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
function updateAdapterInConfig(configObject) {
|
|
3301
|
+
if (!Node.isObjectLiteralExpression(configObject)) return;
|
|
3302
|
+
const kitProperty = configObject.getProperty("kit");
|
|
3303
|
+
if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
|
|
3304
|
+
const kitInitializer = kitProperty.getInitializer();
|
|
3305
|
+
if (Node.isObjectLiteralExpression(kitInitializer)) {
|
|
3306
|
+
const adapterProperty = kitInitializer.getProperty("adapter");
|
|
3307
|
+
if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
|
|
3308
|
+
const initializer = adapterProperty.getInitializer();
|
|
3309
|
+
if (Node.isCallExpression(initializer)) {
|
|
3310
|
+
const expression = initializer.getExpression();
|
|
3311
|
+
if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
//#endregion
|
|
3319
|
+
//#region src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts
|
|
3320
|
+
async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager) {
|
|
3321
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3322
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3323
|
+
await addPackageDependency({
|
|
3324
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3325
|
+
projectDir: webAppDir
|
|
3326
|
+
});
|
|
3327
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3328
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3329
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3330
|
+
pkg.scripts = {
|
|
3331
|
+
...pkg.scripts,
|
|
3332
|
+
deploy: "alchemy deploy",
|
|
3333
|
+
destroy: "alchemy destroy",
|
|
3334
|
+
"alchemy:dev": "alchemy dev"
|
|
3335
|
+
};
|
|
3336
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
//#endregion
|
|
3341
|
+
//#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
|
|
3342
|
+
async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager) {
|
|
3343
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3344
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3345
|
+
await addPackageDependency({
|
|
3346
|
+
devDependencies: [
|
|
3347
|
+
"alchemy",
|
|
3348
|
+
"nitropack",
|
|
3349
|
+
"dotenv"
|
|
3350
|
+
],
|
|
3351
|
+
projectDir: webAppDir
|
|
3352
|
+
});
|
|
3353
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3354
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3355
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3356
|
+
pkg.scripts = {
|
|
3357
|
+
...pkg.scripts,
|
|
3358
|
+
deploy: "alchemy deploy",
|
|
3359
|
+
destroy: "alchemy destroy",
|
|
3360
|
+
"alchemy:dev": "alchemy dev"
|
|
3361
|
+
};
|
|
3362
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3363
|
+
}
|
|
3364
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3365
|
+
if (await fs.pathExists(viteConfigPath)) try {
|
|
3366
|
+
const project = new Project({ manipulationSettings: {
|
|
3367
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3368
|
+
quoteKind: QuoteKind.Double
|
|
3369
|
+
} });
|
|
3370
|
+
project.addSourceFileAtPath(viteConfigPath);
|
|
3371
|
+
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
|
3372
|
+
const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
|
|
3373
|
+
if (!alchemyImport) sourceFile.addImportDeclaration({
|
|
3374
|
+
moduleSpecifier: "alchemy/cloudflare/tanstack-start",
|
|
3375
|
+
defaultImport: "alchemy"
|
|
3376
|
+
});
|
|
3377
|
+
else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
|
|
3378
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3379
|
+
if (!exportAssignment) return;
|
|
3380
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3381
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
|
|
3382
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3383
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3384
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3385
|
+
if (!configObject.getProperty("build")) configObject.addPropertyAssignment({
|
|
3386
|
+
name: "build",
|
|
3387
|
+
initializer: `{
|
|
3388
|
+
target: "esnext",
|
|
3389
|
+
rollupOptions: {
|
|
3390
|
+
external: ["node:async_hooks", "cloudflare:workers"],
|
|
3391
|
+
},
|
|
3392
|
+
}`
|
|
3393
|
+
});
|
|
3394
|
+
const pluginsProperty = configObject.getProperty("plugins");
|
|
3395
|
+
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
|
3396
|
+
const initializer = pluginsProperty.getInitializer();
|
|
3397
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3398
|
+
const hasShim = initializer.getElements().some((el) => el.getText().includes("alchemy"));
|
|
3399
|
+
if (!hasShim) initializer.addElement("alchemy()");
|
|
3400
|
+
const tanstackElements = initializer.getElements().filter((el) => el.getText().includes("tanstackStart"));
|
|
3401
|
+
tanstackElements.forEach((element) => {
|
|
3402
|
+
if (Node.isCallExpression(element)) {
|
|
3403
|
+
const args = element.getArguments();
|
|
3404
|
+
if (args.length === 0) element.addArgument(`{
|
|
3405
|
+
target: "cloudflare-module",
|
|
3406
|
+
customViteReactPlugin: true,
|
|
3407
|
+
}`);
|
|
3408
|
+
else if (args.length === 1 && Node.isObjectLiteralExpression(args[0])) {
|
|
3409
|
+
const configObj = args[0];
|
|
3410
|
+
if (!configObj.getProperty("target")) configObj.addPropertyAssignment({
|
|
3411
|
+
name: "target",
|
|
3412
|
+
initializer: "\"cloudflare-module\""
|
|
3413
|
+
});
|
|
3414
|
+
if (!configObj.getProperty("customViteReactPlugin")) configObj.addPropertyAssignment({
|
|
3415
|
+
name: "customViteReactPlugin",
|
|
3416
|
+
initializer: "true"
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
} else configObject.addPropertyAssignment({
|
|
3423
|
+
name: "plugins",
|
|
3424
|
+
initializer: "[alchemy()]"
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
await project.save();
|
|
3428
|
+
} catch (error) {
|
|
3429
|
+
console.warn("Failed to update vite.config.ts:", error);
|
|
3430
|
+
}
|
|
3431
|
+
const nitroConfigPath = path.join(webAppDir, "nitro.config.ts");
|
|
3432
|
+
const nitroConfigContent = `import { defineNitroConfig } from "nitropack/config";
|
|
3433
|
+
|
|
3434
|
+
export default defineNitroConfig({
|
|
3435
|
+
preset: "cloudflare-module",
|
|
3436
|
+
cloudflare: {
|
|
3437
|
+
nodeCompat: true,
|
|
3438
|
+
},
|
|
3439
|
+
});
|
|
3440
|
+
`;
|
|
3441
|
+
await fs.writeFile(nitroConfigPath, nitroConfigContent, "utf-8");
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
//#endregion
|
|
3445
|
+
//#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
|
|
3446
|
+
async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
|
|
3447
|
+
await addPackageDependency({
|
|
3448
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3449
|
+
projectDir
|
|
3450
|
+
});
|
|
3451
|
+
const rootPkgPath = path.join(projectDir, "package.json");
|
|
3452
|
+
if (await fs.pathExists(rootPkgPath)) {
|
|
3453
|
+
const pkg = await fs.readJson(rootPkgPath);
|
|
3454
|
+
pkg.scripts = {
|
|
3455
|
+
...pkg.scripts,
|
|
3456
|
+
deploy: "alchemy deploy",
|
|
3457
|
+
destroy: "alchemy destroy",
|
|
3458
|
+
"alchemy:dev": "alchemy dev"
|
|
3459
|
+
};
|
|
3460
|
+
await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
|
|
3461
|
+
}
|
|
3462
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3463
|
+
if (await fs.pathExists(serverDir)) await setupAlchemyServerDeploy(serverDir, packageManager);
|
|
3464
|
+
const frontend = config.frontend;
|
|
3465
|
+
const isNext = frontend.includes("next");
|
|
3466
|
+
const isNuxt = frontend.includes("nuxt");
|
|
3467
|
+
const isSvelte = frontend.includes("svelte");
|
|
3468
|
+
const isTanstackRouter = frontend.includes("tanstack-router");
|
|
3469
|
+
const isTanstackStart = frontend.includes("tanstack-start");
|
|
3470
|
+
const isReactRouter = frontend.includes("react-router");
|
|
3471
|
+
const isSolid = frontend.includes("solid");
|
|
3472
|
+
if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
|
|
3473
|
+
else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
|
3474
|
+
else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
|
3475
|
+
else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
|
3476
|
+
else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
|
3477
|
+
else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
|
3478
|
+
else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
//#endregion
|
|
3482
|
+
//#region src/helpers/deployment/workers/workers-next-setup.ts
|
|
3483
|
+
async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
3484
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3485
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3486
|
+
await addPackageDependency({
|
|
3487
|
+
dependencies: ["@opennextjs/cloudflare"],
|
|
3488
|
+
devDependencies: ["wrangler"],
|
|
3489
|
+
projectDir: webAppDir
|
|
3490
|
+
});
|
|
3491
|
+
const packageJsonPath = path.join(webAppDir, "package.json");
|
|
3492
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
3493
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
3494
|
+
pkg.scripts = {
|
|
3495
|
+
...pkg.scripts,
|
|
3496
|
+
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
3497
|
+
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
3498
|
+
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
|
3499
|
+
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
3500
|
+
};
|
|
3501
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
//#endregion
|
|
3506
|
+
//#region src/helpers/deployment/workers/workers-nuxt-setup.ts
|
|
3507
|
+
async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
3508
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3509
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3510
|
+
await addPackageDependency({
|
|
3511
|
+
devDependencies: ["nitro-cloudflare-dev", "wrangler"],
|
|
3512
|
+
projectDir: webAppDir
|
|
3513
|
+
});
|
|
3514
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3515
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3516
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3517
|
+
pkg.scripts = {
|
|
3518
|
+
...pkg.scripts,
|
|
3519
|
+
deploy: `${packageManager} run build && wrangler deploy`,
|
|
3520
|
+
"cf-typegen": "wrangler types"
|
|
3521
|
+
};
|
|
3522
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3523
|
+
}
|
|
3524
|
+
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
3525
|
+
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
3526
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
|
|
3527
|
+
if (!sourceFile) return;
|
|
3528
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
3529
|
+
const expression = expr.getExpression();
|
|
3530
|
+
return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
|
|
3531
|
+
});
|
|
3532
|
+
if (!defineCall) return;
|
|
3533
|
+
const configObj = defineCall.getArguments()[0];
|
|
3534
|
+
if (!configObj) return;
|
|
3535
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3536
|
+
const compatProp = configObj.getProperty("compatibilityDate");
|
|
3537
|
+
if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
|
|
3538
|
+
else configObj.addPropertyAssignment({
|
|
3539
|
+
name: "compatibilityDate",
|
|
3540
|
+
initializer: `'${today}'`
|
|
3541
|
+
});
|
|
3542
|
+
const nitroInitializer = `{
|
|
3543
|
+
preset: "cloudflare_module",
|
|
3544
|
+
cloudflare: {
|
|
3545
|
+
deployConfig: true,
|
|
3546
|
+
nodeCompat: true
|
|
3547
|
+
}
|
|
3548
|
+
}`;
|
|
3549
|
+
const nitroProp = configObj.getProperty("nitro");
|
|
3550
|
+
if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
|
|
3551
|
+
else configObj.addPropertyAssignment({
|
|
3552
|
+
name: "nitro",
|
|
3553
|
+
initializer: nitroInitializer
|
|
3554
|
+
});
|
|
3555
|
+
const modulesProp = configObj.getProperty("modules");
|
|
2704
3556
|
if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
2705
3557
|
const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
|
|
2706
3558
|
if (arrayExpr) {
|
|
@@ -2715,7 +3567,7 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
|
2715
3567
|
}
|
|
2716
3568
|
|
|
2717
3569
|
//#endregion
|
|
2718
|
-
//#region src/helpers/
|
|
3570
|
+
//#region src/helpers/deployment/workers/workers-svelte-setup.ts
|
|
2719
3571
|
async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
2720
3572
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2721
3573
|
if (!await fs.pathExists(webAppDir)) return;
|
|
@@ -2752,7 +3604,7 @@ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
|
2752
3604
|
}
|
|
2753
3605
|
|
|
2754
3606
|
//#endregion
|
|
2755
|
-
//#region src/helpers/
|
|
3607
|
+
//#region src/helpers/deployment/workers/workers-tanstack-start-setup.ts
|
|
2756
3608
|
async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
2757
3609
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2758
3610
|
if (!await fs.pathExists(webAppDir)) return;
|
|
@@ -2790,7 +3642,7 @@ async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
|
2790
3642
|
}
|
|
2791
3643
|
|
|
2792
3644
|
//#endregion
|
|
2793
|
-
//#region src/helpers/
|
|
3645
|
+
//#region src/helpers/deployment/workers/workers-vite-setup.ts
|
|
2794
3646
|
async function setupWorkersVitePlugin(projectDir) {
|
|
2795
3647
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2796
3648
|
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
@@ -2821,12 +3673,16 @@ async function setupWorkersVitePlugin(projectDir) {
|
|
|
2821
3673
|
}
|
|
2822
3674
|
|
|
2823
3675
|
//#endregion
|
|
2824
|
-
//#region src/helpers/
|
|
3676
|
+
//#region src/helpers/deployment/web-deploy-setup.ts
|
|
2825
3677
|
async function setupWebDeploy(config) {
|
|
2826
|
-
const { webDeploy, frontend, projectDir } = config;
|
|
3678
|
+
const { webDeploy, serverDeploy, frontend, projectDir } = config;
|
|
2827
3679
|
const { packageManager } = config;
|
|
2828
3680
|
if (webDeploy === "none") return;
|
|
2829
|
-
if (webDeploy !== "
|
|
3681
|
+
if (webDeploy !== "wrangler" && webDeploy !== "alchemy") return;
|
|
3682
|
+
if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
|
|
3683
|
+
await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
2830
3686
|
const isNext = frontend.includes("next");
|
|
2831
3687
|
const isNuxt = frontend.includes("nuxt");
|
|
2832
3688
|
const isSvelte = frontend.includes("svelte");
|
|
@@ -2834,11 +3690,21 @@ async function setupWebDeploy(config) {
|
|
|
2834
3690
|
const isTanstackStart = frontend.includes("tanstack-start");
|
|
2835
3691
|
const isReactRouter = frontend.includes("react-router");
|
|
2836
3692
|
const isSolid = frontend.includes("solid");
|
|
2837
|
-
if (
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
3693
|
+
if (webDeploy === "wrangler") {
|
|
3694
|
+
if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
|
|
3695
|
+
else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
|
|
3696
|
+
else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
|
|
3697
|
+
else if (isTanstackStart) await setupTanstackStartWorkersDeploy(projectDir, packageManager);
|
|
3698
|
+
else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
|
|
3699
|
+
} else if (webDeploy === "alchemy") {
|
|
3700
|
+
if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
|
|
3701
|
+
else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
|
3702
|
+
else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
|
3703
|
+
else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
|
3704
|
+
else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
|
3705
|
+
else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
|
3706
|
+
else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
|
|
3707
|
+
}
|
|
2842
3708
|
}
|
|
2843
3709
|
async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
2844
3710
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
@@ -2855,30 +3721,9 @@ async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
|
2855
3721
|
}
|
|
2856
3722
|
await setupWorkersVitePlugin(projectDir);
|
|
2857
3723
|
}
|
|
2858
|
-
async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
2859
|
-
const webAppDir = path.join(projectDir, "apps/web");
|
|
2860
|
-
if (!await fs.pathExists(webAppDir)) return;
|
|
2861
|
-
await addPackageDependency({
|
|
2862
|
-
dependencies: ["@opennextjs/cloudflare"],
|
|
2863
|
-
devDependencies: ["wrangler"],
|
|
2864
|
-
projectDir: webAppDir
|
|
2865
|
-
});
|
|
2866
|
-
const packageJsonPath = path.join(webAppDir, "package.json");
|
|
2867
|
-
if (await fs.pathExists(packageJsonPath)) {
|
|
2868
|
-
const pkg = await fs.readJson(packageJsonPath);
|
|
2869
|
-
pkg.scripts = {
|
|
2870
|
-
...pkg.scripts,
|
|
2871
|
-
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
2872
|
-
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
2873
|
-
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
|
2874
|
-
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
2875
|
-
};
|
|
2876
|
-
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
2877
|
-
}
|
|
2878
|
-
}
|
|
2879
3724
|
|
|
2880
3725
|
//#endregion
|
|
2881
|
-
//#region src/helpers/
|
|
3726
|
+
//#region src/helpers/core/add-deployment.ts
|
|
2882
3727
|
async function addDeploymentToProject(input) {
|
|
2883
3728
|
try {
|
|
2884
3729
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -2886,7 +3731,8 @@ async function addDeploymentToProject(input) {
|
|
|
2886
3731
|
if (!isBetterTStack) exitWithError("This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.");
|
|
2887
3732
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
2888
3733
|
if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
2889
|
-
if (detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} deployment is already configured for this project.`);
|
|
3734
|
+
if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} web deployment is already configured for this project.`);
|
|
3735
|
+
if (input.serverDeploy && detectedConfig.serverDeploy === input.serverDeploy) exitWithError(`${input.serverDeploy} server deployment is already configured for this project.`);
|
|
2890
3736
|
const config = {
|
|
2891
3737
|
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
2892
3738
|
projectDir,
|
|
@@ -2903,224 +3749,79 @@ async function addDeploymentToProject(input) {
|
|
|
2903
3749
|
packageManager: input.packageManager || detectedConfig.packageManager || "npm",
|
|
2904
3750
|
install: input.install || false,
|
|
2905
3751
|
dbSetup: detectedConfig.dbSetup || "none",
|
|
2906
|
-
api: detectedConfig.api || "none",
|
|
2907
|
-
webDeploy: input.webDeploy
|
|
2908
|
-
|
|
2909
|
-
log.info(pc.green(`Adding ${input.webDeploy} deployment to ${config.frontend.join("/")}`));
|
|
2910
|
-
await setupDeploymentTemplates(projectDir, config);
|
|
2911
|
-
await setupWebDeploy(config);
|
|
2912
|
-
await updateBtsConfig(projectDir, { webDeploy: input.webDeploy });
|
|
2913
|
-
if (config.install) await installDependencies({
|
|
2914
|
-
projectDir,
|
|
2915
|
-
packageManager: config.packageManager
|
|
2916
|
-
});
|
|
2917
|
-
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
2918
|
-
} catch (error) {
|
|
2919
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2920
|
-
exitWithError(`Error adding deployment: ${message}`);
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
//#endregion
|
|
2925
|
-
//#region src/helpers/setup/api-setup.ts
|
|
2926
|
-
async function setupApi(config) {
|
|
2927
|
-
const { api, projectName, frontend, backend, packageManager, projectDir } = config;
|
|
2928
|
-
const isConvex = backend === "convex";
|
|
2929
|
-
const webDir = path.join(projectDir, "apps/web");
|
|
2930
|
-
const nativeDir = path.join(projectDir, "apps/native");
|
|
2931
|
-
const webDirExists = await fs.pathExists(webDir);
|
|
2932
|
-
const nativeDirExists = await fs.pathExists(nativeDir);
|
|
2933
|
-
const hasReactWeb = frontend.some((f) => [
|
|
2934
|
-
"tanstack-router",
|
|
2935
|
-
"react-router",
|
|
2936
|
-
"tanstack-start",
|
|
2937
|
-
"next"
|
|
2938
|
-
].includes(f));
|
|
2939
|
-
const hasNuxtWeb = frontend.includes("nuxt");
|
|
2940
|
-
const hasSvelteWeb = frontend.includes("svelte");
|
|
2941
|
-
const hasSolidWeb = frontend.includes("solid");
|
|
2942
|
-
if (!isConvex && api !== "none") {
|
|
2943
|
-
const serverDir = path.join(projectDir, "apps/server");
|
|
2944
|
-
const serverDirExists = await fs.pathExists(serverDir);
|
|
2945
|
-
if (serverDirExists) {
|
|
2946
|
-
if (api === "orpc") await addPackageDependency({
|
|
2947
|
-
dependencies: ["@orpc/server", "@orpc/client"],
|
|
2948
|
-
projectDir: serverDir
|
|
2949
|
-
});
|
|
2950
|
-
else if (api === "trpc") {
|
|
2951
|
-
await addPackageDependency({
|
|
2952
|
-
dependencies: ["@trpc/server", "@trpc/client"],
|
|
2953
|
-
projectDir: serverDir
|
|
2954
|
-
});
|
|
2955
|
-
if (config.backend === "hono") await addPackageDependency({
|
|
2956
|
-
dependencies: ["@hono/trpc-server"],
|
|
2957
|
-
projectDir: serverDir
|
|
2958
|
-
});
|
|
2959
|
-
else if (config.backend === "elysia") await addPackageDependency({
|
|
2960
|
-
dependencies: ["@elysiajs/trpc"],
|
|
2961
|
-
projectDir: serverDir
|
|
2962
|
-
});
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
if (webDirExists) {
|
|
2966
|
-
if (hasReactWeb) {
|
|
2967
|
-
if (api === "orpc") await addPackageDependency({
|
|
2968
|
-
dependencies: ["@orpc/tanstack-query", "@orpc/client"],
|
|
2969
|
-
projectDir: webDir
|
|
2970
|
-
});
|
|
2971
|
-
else if (api === "trpc") await addPackageDependency({
|
|
2972
|
-
dependencies: [
|
|
2973
|
-
"@trpc/tanstack-react-query",
|
|
2974
|
-
"@trpc/client",
|
|
2975
|
-
"@trpc/server"
|
|
2976
|
-
],
|
|
2977
|
-
projectDir: webDir
|
|
2978
|
-
});
|
|
2979
|
-
} else if (hasNuxtWeb) {
|
|
2980
|
-
if (api === "orpc") await addPackageDependency({
|
|
2981
|
-
dependencies: [
|
|
2982
|
-
"@tanstack/vue-query",
|
|
2983
|
-
"@tanstack/vue-query-devtools",
|
|
2984
|
-
"@orpc/tanstack-query",
|
|
2985
|
-
"@orpc/client"
|
|
2986
|
-
],
|
|
2987
|
-
projectDir: webDir
|
|
2988
|
-
});
|
|
2989
|
-
} else if (hasSvelteWeb) {
|
|
2990
|
-
if (api === "orpc") await addPackageDependency({
|
|
2991
|
-
dependencies: [
|
|
2992
|
-
"@orpc/tanstack-query",
|
|
2993
|
-
"@orpc/client",
|
|
2994
|
-
"@tanstack/svelte-query"
|
|
2995
|
-
],
|
|
2996
|
-
projectDir: webDir
|
|
2997
|
-
});
|
|
2998
|
-
} else if (hasSolidWeb) {
|
|
2999
|
-
if (api === "orpc") await addPackageDependency({
|
|
3000
|
-
dependencies: [
|
|
3001
|
-
"@orpc/tanstack-query",
|
|
3002
|
-
"@orpc/client",
|
|
3003
|
-
"@tanstack/solid-query"
|
|
3004
|
-
],
|
|
3005
|
-
projectDir: webDir
|
|
3006
|
-
});
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
if (nativeDirExists) {
|
|
3010
|
-
if (api === "trpc") await addPackageDependency({
|
|
3011
|
-
dependencies: [
|
|
3012
|
-
"@trpc/tanstack-react-query",
|
|
3013
|
-
"@trpc/client",
|
|
3014
|
-
"@trpc/server"
|
|
3015
|
-
],
|
|
3016
|
-
projectDir: nativeDir
|
|
3017
|
-
});
|
|
3018
|
-
else if (api === "orpc") await addPackageDependency({
|
|
3019
|
-
dependencies: ["@orpc/tanstack-query", "@orpc/client"],
|
|
3020
|
-
projectDir: nativeDir
|
|
3021
|
-
});
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
const reactBasedFrontends = [
|
|
3025
|
-
"react-router",
|
|
3026
|
-
"tanstack-router",
|
|
3027
|
-
"tanstack-start",
|
|
3028
|
-
"next",
|
|
3029
|
-
"native-nativewind",
|
|
3030
|
-
"native-unistyles"
|
|
3031
|
-
];
|
|
3032
|
-
const needsSolidQuery = frontend.includes("solid");
|
|
3033
|
-
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
|
3034
|
-
if (needsReactQuery && !isConvex) {
|
|
3035
|
-
const reactQueryDeps = ["@tanstack/react-query"];
|
|
3036
|
-
const reactQueryDevDeps = ["@tanstack/react-query-devtools"];
|
|
3037
|
-
const hasReactWeb$1 = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
|
|
3038
|
-
const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3039
|
-
if (hasReactWeb$1 && webDirExists) {
|
|
3040
|
-
const webPkgJsonPath = path.join(webDir, "package.json");
|
|
3041
|
-
if (await fs.pathExists(webPkgJsonPath)) try {
|
|
3042
|
-
await addPackageDependency({
|
|
3043
|
-
dependencies: reactQueryDeps,
|
|
3044
|
-
devDependencies: reactQueryDevDeps,
|
|
3045
|
-
projectDir: webDir
|
|
3046
|
-
});
|
|
3047
|
-
} catch (_error) {}
|
|
3048
|
-
}
|
|
3049
|
-
if (hasNative && nativeDirExists) {
|
|
3050
|
-
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
|
3051
|
-
if (await fs.pathExists(nativePkgJsonPath)) try {
|
|
3052
|
-
await addPackageDependency({
|
|
3053
|
-
dependencies: reactQueryDeps,
|
|
3054
|
-
projectDir: nativeDir
|
|
3055
|
-
});
|
|
3056
|
-
} catch (_error) {}
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
if (needsSolidQuery && !isConvex) {
|
|
3060
|
-
const solidQueryDeps = ["@tanstack/solid-query"];
|
|
3061
|
-
const solidQueryDevDeps = ["@tanstack/solid-query-devtools"];
|
|
3062
|
-
if (webDirExists) {
|
|
3063
|
-
const webPkgJsonPath = path.join(webDir, "package.json");
|
|
3064
|
-
if (await fs.pathExists(webPkgJsonPath)) try {
|
|
3065
|
-
await addPackageDependency({
|
|
3066
|
-
dependencies: solidQueryDeps,
|
|
3067
|
-
devDependencies: solidQueryDevDeps,
|
|
3068
|
-
projectDir: webDir
|
|
3069
|
-
});
|
|
3070
|
-
} catch (_error) {}
|
|
3071
|
-
}
|
|
3072
|
-
}
|
|
3073
|
-
if (isConvex) {
|
|
3074
|
-
if (webDirExists) {
|
|
3075
|
-
const webPkgJsonPath = path.join(webDir, "package.json");
|
|
3076
|
-
if (await fs.pathExists(webPkgJsonPath)) try {
|
|
3077
|
-
const webDepsToAdd = ["convex"];
|
|
3078
|
-
if (frontend.includes("tanstack-start")) webDepsToAdd.push("@convex-dev/react-query");
|
|
3079
|
-
if (hasSvelteWeb) webDepsToAdd.push("convex-svelte");
|
|
3080
|
-
if (hasNuxtWeb) {
|
|
3081
|
-
webDepsToAdd.push("convex-nuxt");
|
|
3082
|
-
webDepsToAdd.push("convex-vue");
|
|
3083
|
-
}
|
|
3084
|
-
await addPackageDependency({
|
|
3085
|
-
dependencies: webDepsToAdd,
|
|
3086
|
-
projectDir: webDir
|
|
3087
|
-
});
|
|
3088
|
-
} catch (_error) {}
|
|
3089
|
-
}
|
|
3090
|
-
if (nativeDirExists) {
|
|
3091
|
-
const nativePkgJsonPath = path.join(nativeDir, "package.json");
|
|
3092
|
-
if (await fs.pathExists(nativePkgJsonPath)) try {
|
|
3093
|
-
await addPackageDependency({
|
|
3094
|
-
dependencies: ["convex"],
|
|
3095
|
-
projectDir: nativeDir
|
|
3096
|
-
});
|
|
3097
|
-
} catch (_error) {}
|
|
3098
|
-
}
|
|
3099
|
-
const backendPackageName = `@${projectName}/backend`;
|
|
3100
|
-
const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
|
|
3101
|
-
const addWorkspaceDepManually = async (pkgJsonPath, depName, depVersion) => {
|
|
3102
|
-
try {
|
|
3103
|
-
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
3104
|
-
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
3105
|
-
if (pkgJson.dependencies[depName] !== depVersion) {
|
|
3106
|
-
pkgJson.dependencies[depName] = depVersion;
|
|
3107
|
-
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
3108
|
-
}
|
|
3109
|
-
} catch (_error) {}
|
|
3752
|
+
api: detectedConfig.api || "none",
|
|
3753
|
+
webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
|
|
3754
|
+
serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none"
|
|
3110
3755
|
};
|
|
3111
|
-
if (
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3756
|
+
if (input.webDeploy && input.webDeploy !== "none") log.info(pc.green(`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`));
|
|
3757
|
+
if (input.serverDeploy && input.serverDeploy !== "none") log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
|
|
3758
|
+
await setupDeploymentTemplates(projectDir, config);
|
|
3759
|
+
await setupWebDeploy(config);
|
|
3760
|
+
await setupServerDeploy(config);
|
|
3761
|
+
await updateBtsConfig(projectDir, {
|
|
3762
|
+
webDeploy: input.webDeploy || config.webDeploy,
|
|
3763
|
+
serverDeploy: input.serverDeploy || config.serverDeploy
|
|
3764
|
+
});
|
|
3765
|
+
if (config.install) await installDependencies({
|
|
3766
|
+
projectDir,
|
|
3767
|
+
packageManager: config.packageManager
|
|
3768
|
+
});
|
|
3769
|
+
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
3770
|
+
} catch (error) {
|
|
3771
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3772
|
+
exitWithError(`Error adding deployment: ${message}`);
|
|
3119
3773
|
}
|
|
3120
3774
|
}
|
|
3121
3775
|
|
|
3122
3776
|
//#endregion
|
|
3123
|
-
//#region src/
|
|
3777
|
+
//#region src/utils/format-with-biome.ts
|
|
3778
|
+
async function formatProjectWithBiome(projectDir) {
|
|
3779
|
+
const biome = new Biome();
|
|
3780
|
+
const { projectKey } = biome.openProject(projectDir);
|
|
3781
|
+
biome.applyConfiguration(projectKey, {
|
|
3782
|
+
formatter: {
|
|
3783
|
+
enabled: true,
|
|
3784
|
+
indentStyle: "tab"
|
|
3785
|
+
},
|
|
3786
|
+
javascript: { formatter: { quoteStyle: "double" } }
|
|
3787
|
+
});
|
|
3788
|
+
const files = await glob("**/*", {
|
|
3789
|
+
cwd: projectDir,
|
|
3790
|
+
dot: true,
|
|
3791
|
+
absolute: true,
|
|
3792
|
+
onlyFiles: true
|
|
3793
|
+
});
|
|
3794
|
+
for (const filePath of files) try {
|
|
3795
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
3796
|
+
const supported = new Set([
|
|
3797
|
+
".ts",
|
|
3798
|
+
".tsx",
|
|
3799
|
+
".js",
|
|
3800
|
+
".jsx",
|
|
3801
|
+
".cjs",
|
|
3802
|
+
".mjs",
|
|
3803
|
+
".cts",
|
|
3804
|
+
".mts",
|
|
3805
|
+
".json",
|
|
3806
|
+
".jsonc",
|
|
3807
|
+
".md",
|
|
3808
|
+
".mdx",
|
|
3809
|
+
".css",
|
|
3810
|
+
".scss",
|
|
3811
|
+
".html"
|
|
3812
|
+
]);
|
|
3813
|
+
if (!supported.has(ext)) continue;
|
|
3814
|
+
const original = await fs.readFile(filePath, "utf8");
|
|
3815
|
+
const result = biome.formatContent(projectKey, original, { filePath });
|
|
3816
|
+
const content = result?.content;
|
|
3817
|
+
if (typeof content !== "string") continue;
|
|
3818
|
+
if (content.length === 0 && original.length > 0) continue;
|
|
3819
|
+
if (content !== original) await fs.writeFile(filePath, content);
|
|
3820
|
+
} catch {}
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
//#endregion
|
|
3824
|
+
//#region src/helpers/addons/auth-setup.ts
|
|
3124
3825
|
async function setupAuth(config) {
|
|
3125
3826
|
const { auth, frontend, backend, projectDir } = config;
|
|
3126
3827
|
if (backend === "convex" || !auth) return;
|
|
@@ -3172,7 +3873,217 @@ function generateAuthSecret(length = 32) {
|
|
|
3172
3873
|
}
|
|
3173
3874
|
|
|
3174
3875
|
//#endregion
|
|
3175
|
-
//#region src/helpers/
|
|
3876
|
+
//#region src/helpers/addons/examples-setup.ts
|
|
3877
|
+
async function setupExamples(config) {
|
|
3878
|
+
const { examples, frontend, backend, projectDir } = config;
|
|
3879
|
+
if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
|
|
3880
|
+
if (examples.includes("ai")) {
|
|
3881
|
+
const webClientDir = path.join(projectDir, "apps/web");
|
|
3882
|
+
const nativeClientDir = path.join(projectDir, "apps/native");
|
|
3883
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3884
|
+
const webClientDirExists = await fs.pathExists(webClientDir);
|
|
3885
|
+
const nativeClientDirExists = await fs.pathExists(nativeClientDir);
|
|
3886
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
3887
|
+
const hasNuxt = frontend.includes("nuxt");
|
|
3888
|
+
const hasSvelte = frontend.includes("svelte");
|
|
3889
|
+
const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
|
|
3890
|
+
const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3891
|
+
if (webClientDirExists) {
|
|
3892
|
+
const dependencies = ["ai"];
|
|
3893
|
+
if (hasNuxt) dependencies.push("@ai-sdk/vue");
|
|
3894
|
+
else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
|
|
3895
|
+
else if (hasReactWeb) dependencies.push("@ai-sdk/react");
|
|
3896
|
+
await addPackageDependency({
|
|
3897
|
+
dependencies,
|
|
3898
|
+
projectDir: webClientDir
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
if (nativeClientDirExists && hasReactNative) await addPackageDependency({
|
|
3902
|
+
dependencies: ["ai", "@ai-sdk/react"],
|
|
3903
|
+
projectDir: nativeClientDir
|
|
3904
|
+
});
|
|
3905
|
+
if (serverDirExists && backend !== "none") await addPackageDependency({
|
|
3906
|
+
dependencies: ["ai", "@ai-sdk/google"],
|
|
3907
|
+
projectDir: serverDir
|
|
3908
|
+
});
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
//#endregion
|
|
3913
|
+
//#region src/helpers/core/api-setup.ts
|
|
3914
|
+
async function addBackendWorkspaceDependency(projectDir, backendPackageName, workspaceVersion) {
|
|
3915
|
+
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
3916
|
+
try {
|
|
3917
|
+
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
3918
|
+
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
3919
|
+
pkgJson.dependencies[backendPackageName] = workspaceVersion;
|
|
3920
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
3921
|
+
} catch (_error) {}
|
|
3922
|
+
}
|
|
3923
|
+
function getFrontendType(frontend) {
|
|
3924
|
+
const reactBasedFrontends = [
|
|
3925
|
+
"tanstack-router",
|
|
3926
|
+
"react-router",
|
|
3927
|
+
"tanstack-start",
|
|
3928
|
+
"next"
|
|
3929
|
+
];
|
|
3930
|
+
const nativeFrontends = ["native-nativewind", "native-unistyles"];
|
|
3931
|
+
return {
|
|
3932
|
+
hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
|
|
3933
|
+
hasNuxtWeb: frontend.includes("nuxt"),
|
|
3934
|
+
hasSvelteWeb: frontend.includes("svelte"),
|
|
3935
|
+
hasSolidWeb: frontend.includes("solid"),
|
|
3936
|
+
hasNative: frontend.some((f) => nativeFrontends.includes(f))
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
function getApiDependencies(api, frontendType) {
|
|
3940
|
+
const deps = {};
|
|
3941
|
+
if (api === "orpc") deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
|
|
3942
|
+
else if (api === "trpc") deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
|
|
3943
|
+
if (frontendType.hasReactWeb) {
|
|
3944
|
+
if (api === "orpc") deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
|
3945
|
+
else if (api === "trpc") deps.web = { dependencies: [
|
|
3946
|
+
"@trpc/tanstack-react-query",
|
|
3947
|
+
"@trpc/client",
|
|
3948
|
+
"@trpc/server"
|
|
3949
|
+
] };
|
|
3950
|
+
} else if (frontendType.hasNuxtWeb && api === "orpc") deps.web = {
|
|
3951
|
+
dependencies: [
|
|
3952
|
+
"@tanstack/vue-query",
|
|
3953
|
+
"@orpc/tanstack-query",
|
|
3954
|
+
"@orpc/client"
|
|
3955
|
+
],
|
|
3956
|
+
devDependencies: ["@tanstack/vue-query-devtools"]
|
|
3957
|
+
};
|
|
3958
|
+
else if (frontendType.hasSvelteWeb && api === "orpc") deps.web = {
|
|
3959
|
+
dependencies: [
|
|
3960
|
+
"@orpc/tanstack-query",
|
|
3961
|
+
"@orpc/client",
|
|
3962
|
+
"@tanstack/svelte-query"
|
|
3963
|
+
],
|
|
3964
|
+
devDependencies: ["@tanstack/svelte-query-devtools"]
|
|
3965
|
+
};
|
|
3966
|
+
else if (frontendType.hasSolidWeb && api === "orpc") deps.web = {
|
|
3967
|
+
dependencies: [
|
|
3968
|
+
"@orpc/tanstack-query",
|
|
3969
|
+
"@orpc/client",
|
|
3970
|
+
"@tanstack/solid-query"
|
|
3971
|
+
],
|
|
3972
|
+
devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
|
|
3973
|
+
};
|
|
3974
|
+
if (api === "trpc") deps.native = { dependencies: [
|
|
3975
|
+
"@trpc/tanstack-react-query",
|
|
3976
|
+
"@trpc/client",
|
|
3977
|
+
"@trpc/server"
|
|
3978
|
+
] };
|
|
3979
|
+
else if (api === "orpc") deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
|
3980
|
+
return deps;
|
|
3981
|
+
}
|
|
3982
|
+
function getQueryDependencies(frontend) {
|
|
3983
|
+
const reactBasedFrontends = [
|
|
3984
|
+
"react-router",
|
|
3985
|
+
"tanstack-router",
|
|
3986
|
+
"tanstack-start",
|
|
3987
|
+
"next",
|
|
3988
|
+
"native-nativewind",
|
|
3989
|
+
"native-unistyles"
|
|
3990
|
+
];
|
|
3991
|
+
const deps = {};
|
|
3992
|
+
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
|
3993
|
+
if (needsReactQuery) {
|
|
3994
|
+
const hasReactWeb = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
|
|
3995
|
+
const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3996
|
+
if (hasReactWeb) deps.web = {
|
|
3997
|
+
dependencies: ["@tanstack/react-query"],
|
|
3998
|
+
devDependencies: ["@tanstack/react-query-devtools"]
|
|
3999
|
+
};
|
|
4000
|
+
if (hasNative) deps.native = { dependencies: ["@tanstack/react-query"] };
|
|
4001
|
+
}
|
|
4002
|
+
if (frontend.includes("solid")) deps.web = {
|
|
4003
|
+
dependencies: ["@tanstack/solid-query"],
|
|
4004
|
+
devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
|
|
4005
|
+
};
|
|
4006
|
+
return deps;
|
|
4007
|
+
}
|
|
4008
|
+
function getConvexDependencies(frontend) {
|
|
4009
|
+
const deps = {
|
|
4010
|
+
web: { dependencies: ["convex"] },
|
|
4011
|
+
native: { dependencies: ["convex"] }
|
|
4012
|
+
};
|
|
4013
|
+
if (frontend.includes("tanstack-start")) deps.web.dependencies.push("@convex-dev/react-query");
|
|
4014
|
+
if (frontend.includes("svelte")) deps.web.dependencies.push("convex-svelte");
|
|
4015
|
+
if (frontend.includes("nuxt")) deps.web.dependencies.push("convex-nuxt", "convex-vue");
|
|
4016
|
+
return deps;
|
|
4017
|
+
}
|
|
4018
|
+
async function setupApi(config) {
|
|
4019
|
+
const { api, projectName, frontend, backend, packageManager, projectDir } = config;
|
|
4020
|
+
const isConvex = backend === "convex";
|
|
4021
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
4022
|
+
const nativeDir = path.join(projectDir, "apps/native");
|
|
4023
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
4024
|
+
const webDirExists = await fs.pathExists(webDir);
|
|
4025
|
+
const nativeDirExists = await fs.pathExists(nativeDir);
|
|
4026
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
4027
|
+
const frontendType = getFrontendType(frontend);
|
|
4028
|
+
if (!isConvex && api !== "none") {
|
|
4029
|
+
const apiDeps = getApiDependencies(api, frontendType);
|
|
4030
|
+
if (serverDirExists && apiDeps.server) {
|
|
4031
|
+
await addPackageDependency({
|
|
4032
|
+
dependencies: apiDeps.server.dependencies,
|
|
4033
|
+
projectDir: serverDir
|
|
4034
|
+
});
|
|
4035
|
+
if (api === "trpc") {
|
|
4036
|
+
if (backend === "hono") await addPackageDependency({
|
|
4037
|
+
dependencies: ["@hono/trpc-server"],
|
|
4038
|
+
projectDir: serverDir
|
|
4039
|
+
});
|
|
4040
|
+
else if (backend === "elysia") await addPackageDependency({
|
|
4041
|
+
dependencies: ["@elysiajs/trpc"],
|
|
4042
|
+
projectDir: serverDir
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
if (webDirExists && apiDeps.web) await addPackageDependency({
|
|
4047
|
+
dependencies: apiDeps.web.dependencies,
|
|
4048
|
+
devDependencies: apiDeps.web.devDependencies,
|
|
4049
|
+
projectDir: webDir
|
|
4050
|
+
});
|
|
4051
|
+
if (nativeDirExists && apiDeps.native) await addPackageDependency({
|
|
4052
|
+
dependencies: apiDeps.native.dependencies,
|
|
4053
|
+
projectDir: nativeDir
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
4056
|
+
if (!isConvex) {
|
|
4057
|
+
const queryDeps = getQueryDependencies(frontend);
|
|
4058
|
+
if (webDirExists && queryDeps.web) await addPackageDependency({
|
|
4059
|
+
dependencies: queryDeps.web.dependencies,
|
|
4060
|
+
devDependencies: queryDeps.web.devDependencies,
|
|
4061
|
+
projectDir: webDir
|
|
4062
|
+
});
|
|
4063
|
+
if (nativeDirExists && queryDeps.native) await addPackageDependency({
|
|
4064
|
+
dependencies: queryDeps.native.dependencies,
|
|
4065
|
+
projectDir: nativeDir
|
|
4066
|
+
});
|
|
4067
|
+
}
|
|
4068
|
+
if (isConvex) {
|
|
4069
|
+
const convexDeps = getConvexDependencies(frontend);
|
|
4070
|
+
if (webDirExists) await addPackageDependency({
|
|
4071
|
+
dependencies: convexDeps.web.dependencies,
|
|
4072
|
+
projectDir: webDir
|
|
4073
|
+
});
|
|
4074
|
+
if (nativeDirExists) await addPackageDependency({
|
|
4075
|
+
dependencies: convexDeps.native.dependencies,
|
|
4076
|
+
projectDir: nativeDir
|
|
4077
|
+
});
|
|
4078
|
+
const backendPackageName = `@${projectName}/backend`;
|
|
4079
|
+
const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
|
|
4080
|
+
if (webDirExists) await addBackendWorkspaceDependency(webDir, backendPackageName, backendWorkspaceVersion);
|
|
4081
|
+
if (nativeDirExists) await addBackendWorkspaceDependency(nativeDir, backendPackageName, backendWorkspaceVersion);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
//#endregion
|
|
4086
|
+
//#region src/helpers/core/backend-setup.ts
|
|
3176
4087
|
async function setupBackendDependencies(config) {
|
|
3177
4088
|
const { backend, runtime, api, projectDir } = config;
|
|
3178
4089
|
if (backend === "convex") return;
|
|
@@ -3211,7 +4122,7 @@ async function setupBackendDependencies(config) {
|
|
|
3211
4122
|
}
|
|
3212
4123
|
|
|
3213
4124
|
//#endregion
|
|
3214
|
-
//#region src/helpers/
|
|
4125
|
+
//#region src/helpers/core/env-setup.ts
|
|
3215
4126
|
async function addEnvVariablesToFile(filePath, variables) {
|
|
3216
4127
|
await fs.ensureDir(path.dirname(filePath));
|
|
3217
4128
|
let envContent = "";
|
|
@@ -3259,7 +4170,7 @@ async function addEnvVariablesToFile(filePath, variables) {
|
|
|
3259
4170
|
if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
|
|
3260
4171
|
}
|
|
3261
4172
|
async function setupEnvironmentVariables(config) {
|
|
3262
|
-
const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
|
|
4173
|
+
const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
|
|
3263
4174
|
const hasReactRouter = frontend.includes("react-router");
|
|
3264
4175
|
const hasTanStackRouter = frontend.includes("tanstack-router");
|
|
3265
4176
|
const hasTanStackStart = frontend.includes("tanstack-start");
|
|
@@ -3359,39 +4270,69 @@ async function setupEnvironmentVariables(config) {
|
|
|
3359
4270
|
}
|
|
3360
4271
|
];
|
|
3361
4272
|
await addEnvVariablesToFile(envPath, serverVars);
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
4273
|
+
const isUnifiedAlchemy = webDeploy === "alchemy" && serverDeploy === "alchemy";
|
|
4274
|
+
const isIndividualAlchemy = webDeploy === "alchemy" || serverDeploy === "alchemy";
|
|
4275
|
+
if (isUnifiedAlchemy) {
|
|
4276
|
+
const rootEnvPath = path.join(projectDir, ".env");
|
|
4277
|
+
const rootAlchemyVars = [{
|
|
4278
|
+
key: "ALCHEMY_PASSWORD",
|
|
4279
|
+
value: "please-change-this",
|
|
4280
|
+
condition: true
|
|
4281
|
+
}];
|
|
4282
|
+
await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
|
|
4283
|
+
} else if (isIndividualAlchemy) {
|
|
4284
|
+
if (webDeploy === "alchemy") {
|
|
4285
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
4286
|
+
if (await fs.pathExists(webDir)) {
|
|
4287
|
+
const webAlchemyVars = [{
|
|
4288
|
+
key: "ALCHEMY_PASSWORD",
|
|
4289
|
+
value: "please-change-this",
|
|
4290
|
+
condition: true
|
|
4291
|
+
}];
|
|
4292
|
+
await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
if (serverDeploy === "alchemy") {
|
|
4296
|
+
const serverDir$1 = path.join(projectDir, "apps/server");
|
|
4297
|
+
if (await fs.pathExists(serverDir$1)) {
|
|
4298
|
+
const serverAlchemyVars = [{
|
|
4299
|
+
key: "ALCHEMY_PASSWORD",
|
|
4300
|
+
value: "please-change-this",
|
|
4301
|
+
condition: true
|
|
4302
|
+
}];
|
|
4303
|
+
await addEnvVariablesToFile(path.join(serverDir$1, ".env"), serverAlchemyVars);
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
3367
4306
|
}
|
|
3368
4307
|
}
|
|
3369
4308
|
|
|
3370
4309
|
//#endregion
|
|
3371
4310
|
//#region src/helpers/database-providers/d1-setup.ts
|
|
3372
4311
|
async function setupCloudflareD1(config) {
|
|
3373
|
-
const { projectDir } = config;
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
4312
|
+
const { projectDir, serverDeploy } = config;
|
|
4313
|
+
if (serverDeploy === "wrangler") {
|
|
4314
|
+
const envPath = path.join(projectDir, "apps/server", ".env");
|
|
4315
|
+
const variables = [
|
|
4316
|
+
{
|
|
4317
|
+
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
4318
|
+
value: "",
|
|
4319
|
+
condition: true
|
|
4320
|
+
},
|
|
4321
|
+
{
|
|
4322
|
+
key: "CLOUDFLARE_DATABASE_ID",
|
|
4323
|
+
value: "",
|
|
4324
|
+
condition: true
|
|
4325
|
+
},
|
|
4326
|
+
{
|
|
4327
|
+
key: "CLOUDFLARE_D1_TOKEN",
|
|
4328
|
+
value: "",
|
|
4329
|
+
condition: true
|
|
4330
|
+
}
|
|
4331
|
+
];
|
|
4332
|
+
try {
|
|
4333
|
+
await addEnvVariablesToFile(envPath, variables);
|
|
4334
|
+
} catch (_err) {}
|
|
4335
|
+
}
|
|
3395
4336
|
}
|
|
3396
4337
|
|
|
3397
4338
|
//#endregion
|
|
@@ -3839,8 +4780,10 @@ async function setupPrismaPostgres(config) {
|
|
|
3839
4780
|
else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
|
|
3840
4781
|
if (prismaConfig) {
|
|
3841
4782
|
await writeEnvFile$1(projectDir, prismaConfig);
|
|
3842
|
-
|
|
3843
|
-
|
|
4783
|
+
if (orm === "prisma") {
|
|
4784
|
+
await addDotenvImportToPrismaConfig(projectDir);
|
|
4785
|
+
await addPrismaAccelerateExtension(serverDir);
|
|
4786
|
+
}
|
|
3844
4787
|
log.success(pc.green("Prisma Postgres database configured successfully!"));
|
|
3845
4788
|
} else {
|
|
3846
4789
|
await writeEnvFile$1(projectDir);
|
|
@@ -4185,7 +5128,7 @@ async function setupTurso(config) {
|
|
|
4185
5128
|
}
|
|
4186
5129
|
|
|
4187
5130
|
//#endregion
|
|
4188
|
-
//#region src/helpers/
|
|
5131
|
+
//#region src/helpers/core/db-setup.ts
|
|
4189
5132
|
async function setupDatabase(config) {
|
|
4190
5133
|
const { database, orm, dbSetup, backend, projectDir } = config;
|
|
4191
5134
|
if (backend === "convex" || database === "none") {
|
|
@@ -4250,44 +5193,7 @@ async function setupDatabase(config) {
|
|
|
4250
5193
|
}
|
|
4251
5194
|
|
|
4252
5195
|
//#endregion
|
|
4253
|
-
//#region src/helpers/
|
|
4254
|
-
async function setupExamples(config) {
|
|
4255
|
-
const { examples, frontend, backend, projectDir } = config;
|
|
4256
|
-
if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
|
|
4257
|
-
if (examples.includes("ai")) {
|
|
4258
|
-
const webClientDir = path.join(projectDir, "apps/web");
|
|
4259
|
-
const nativeClientDir = path.join(projectDir, "apps/native");
|
|
4260
|
-
const serverDir = path.join(projectDir, "apps/server");
|
|
4261
|
-
const webClientDirExists = await fs.pathExists(webClientDir);
|
|
4262
|
-
const nativeClientDirExists = await fs.pathExists(nativeClientDir);
|
|
4263
|
-
const serverDirExists = await fs.pathExists(serverDir);
|
|
4264
|
-
const hasNuxt = frontend.includes("nuxt");
|
|
4265
|
-
const hasSvelte = frontend.includes("svelte");
|
|
4266
|
-
const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
|
|
4267
|
-
const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
4268
|
-
if (webClientDirExists) {
|
|
4269
|
-
const dependencies = ["ai"];
|
|
4270
|
-
if (hasNuxt) dependencies.push("@ai-sdk/vue");
|
|
4271
|
-
else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
|
|
4272
|
-
else if (hasReactWeb) dependencies.push("@ai-sdk/react");
|
|
4273
|
-
await addPackageDependency({
|
|
4274
|
-
dependencies,
|
|
4275
|
-
projectDir: webClientDir
|
|
4276
|
-
});
|
|
4277
|
-
}
|
|
4278
|
-
if (nativeClientDirExists && hasReactNative) await addPackageDependency({
|
|
4279
|
-
dependencies: ["ai", "@ai-sdk/react"],
|
|
4280
|
-
projectDir: nativeClientDir
|
|
4281
|
-
});
|
|
4282
|
-
if (serverDirExists && backend !== "none") await addPackageDependency({
|
|
4283
|
-
dependencies: ["ai", "@ai-sdk/google"],
|
|
4284
|
-
projectDir: serverDir
|
|
4285
|
-
});
|
|
4286
|
-
}
|
|
4287
|
-
}
|
|
4288
|
-
|
|
4289
|
-
//#endregion
|
|
4290
|
-
//#region src/helpers/setup/runtime-setup.ts
|
|
5196
|
+
//#region src/helpers/core/runtime-setup.ts
|
|
4291
5197
|
async function setupRuntime(config) {
|
|
4292
5198
|
const { runtime, backend, projectDir } = config;
|
|
4293
5199
|
if (backend === "convex" || backend === "next" || runtime === "none") return;
|
|
@@ -4295,23 +5201,6 @@ async function setupRuntime(config) {
|
|
|
4295
5201
|
if (!await fs.pathExists(serverDir)) return;
|
|
4296
5202
|
if (runtime === "bun") await setupBunRuntime(serverDir, backend);
|
|
4297
5203
|
else if (runtime === "node") await setupNodeRuntime(serverDir, backend);
|
|
4298
|
-
else if (runtime === "workers") await setupWorkersRuntime(serverDir);
|
|
4299
|
-
}
|
|
4300
|
-
async function generateCloudflareWorkerTypes(config) {
|
|
4301
|
-
if (config.runtime !== "workers") return;
|
|
4302
|
-
const serverDir = path.join(config.projectDir, "apps/server");
|
|
4303
|
-
if (!await fs.pathExists(serverDir)) return;
|
|
4304
|
-
const s = spinner();
|
|
4305
|
-
try {
|
|
4306
|
-
s.start("Generating Cloudflare Workers types...");
|
|
4307
|
-
const runCmd = config.packageManager === "npm" ? "npm" : config.packageManager;
|
|
4308
|
-
await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
|
|
4309
|
-
s.stop("Cloudflare Workers types generated successfully!");
|
|
4310
|
-
} catch {
|
|
4311
|
-
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
|
4312
|
-
const managerCmd = config.packageManager === "npm" ? "npm run" : `${config.packageManager} run`;
|
|
4313
|
-
console.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
|
|
4314
|
-
}
|
|
4315
5204
|
}
|
|
4316
5205
|
async function setupBunRuntime(serverDir, _backend) {
|
|
4317
5206
|
const packageJsonPath = path.join(serverDir, "package.json");
|
|
@@ -4351,27 +5240,20 @@ async function setupNodeRuntime(serverDir, backend) {
|
|
|
4351
5240
|
projectDir: serverDir
|
|
4352
5241
|
});
|
|
4353
5242
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
build: "wrangler deploy --dry-run",
|
|
4364
|
-
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
|
|
4365
|
-
};
|
|
4366
|
-
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
4367
|
-
await addPackageDependency({
|
|
4368
|
-
devDependencies: ["wrangler", "@types/node"],
|
|
4369
|
-
projectDir: serverDir
|
|
5243
|
+
|
|
5244
|
+
//#endregion
|
|
5245
|
+
//#region src/helpers/core/convex-codegen.ts
|
|
5246
|
+
async function runConvexCodegen(projectDir, packageManager) {
|
|
5247
|
+
const backendDir = path.join(projectDir, "packages/backend");
|
|
5248
|
+
const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
|
|
5249
|
+
await execa(cmd, {
|
|
5250
|
+
cwd: backendDir,
|
|
5251
|
+
shell: true
|
|
4370
5252
|
});
|
|
4371
5253
|
}
|
|
4372
5254
|
|
|
4373
5255
|
//#endregion
|
|
4374
|
-
//#region src/helpers/
|
|
5256
|
+
//#region src/helpers/core/create-readme.ts
|
|
4375
5257
|
async function createReadme(projectDir, options) {
|
|
4376
5258
|
const readmePath = path.join(projectDir, "README.md");
|
|
4377
5259
|
const content = generateReadmeContent(options);
|
|
@@ -4648,7 +5530,7 @@ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNati
|
|
|
4648
5530
|
}
|
|
4649
5531
|
|
|
4650
5532
|
//#endregion
|
|
4651
|
-
//#region src/helpers/
|
|
5533
|
+
//#region src/helpers/core/git.ts
|
|
4652
5534
|
async function initializeGit(projectDir, useGit) {
|
|
4653
5535
|
if (!useGit) return;
|
|
4654
5536
|
const gitVersionResult = await $({
|
|
@@ -4724,20 +5606,21 @@ async function getDockerStatus(database) {
|
|
|
4724
5606
|
}
|
|
4725
5607
|
|
|
4726
5608
|
//#endregion
|
|
4727
|
-
//#region src/helpers/
|
|
5609
|
+
//#region src/helpers/core/post-installation.ts
|
|
4728
5610
|
async function displayPostInstallInstructions(config) {
|
|
4729
|
-
const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy } = config;
|
|
5611
|
+
const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy } = config;
|
|
4730
5612
|
const isConvex = backend === "convex";
|
|
4731
5613
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
4732
5614
|
const cdCmd = `cd ${relativePath}`;
|
|
4733
5615
|
const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
|
|
4734
|
-
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
|
|
5616
|
+
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
|
|
4735
5617
|
const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
|
|
4736
5618
|
const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
|
|
4737
5619
|
const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
|
|
4738
5620
|
const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
|
|
4739
5621
|
const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
|
|
4740
|
-
const
|
|
5622
|
+
const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
5623
|
+
const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
4741
5624
|
const hasWeb = frontend?.some((f) => [
|
|
4742
5625
|
"tanstack-router",
|
|
4743
5626
|
"react-router",
|
|
@@ -4766,8 +5649,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
4766
5649
|
if (runtime === "workers") {
|
|
4767
5650
|
if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
|
|
4768
5651
|
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
4769
|
-
output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd}
|
|
4770
|
-
}
|
|
5652
|
+
if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
|
|
5653
|
+
}
|
|
4771
5654
|
}
|
|
4772
5655
|
output += `${pc.bold("Your project will be available at:")}\n`;
|
|
4773
5656
|
if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
|
|
@@ -4780,7 +5663,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
4780
5663
|
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
|
4781
5664
|
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
|
|
4782
5665
|
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
|
|
4783
|
-
if (
|
|
5666
|
+
if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
|
|
5667
|
+
if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
|
|
4784
5668
|
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
|
4785
5669
|
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
|
4786
5670
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
|
@@ -4801,7 +5685,7 @@ function getNativeInstructions(isConvex) {
|
|
|
4801
5685
|
function getLintingInstructions(runCmd) {
|
|
4802
5686
|
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
|
|
4803
5687
|
}
|
|
4804
|
-
async function getDatabaseInstructions(database, orm, runCmd,
|
|
5688
|
+
async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
|
|
4805
5689
|
const instructions = [];
|
|
4806
5690
|
if (dbSetup === "docker") {
|
|
4807
5691
|
const dockerStatus = await getDockerStatus(database);
|
|
@@ -4810,7 +5694,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4810
5694
|
instructions.push("");
|
|
4811
5695
|
}
|
|
4812
5696
|
}
|
|
4813
|
-
if (
|
|
5697
|
+
if (serverDeploy === "wrangler" && dbSetup === "d1") {
|
|
4814
5698
|
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
|
|
4815
5699
|
instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
|
|
4816
5700
|
instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
|
|
@@ -4818,8 +5702,8 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4818
5702
|
instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white(`cd apps/server && ${packageManager} db:generate`)}`);
|
|
4819
5703
|
instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
|
|
4820
5704
|
instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
|
|
4821
|
-
instructions.push("");
|
|
4822
5705
|
}
|
|
5706
|
+
if (dbSetup === "d1" && serverDeploy === "alchemy") {}
|
|
4823
5707
|
if (orm === "prisma") {
|
|
4824
5708
|
if (dbSetup === "turso") instructions.push(`${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires\n additional setup. Learn more at:\n https://www.prisma.io/docs/orm/overview/databases/turso`);
|
|
4825
5709
|
if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
|
|
@@ -4828,7 +5712,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4828
5712
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
|
4829
5713
|
} else if (orm === "drizzle") {
|
|
4830
5714
|
if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
|
|
4831
|
-
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
5715
|
+
if (dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
4832
5716
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
|
4833
5717
|
if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
|
|
4834
5718
|
} else if (orm === "mongoose") {
|
|
@@ -4851,12 +5735,22 @@ function getNoOrmWarning() {
|
|
|
4851
5735
|
function getBunWebNativeWarning() {
|
|
4852
5736
|
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
|
|
4853
5737
|
}
|
|
4854
|
-
function
|
|
4855
|
-
|
|
5738
|
+
function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5739
|
+
const instructions = [];
|
|
5740
|
+
if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
|
|
5741
|
+
if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
|
|
5742
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
5743
|
+
}
|
|
5744
|
+
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5745
|
+
const instructions = [];
|
|
5746
|
+
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
|
|
5747
|
+
else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
|
|
5748
|
+
else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
|
|
5749
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
4856
5750
|
}
|
|
4857
5751
|
|
|
4858
5752
|
//#endregion
|
|
4859
|
-
//#region src/helpers/
|
|
5753
|
+
//#region src/helpers/core/project-config.ts
|
|
4860
5754
|
async function updatePackageConfigurations(projectDir, options) {
|
|
4861
5755
|
await updateRootPackageJson(projectDir, options);
|
|
4862
5756
|
if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
|
|
@@ -5037,7 +5931,7 @@ async function updateConvexPackageJson(projectDir, options) {
|
|
|
5037
5931
|
}
|
|
5038
5932
|
|
|
5039
5933
|
//#endregion
|
|
5040
|
-
//#region src/helpers/
|
|
5934
|
+
//#region src/helpers/core/create-project.ts
|
|
5041
5935
|
async function createProject(options) {
|
|
5042
5936
|
const projectDir = options.projectDir;
|
|
5043
5937
|
const isConvex = options.backend === "convex";
|
|
@@ -5065,18 +5959,18 @@ async function createProject(options) {
|
|
|
5065
5959
|
if (!isConvex && options.auth) await setupAuth(options);
|
|
5066
5960
|
await handleExtras(projectDir, options);
|
|
5067
5961
|
await setupWebDeploy(options);
|
|
5962
|
+
await setupServerDeploy(options);
|
|
5068
5963
|
await setupEnvironmentVariables(options);
|
|
5069
5964
|
await updatePackageConfigurations(projectDir, options);
|
|
5070
5965
|
await createReadme(projectDir, options);
|
|
5071
5966
|
await writeBtsConfig(options);
|
|
5967
|
+
await formatProjectWithBiome(projectDir);
|
|
5968
|
+
if (isConvex) await runConvexCodegen(projectDir, options.packageManager);
|
|
5072
5969
|
log.success("Project template successfully scaffolded!");
|
|
5073
|
-
if (options.install) {
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
});
|
|
5078
|
-
await generateCloudflareWorkerTypes(options);
|
|
5079
|
-
}
|
|
5970
|
+
if (options.install) await installDependencies({
|
|
5971
|
+
projectDir,
|
|
5972
|
+
packageManager: options.packageManager
|
|
5973
|
+
});
|
|
5080
5974
|
await initializeGit(projectDir, options.git);
|
|
5081
5975
|
await displayPostInstallInstructions({
|
|
5082
5976
|
...options,
|
|
@@ -5095,7 +5989,7 @@ async function createProject(options) {
|
|
|
5095
5989
|
}
|
|
5096
5990
|
|
|
5097
5991
|
//#endregion
|
|
5098
|
-
//#region src/helpers/
|
|
5992
|
+
//#region src/helpers/core/command-handlers.ts
|
|
5099
5993
|
async function createProjectHandler(input) {
|
|
5100
5994
|
const startTime = Date.now();
|
|
5101
5995
|
const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5105,10 +5999,11 @@ async function createProjectHandler(input) {
|
|
|
5105
5999
|
let currentPathInput;
|
|
5106
6000
|
if (input.yes && input.projectName) currentPathInput = input.projectName;
|
|
5107
6001
|
else if (input.yes) {
|
|
5108
|
-
|
|
6002
|
+
const defaultConfig = getDefaultConfig();
|
|
6003
|
+
let defaultName = defaultConfig.relativePath;
|
|
5109
6004
|
let counter = 1;
|
|
5110
|
-
while (fs.
|
|
5111
|
-
defaultName = `${
|
|
6005
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
6006
|
+
defaultName = `${defaultConfig.projectName}-${counter}`;
|
|
5112
6007
|
counter++;
|
|
5113
6008
|
}
|
|
5114
6009
|
currentPathInput = defaultName;
|
|
@@ -5146,7 +6041,8 @@ async function createProjectHandler(input) {
|
|
|
5146
6041
|
install: false,
|
|
5147
6042
|
dbSetup: "none",
|
|
5148
6043
|
api: "none",
|
|
5149
|
-
webDeploy: "none"
|
|
6044
|
+
webDeploy: "none",
|
|
6045
|
+
serverDeploy: "none"
|
|
5150
6046
|
},
|
|
5151
6047
|
reproducibleCommand: "",
|
|
5152
6048
|
timeScaffolded,
|
|
@@ -5162,28 +6058,33 @@ async function createProjectHandler(input) {
|
|
|
5162
6058
|
projectDirectory: input.projectName
|
|
5163
6059
|
};
|
|
5164
6060
|
const providedFlags = getProvidedFlags(cliInput);
|
|
5165
|
-
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
5166
|
-
const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
|
|
5167
|
-
if (!input.yes && Object.keys(otherFlags).length > 0) {
|
|
5168
|
-
log.info(pc.yellow("Using these pre-selected options:"));
|
|
5169
|
-
log.message(displayConfig(otherFlags));
|
|
5170
|
-
log.message("");
|
|
5171
|
-
}
|
|
5172
6061
|
let config;
|
|
5173
6062
|
if (input.yes) {
|
|
6063
|
+
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
5174
6064
|
config = {
|
|
5175
|
-
...
|
|
6065
|
+
...getDefaultConfig(),
|
|
5176
6066
|
...flagConfig,
|
|
5177
6067
|
projectName: finalBaseName,
|
|
5178
6068
|
projectDir: finalResolvedPath,
|
|
5179
6069
|
relativePath: finalPathInput
|
|
5180
6070
|
};
|
|
6071
|
+
coerceBackendPresets(config);
|
|
6072
|
+
validateConfigCompatibility(config, providedFlags, cliInput);
|
|
5181
6073
|
if (config.backend === "convex") log.info("Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo");
|
|
5182
6074
|
else if (config.backend === "none") log.info("Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none");
|
|
5183
6075
|
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
5184
6076
|
log.message(displayConfig(config));
|
|
5185
6077
|
log.message("");
|
|
5186
|
-
} else
|
|
6078
|
+
} else {
|
|
6079
|
+
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
6080
|
+
const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
|
|
6081
|
+
if (Object.keys(otherFlags).length > 0) {
|
|
6082
|
+
log.info(pc.yellow("Using these pre-selected options:"));
|
|
6083
|
+
log.message(displayConfig(otherFlags));
|
|
6084
|
+
log.message("");
|
|
6085
|
+
}
|
|
6086
|
+
config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
|
|
6087
|
+
}
|
|
5187
6088
|
await createProject(config);
|
|
5188
6089
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
5189
6090
|
log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
@@ -5203,11 +6104,11 @@ async function createProjectHandler(input) {
|
|
|
5203
6104
|
}
|
|
5204
6105
|
async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
|
|
5205
6106
|
const currentPath = path.resolve(process.cwd(), currentPathInput);
|
|
5206
|
-
if (!fs.
|
|
6107
|
+
if (!await fs.pathExists(currentPath)) return {
|
|
5207
6108
|
finalPathInput: currentPathInput,
|
|
5208
6109
|
shouldClearDirectory: false
|
|
5209
6110
|
};
|
|
5210
|
-
const dirContents = fs.
|
|
6111
|
+
const dirContents = await fs.readdir(currentPath);
|
|
5211
6112
|
const isNotEmpty = dirContents.length > 0;
|
|
5212
6113
|
if (!isNotEmpty) return {
|
|
5213
6114
|
finalPathInput: currentPathInput,
|
|
@@ -5226,7 +6127,7 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
|
|
|
5226
6127
|
let counter = 1;
|
|
5227
6128
|
const baseName = currentPathInput;
|
|
5228
6129
|
let finalPathInput = `${baseName}-${counter}`;
|
|
5229
|
-
while (fs.
|
|
6130
|
+
while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
|
|
5230
6131
|
counter++;
|
|
5231
6132
|
finalPathInput = `${baseName}-${counter}`;
|
|
5232
6133
|
}
|
|
@@ -5252,6 +6153,10 @@ async function addAddonsHandler(input) {
|
|
|
5252
6153
|
const deploymentPrompt = await getDeploymentToAdd(detectedConfig.frontend || [], detectedConfig.webDeploy);
|
|
5253
6154
|
if (deploymentPrompt !== "none") input.webDeploy = deploymentPrompt;
|
|
5254
6155
|
}
|
|
6156
|
+
if (!input.serverDeploy) {
|
|
6157
|
+
const serverDeploymentPrompt = await getServerDeploymentToAdd(detectedConfig.runtime, detectedConfig.serverDeploy);
|
|
6158
|
+
if (serverDeploymentPrompt !== "none") input.serverDeploy = serverDeploymentPrompt;
|
|
6159
|
+
}
|
|
5255
6160
|
const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
|
|
5256
6161
|
let somethingAdded = false;
|
|
5257
6162
|
if (input.addons && input.addons.length > 0) {
|
|
@@ -5272,6 +6177,15 @@ async function addAddonsHandler(input) {
|
|
|
5272
6177
|
});
|
|
5273
6178
|
somethingAdded = true;
|
|
5274
6179
|
}
|
|
6180
|
+
if (input.serverDeploy && input.serverDeploy !== "none") {
|
|
6181
|
+
await addDeploymentToProject({
|
|
6182
|
+
...input,
|
|
6183
|
+
install: false,
|
|
6184
|
+
suppressInstallMessage: true,
|
|
6185
|
+
serverDeploy: input.serverDeploy
|
|
6186
|
+
});
|
|
6187
|
+
somethingAdded = true;
|
|
6188
|
+
}
|
|
5275
6189
|
if (!somethingAdded) {
|
|
5276
6190
|
outro(pc.yellow("No addons or deployment configurations to add."));
|
|
5277
6191
|
return;
|
|
@@ -5375,6 +6289,7 @@ const router = t.router({
|
|
|
5375
6289
|
runtime: RuntimeSchema.optional(),
|
|
5376
6290
|
api: APISchema.optional(),
|
|
5377
6291
|
webDeploy: WebDeploySchema.optional(),
|
|
6292
|
+
serverDeploy: ServerDeploySchema.optional(),
|
|
5378
6293
|
directoryConflict: DirectoryConflictSchema.optional(),
|
|
5379
6294
|
renderTitle: z.boolean().optional(),
|
|
5380
6295
|
disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics")
|
|
@@ -5390,6 +6305,7 @@ const router = t.router({
|
|
|
5390
6305
|
add: t.procedure.meta({ description: "Add addons or deployment configurations to an existing Better-T Stack project" }).input(z.tuple([z.object({
|
|
5391
6306
|
addons: z.array(AddonsSchema).optional().default([]),
|
|
5392
6307
|
webDeploy: WebDeploySchema.optional(),
|
|
6308
|
+
serverDeploy: ServerDeploySchema.optional(),
|
|
5393
6309
|
projectDir: z.string().optional(),
|
|
5394
6310
|
install: z.boolean().optional().default(false).describe("Install dependencies after adding addons or deployment"),
|
|
5395
6311
|
packageManager: PackageManagerSchema.optional()
|