create-better-t-stack 2.33.8 → 2.33.9-canary.2ec142a9
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-jDxvJPRx.js → src-DlHyCY_I.js} +1466 -562
- 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,23 @@ 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
|
+
}
|
|
547
591
|
|
|
548
592
|
//#endregion
|
|
549
593
|
//#region src/prompts/api.ts
|
|
@@ -1004,16 +1048,97 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
1004
1048
|
return response;
|
|
1005
1049
|
}
|
|
1006
1050
|
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/prompts/server-deploy.ts
|
|
1053
|
+
function getDeploymentDisplay$1(deployment) {
|
|
1054
|
+
if (deployment === "wrangler") return {
|
|
1055
|
+
label: "Wrangler",
|
|
1056
|
+
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
1057
|
+
};
|
|
1058
|
+
if (deployment === "alchemy") return {
|
|
1059
|
+
label: "Alchemy",
|
|
1060
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
1061
|
+
};
|
|
1062
|
+
return {
|
|
1063
|
+
label: deployment,
|
|
1064
|
+
hint: `Add ${deployment} deployment`
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
async function getServerDeploymentChoice(deployment, runtime, backend, webDeploy) {
|
|
1068
|
+
if (deployment !== void 0) return deployment;
|
|
1069
|
+
if (backend === "none" || backend === "convex") return "none";
|
|
1070
|
+
const options = [];
|
|
1071
|
+
if (runtime === "workers") ["alchemy", "wrangler"].forEach((deploy) => {
|
|
1072
|
+
const { label, hint } = getDeploymentDisplay$1(deploy);
|
|
1073
|
+
options.unshift({
|
|
1074
|
+
value: deploy,
|
|
1075
|
+
label,
|
|
1076
|
+
hint
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
else options.push({
|
|
1080
|
+
value: "none",
|
|
1081
|
+
label: "None",
|
|
1082
|
+
hint: "Manual setup"
|
|
1083
|
+
});
|
|
1084
|
+
const response = await select({
|
|
1085
|
+
message: "Select server deployment",
|
|
1086
|
+
options,
|
|
1087
|
+
initialValue: webDeploy === "alchemy" ? "alchemy" : runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
|
|
1088
|
+
});
|
|
1089
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1090
|
+
return response;
|
|
1091
|
+
}
|
|
1092
|
+
async function getServerDeploymentToAdd(runtime, existingDeployment) {
|
|
1093
|
+
const options = [];
|
|
1094
|
+
if (runtime === "workers") {
|
|
1095
|
+
if (existingDeployment !== "wrangler") {
|
|
1096
|
+
const { label, hint } = getDeploymentDisplay$1("wrangler");
|
|
1097
|
+
options.push({
|
|
1098
|
+
value: "wrangler",
|
|
1099
|
+
label,
|
|
1100
|
+
hint
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
if (existingDeployment !== "alchemy") {
|
|
1104
|
+
const { label, hint } = getDeploymentDisplay$1("alchemy");
|
|
1105
|
+
options.push({
|
|
1106
|
+
value: "alchemy",
|
|
1107
|
+
label,
|
|
1108
|
+
hint
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (existingDeployment && existingDeployment !== "none") return "none";
|
|
1113
|
+
if (options.length > 0) options.push({
|
|
1114
|
+
value: "none",
|
|
1115
|
+
label: "None",
|
|
1116
|
+
hint: "Skip deployment setup"
|
|
1117
|
+
});
|
|
1118
|
+
if (options.length === 0) return "none";
|
|
1119
|
+
const response = await select({
|
|
1120
|
+
message: "Select server deployment",
|
|
1121
|
+
options,
|
|
1122
|
+
initialValue: runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
|
|
1123
|
+
});
|
|
1124
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1125
|
+
return response;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1007
1128
|
//#endregion
|
|
1008
1129
|
//#region src/prompts/web-deploy.ts
|
|
1009
1130
|
function hasWebFrontend(frontends) {
|
|
1010
1131
|
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
|
|
1011
1132
|
}
|
|
1012
1133
|
function getDeploymentDisplay(deployment) {
|
|
1013
|
-
if (deployment === "
|
|
1014
|
-
label: "
|
|
1134
|
+
if (deployment === "wrangler") return {
|
|
1135
|
+
label: "Wrangler",
|
|
1015
1136
|
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
1016
1137
|
};
|
|
1138
|
+
if (deployment === "alchemy") return {
|
|
1139
|
+
label: "Alchemy",
|
|
1140
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
1141
|
+
};
|
|
1017
1142
|
return {
|
|
1018
1143
|
label: deployment,
|
|
1019
1144
|
hint: `Add ${deployment} deployment`
|
|
@@ -1022,15 +1147,18 @@ function getDeploymentDisplay(deployment) {
|
|
|
1022
1147
|
async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
|
|
1023
1148
|
if (deployment !== void 0) return deployment;
|
|
1024
1149
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1025
|
-
const options = [
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1150
|
+
const options = [
|
|
1151
|
+
"wrangler",
|
|
1152
|
+
"alchemy",
|
|
1153
|
+
"none"
|
|
1154
|
+
].map((deploy) => {
|
|
1155
|
+
const { label, hint } = getDeploymentDisplay(deploy);
|
|
1156
|
+
return {
|
|
1157
|
+
value: deploy,
|
|
1158
|
+
label,
|
|
1159
|
+
hint
|
|
1160
|
+
};
|
|
1161
|
+
});
|
|
1034
1162
|
const response = await select({
|
|
1035
1163
|
message: "Select web deployment",
|
|
1036
1164
|
options,
|
|
@@ -1042,10 +1170,18 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
|
|
|
1042
1170
|
async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
1043
1171
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1044
1172
|
const options = [];
|
|
1045
|
-
if (existingDeployment !== "
|
|
1046
|
-
const { label, hint } = getDeploymentDisplay("
|
|
1173
|
+
if (existingDeployment !== "wrangler") {
|
|
1174
|
+
const { label, hint } = getDeploymentDisplay("wrangler");
|
|
1175
|
+
options.push({
|
|
1176
|
+
value: "wrangler",
|
|
1177
|
+
label,
|
|
1178
|
+
hint
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
if (existingDeployment !== "alchemy") {
|
|
1182
|
+
const { label, hint } = getDeploymentDisplay("alchemy");
|
|
1047
1183
|
options.push({
|
|
1048
|
-
value: "
|
|
1184
|
+
value: "alchemy",
|
|
1049
1185
|
label,
|
|
1050
1186
|
hint
|
|
1051
1187
|
});
|
|
@@ -1081,6 +1217,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1081
1217
|
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
|
|
1082
1218
|
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
|
|
1083
1219
|
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend),
|
|
1220
|
+
serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
|
|
1084
1221
|
git: () => getGitChoice(flags.git),
|
|
1085
1222
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
|
1086
1223
|
install: () => getinstallChoice(flags.install)
|
|
@@ -1120,7 +1257,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1120
1257
|
install: result.install,
|
|
1121
1258
|
dbSetup: result.dbSetup,
|
|
1122
1259
|
api: result.api,
|
|
1123
|
-
webDeploy: result.webDeploy
|
|
1260
|
+
webDeploy: result.webDeploy,
|
|
1261
|
+
serverDeploy: result.serverDeploy
|
|
1124
1262
|
};
|
|
1125
1263
|
}
|
|
1126
1264
|
|
|
@@ -1152,7 +1290,7 @@ async function getProjectName(initialName) {
|
|
|
1152
1290
|
let projectPath = "";
|
|
1153
1291
|
let defaultName = DEFAULT_CONFIG.projectName;
|
|
1154
1292
|
let counter = 1;
|
|
1155
|
-
while (fs.
|
|
1293
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
1156
1294
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
|
1157
1295
|
counter++;
|
|
1158
1296
|
}
|
|
@@ -1199,7 +1337,7 @@ const getLatestCLIVersion = () => {
|
|
|
1199
1337
|
*/
|
|
1200
1338
|
function isTelemetryEnabled() {
|
|
1201
1339
|
const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
|
|
1202
|
-
const BTS_TELEMETRY = "
|
|
1340
|
+
const BTS_TELEMETRY = "0";
|
|
1203
1341
|
if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
|
|
1204
1342
|
if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
|
|
1205
1343
|
return true;
|
|
@@ -1207,11 +1345,16 @@ function isTelemetryEnabled() {
|
|
|
1207
1345
|
|
|
1208
1346
|
//#endregion
|
|
1209
1347
|
//#region src/utils/analytics.ts
|
|
1210
|
-
const POSTHOG_API_KEY = "
|
|
1211
|
-
const POSTHOG_HOST = "
|
|
1348
|
+
const POSTHOG_API_KEY = "random";
|
|
1349
|
+
const POSTHOG_HOST = "random";
|
|
1350
|
+
function generateSessionId() {
|
|
1351
|
+
const rand = Math.random().toString(36).slice(2);
|
|
1352
|
+
const now = Date.now().toString(36);
|
|
1353
|
+
return `cli_${now}${rand}`;
|
|
1354
|
+
}
|
|
1212
1355
|
async function trackProjectCreation(config, disableAnalytics = false) {
|
|
1213
1356
|
if (!isTelemetryEnabled() || disableAnalytics) return;
|
|
1214
|
-
const sessionId =
|
|
1357
|
+
const sessionId = generateSessionId();
|
|
1215
1358
|
const { projectName, projectDir, relativePath,...safeConfig } = config;
|
|
1216
1359
|
const payload = {
|
|
1217
1360
|
api_key: POSTHOG_API_KEY,
|
|
@@ -1219,8 +1362,8 @@ async function trackProjectCreation(config, disableAnalytics = false) {
|
|
|
1219
1362
|
properties: {
|
|
1220
1363
|
...safeConfig,
|
|
1221
1364
|
cli_version: getLatestCLIVersion(),
|
|
1222
|
-
node_version: process.version,
|
|
1223
|
-
platform: process.platform,
|
|
1365
|
+
node_version: typeof process !== "undefined" ? process.version : "",
|
|
1366
|
+
platform: typeof process !== "undefined" ? process.platform : "",
|
|
1224
1367
|
$ip: null
|
|
1225
1368
|
},
|
|
1226
1369
|
distinct_id: sessionId
|
|
@@ -1274,6 +1417,7 @@ function displayConfig(config) {
|
|
|
1274
1417
|
}
|
|
1275
1418
|
if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
|
|
1276
1419
|
if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
|
|
1420
|
+
if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
|
|
1277
1421
|
if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
|
|
1278
1422
|
return configDisplay.join("\n");
|
|
1279
1423
|
}
|
|
@@ -1296,6 +1440,7 @@ function generateReproducibleCommand(config) {
|
|
|
1296
1440
|
else flags.push("--examples none");
|
|
1297
1441
|
flags.push(`--db-setup ${config.dbSetup}`);
|
|
1298
1442
|
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
1443
|
+
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
1299
1444
|
flags.push(config.git ? "--git" : "--no-git");
|
|
1300
1445
|
flags.push(`--package-manager ${config.packageManager}`);
|
|
1301
1446
|
flags.push(config.install ? "--install" : "--no-install");
|
|
@@ -1313,8 +1458,8 @@ function generateReproducibleCommand(config) {
|
|
|
1313
1458
|
async function handleDirectoryConflict(currentPathInput, silent = false) {
|
|
1314
1459
|
while (true) {
|
|
1315
1460
|
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
|
1316
|
-
const dirExists = fs.
|
|
1317
|
-
const dirIsNotEmpty = dirExists && fs.
|
|
1461
|
+
const dirExists = await fs.pathExists(resolvedPath);
|
|
1462
|
+
const dirIsNotEmpty = dirExists && (await fs.readdir(resolvedPath)).length > 0;
|
|
1318
1463
|
if (!dirIsNotEmpty) return {
|
|
1319
1464
|
finalPathInput: currentPathInput,
|
|
1320
1465
|
shouldClearDirectory: false
|
|
@@ -1440,7 +1585,7 @@ const renderTitle = () => {
|
|
|
1440
1585
|
};
|
|
1441
1586
|
|
|
1442
1587
|
//#endregion
|
|
1443
|
-
//#region src/
|
|
1588
|
+
//#region src/utils/config-processing.ts
|
|
1444
1589
|
function processArrayOption(options) {
|
|
1445
1590
|
if (!options || options.length === 0) return [];
|
|
1446
1591
|
if (options.includes("none")) return [];
|
|
@@ -1451,22 +1596,10 @@ function deriveProjectName(projectName, projectDirectory) {
|
|
|
1451
1596
|
if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
|
|
1452
1597
|
return "";
|
|
1453
1598
|
}
|
|
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) {
|
|
1599
|
+
function processFlags(options, projectName) {
|
|
1459
1600
|
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
|
-
}
|
|
1601
|
+
if (options.api) config.api = options.api;
|
|
1466
1602
|
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
1603
|
if (options.database) config.database = options.database;
|
|
1471
1604
|
if (options.orm) config.orm = options.orm;
|
|
1472
1605
|
if (options.auth !== void 0) config.auth = options.auth;
|
|
@@ -1476,70 +1609,189 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1476
1609
|
if (options.dbSetup) config.dbSetup = options.dbSetup;
|
|
1477
1610
|
if (options.packageManager) config.packageManager = options.packageManager;
|
|
1478
1611
|
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1612
|
+
if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
|
|
1479
1613
|
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
|
-
|
|
1614
|
+
if (derivedName) config.projectName = projectName || derivedName;
|
|
1615
|
+
if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
|
|
1616
|
+
if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
|
|
1617
|
+
if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
|
|
1618
|
+
return config;
|
|
1619
|
+
}
|
|
1620
|
+
function getProvidedFlags(options) {
|
|
1621
|
+
return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
|
|
1622
|
+
}
|
|
1623
|
+
function validateNoneExclusivity(options, optionName) {
|
|
1624
|
+
if (!options || options.length === 0) return;
|
|
1625
|
+
if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
|
|
1626
|
+
}
|
|
1627
|
+
function validateArrayOptions(options) {
|
|
1628
|
+
validateNoneExclusivity(options.frontend, "frontend options");
|
|
1629
|
+
validateNoneExclusivity(options.addons, "addons");
|
|
1630
|
+
validateNoneExclusivity(options.examples, "examples");
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/utils/config-validation.ts
|
|
1635
|
+
function validateDatabaseOrmAuth(cfg, flags) {
|
|
1636
|
+
const db = cfg.database;
|
|
1637
|
+
const orm = cfg.orm;
|
|
1638
|
+
const has = (k) => flags ? flags.has(k) : true;
|
|
1639
|
+
if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
|
|
1640
|
+
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.");
|
|
1641
|
+
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.");
|
|
1642
|
+
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'.");
|
|
1643
|
+
if (has("orm") && has("database") && orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1644
|
+
if (has("auth") && has("database") && cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1645
|
+
if (cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1646
|
+
if (orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1647
|
+
}
|
|
1648
|
+
function validateDatabaseSetup(config, providedFlags) {
|
|
1649
|
+
const { dbSetup, database, runtime } = config;
|
|
1650
|
+
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'.");
|
|
1651
|
+
const setupValidations = {
|
|
1652
|
+
turso: {
|
|
1653
|
+
database: "sqlite",
|
|
1654
|
+
errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
|
|
1655
|
+
},
|
|
1656
|
+
neon: {
|
|
1657
|
+
database: "postgres",
|
|
1658
|
+
errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1659
|
+
},
|
|
1660
|
+
"prisma-postgres": {
|
|
1661
|
+
database: "postgres",
|
|
1662
|
+
errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1663
|
+
},
|
|
1664
|
+
"mongodb-atlas": {
|
|
1665
|
+
database: "mongodb",
|
|
1666
|
+
errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
|
|
1667
|
+
},
|
|
1668
|
+
supabase: {
|
|
1669
|
+
database: "postgres",
|
|
1670
|
+
errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1671
|
+
},
|
|
1672
|
+
d1: {
|
|
1673
|
+
database: "sqlite",
|
|
1674
|
+
runtime: "workers",
|
|
1675
|
+
errorMessage: "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime."
|
|
1676
|
+
},
|
|
1677
|
+
docker: { errorMessage: "Docker setup is not compatible with SQLite database or Cloudflare Workers runtime." },
|
|
1678
|
+
none: { errorMessage: "" }
|
|
1679
|
+
};
|
|
1680
|
+
if (dbSetup && dbSetup !== "none") {
|
|
1681
|
+
const validation = setupValidations[dbSetup];
|
|
1682
|
+
if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
|
|
1683
|
+
if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
|
|
1684
|
+
if (dbSetup === "docker") {
|
|
1685
|
+
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.");
|
|
1686
|
+
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.");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
function validateBackendConstraints(config, providedFlags, options) {
|
|
1691
|
+
const { backend } = config;
|
|
1692
|
+
if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none") {
|
|
1693
|
+
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
1694
|
}
|
|
1505
|
-
if (
|
|
1506
|
-
const incompatibleFlags = incompatibleFlagsForBackend(
|
|
1507
|
-
if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${
|
|
1508
|
-
if (
|
|
1695
|
+
if (backend === "convex" || backend === "none") {
|
|
1696
|
+
const incompatibleFlags = incompatibleFlagsForBackend(backend, providedFlags, options);
|
|
1697
|
+
if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1698
|
+
if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
|
|
1509
1699
|
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
|
1510
1700
|
if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1511
1701
|
}
|
|
1512
1702
|
coerceBackendPresets(config);
|
|
1513
1703
|
}
|
|
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
|
-
}
|
|
1704
|
+
}
|
|
1705
|
+
function validateFrontendConstraints(config, providedFlags) {
|
|
1706
|
+
const { frontend } = config;
|
|
1707
|
+
if (frontend && frontend.length > 0) {
|
|
1708
|
+
ensureSingleWebAndNative(frontend);
|
|
1709
|
+
if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend);
|
|
1533
1710
|
}
|
|
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));
|
|
1711
|
+
const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
|
|
1538
1712
|
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
1713
|
+
}
|
|
1714
|
+
function validateApiConstraints(config, options) {
|
|
1715
|
+
if (config.api === "none") {
|
|
1716
|
+
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.");
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
function validateFullConfig(config, providedFlags, options) {
|
|
1720
|
+
validateDatabaseOrmAuth(config, providedFlags);
|
|
1721
|
+
validateDatabaseSetup(config, providedFlags);
|
|
1722
|
+
validateBackendConstraints(config, providedFlags, options);
|
|
1723
|
+
validateFrontendConstraints(config, providedFlags);
|
|
1724
|
+
validateApiConstraints(config, options);
|
|
1725
|
+
validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
|
1726
|
+
validateWorkersCompatibility(providedFlags, options, config);
|
|
1727
|
+
if (config.addons && config.addons.length > 0) {
|
|
1728
|
+
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
|
1729
|
+
config.addons = [...new Set(config.addons)];
|
|
1730
|
+
}
|
|
1731
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
1732
|
+
}
|
|
1733
|
+
function validateConfigForProgrammaticUse(config) {
|
|
1734
|
+
try {
|
|
1735
|
+
validateDatabaseOrmAuth(config);
|
|
1736
|
+
if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
|
|
1737
|
+
validateApiFrontendCompatibility(config.api, config.frontend);
|
|
1738
|
+
if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend);
|
|
1739
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
if (error instanceof Error) throw error;
|
|
1742
|
+
throw new Error(String(error));
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
//#endregion
|
|
1747
|
+
//#region src/utils/project-name-validation.ts
|
|
1748
|
+
function validateProjectName(name) {
|
|
1749
|
+
const result = ProjectNameSchema.safeParse(name);
|
|
1750
|
+
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1751
|
+
}
|
|
1752
|
+
function validateProjectNameThrow(name) {
|
|
1753
|
+
const result = ProjectNameSchema.safeParse(name);
|
|
1754
|
+
if (!result.success) throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
|
|
1755
|
+
}
|
|
1756
|
+
function extractAndValidateProjectName(projectName, projectDirectory, throwOnError = false) {
|
|
1757
|
+
const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
|
|
1758
|
+
if (!derivedName) return "";
|
|
1759
|
+
const nameToValidate = projectName ? path.basename(projectName) : derivedName;
|
|
1760
|
+
if (throwOnError) validateProjectNameThrow(nameToValidate);
|
|
1761
|
+
else validateProjectName(nameToValidate);
|
|
1762
|
+
return projectName || derivedName;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/validation.ts
|
|
1767
|
+
function processAndValidateFlags(options, providedFlags, projectName) {
|
|
1768
|
+
if (options.yolo) {
|
|
1769
|
+
const cfg = processFlags(options, projectName);
|
|
1770
|
+
const validatedProjectName$1 = extractAndValidateProjectName(projectName, options.projectDirectory, true);
|
|
1771
|
+
if (validatedProjectName$1) cfg.projectName = validatedProjectName$1;
|
|
1772
|
+
return cfg;
|
|
1773
|
+
}
|
|
1774
|
+
try {
|
|
1775
|
+
validateArrayOptions(options);
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
exitWithError(error instanceof Error ? error.message : String(error));
|
|
1778
|
+
}
|
|
1779
|
+
const config = processFlags(options, projectName);
|
|
1780
|
+
const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, false);
|
|
1781
|
+
if (validatedProjectName) config.projectName = validatedProjectName;
|
|
1782
|
+
validateFullConfig(config, providedFlags, options);
|
|
1539
1783
|
return config;
|
|
1540
1784
|
}
|
|
1541
|
-
function
|
|
1542
|
-
|
|
1785
|
+
function processProvidedFlagsWithoutValidation(options, projectName) {
|
|
1786
|
+
const config = processFlags(options, projectName);
|
|
1787
|
+
const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, true);
|
|
1788
|
+
if (validatedProjectName) config.projectName = validatedProjectName;
|
|
1789
|
+
return config;
|
|
1790
|
+
}
|
|
1791
|
+
function validateConfigCompatibility(config, providedFlags, options) {
|
|
1792
|
+
if (options?.yolo) return;
|
|
1793
|
+
if (options && providedFlags) validateFullConfig(config, providedFlags, options);
|
|
1794
|
+
else validateConfigForProgrammaticUse(config);
|
|
1543
1795
|
}
|
|
1544
1796
|
|
|
1545
1797
|
//#endregion
|
|
@@ -1560,7 +1812,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1560
1812
|
packageManager: projectConfig.packageManager,
|
|
1561
1813
|
dbSetup: projectConfig.dbSetup,
|
|
1562
1814
|
api: projectConfig.api,
|
|
1563
|
-
webDeploy: projectConfig.webDeploy
|
|
1815
|
+
webDeploy: projectConfig.webDeploy,
|
|
1816
|
+
serverDeploy: projectConfig.serverDeploy
|
|
1564
1817
|
};
|
|
1565
1818
|
const baseContent = {
|
|
1566
1819
|
$schema: "https://r2.better-t-stack.dev/schema.json",
|
|
@@ -1577,7 +1830,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1577
1830
|
packageManager: btsConfig.packageManager,
|
|
1578
1831
|
dbSetup: btsConfig.dbSetup,
|
|
1579
1832
|
api: btsConfig.api,
|
|
1580
|
-
webDeploy: btsConfig.webDeploy
|
|
1833
|
+
webDeploy: btsConfig.webDeploy,
|
|
1834
|
+
serverDeploy: btsConfig.serverDeploy
|
|
1581
1835
|
};
|
|
1582
1836
|
let configContent = JSON.stringify(baseContent);
|
|
1583
1837
|
const formatResult = JSONC.format(configContent, void 0, {
|
|
@@ -1670,7 +1924,7 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
|
|
|
1670
1924
|
}
|
|
1671
1925
|
|
|
1672
1926
|
//#endregion
|
|
1673
|
-
//#region src/helpers/
|
|
1927
|
+
//#region src/helpers/addons/fumadocs-setup.ts
|
|
1674
1928
|
const TEMPLATES = {
|
|
1675
1929
|
"next-mdx": {
|
|
1676
1930
|
label: "Next.js: Fumadocs MDX",
|
|
@@ -1757,9 +2011,9 @@ handlebars.registerHelper("or", (a, b) => a || b);
|
|
|
1757
2011
|
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
1758
2012
|
|
|
1759
2013
|
//#endregion
|
|
1760
|
-
//#region src/helpers/
|
|
2014
|
+
//#region src/helpers/core/template-manager.ts
|
|
1761
2015
|
async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
|
|
1762
|
-
const sourceFiles = await
|
|
2016
|
+
const sourceFiles = await glob(sourcePattern, {
|
|
1763
2017
|
cwd: baseSourceDir,
|
|
1764
2018
|
dot: true,
|
|
1765
2019
|
onlyFiles: true,
|
|
@@ -2073,10 +2327,6 @@ async function handleExtras(projectDir, context) {
|
|
|
2073
2327
|
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
|
|
2074
2328
|
if (await fs.pathExists(npmrcTemplateSrc)) await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
|
|
2075
2329
|
}
|
|
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
2330
|
}
|
|
2081
2331
|
async function setupDockerComposeTemplates(projectDir, context) {
|
|
2082
2332
|
if (context.dbSetup !== "docker" || context.database === "none") return;
|
|
@@ -2085,29 +2335,62 @@ async function setupDockerComposeTemplates(projectDir, context) {
|
|
|
2085
2335
|
if (await fs.pathExists(dockerSrcDir)) await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
|
|
2086
2336
|
}
|
|
2087
2337
|
async function setupDeploymentTemplates(projectDir, context) {
|
|
2088
|
-
if (context.webDeploy === "
|
|
2089
|
-
|
|
2338
|
+
if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
|
|
2339
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2340
|
+
if (await fs.pathExists(alchemyTemplateSrc)) {
|
|
2341
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, projectDir, context);
|
|
2342
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2343
|
+
if (await fs.pathExists(serverAppDir)) {
|
|
2344
|
+
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2345
|
+
await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
} else {
|
|
2349
|
+
if (context.webDeploy === "alchemy") {
|
|
2350
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2351
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2352
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
|
|
2353
|
+
}
|
|
2354
|
+
if (context.serverDeploy === "alchemy") {
|
|
2355
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2356
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2357
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
|
|
2358
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2359
|
+
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2360
|
+
await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
|
|
2090
2365
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2366
|
+
if (await fs.pathExists(webAppDir)) {
|
|
2367
|
+
const frontends = context.frontend;
|
|
2368
|
+
const templateMap = {
|
|
2369
|
+
"tanstack-router": "react/tanstack-router",
|
|
2370
|
+
"tanstack-start": "react/tanstack-start",
|
|
2371
|
+
"react-router": "react/react-router",
|
|
2372
|
+
solid: "solid",
|
|
2373
|
+
next: "react/next",
|
|
2374
|
+
nuxt: "nuxt",
|
|
2375
|
+
svelte: "svelte"
|
|
2376
|
+
};
|
|
2377
|
+
for (const f of frontends) if (templateMap[f]) {
|
|
2378
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
|
|
2379
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
|
|
2384
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2385
|
+
if (await fs.pathExists(serverAppDir)) {
|
|
2386
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
|
|
2387
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
|
|
2105
2388
|
}
|
|
2106
2389
|
}
|
|
2107
2390
|
}
|
|
2108
2391
|
|
|
2109
2392
|
//#endregion
|
|
2110
|
-
//#region src/helpers/
|
|
2393
|
+
//#region src/helpers/addons/ruler-setup.ts
|
|
2111
2394
|
async function setupVibeRules(config) {
|
|
2112
2395
|
const { packageManager, projectDir } = config;
|
|
2113
2396
|
try {
|
|
@@ -2189,7 +2472,7 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
|
|
|
2189
2472
|
}
|
|
2190
2473
|
|
|
2191
2474
|
//#endregion
|
|
2192
|
-
//#region src/helpers/
|
|
2475
|
+
//#region src/helpers/addons/starlight-setup.ts
|
|
2193
2476
|
async function setupStarlight(config) {
|
|
2194
2477
|
const { packageManager, projectDir } = config;
|
|
2195
2478
|
const s = spinner();
|
|
@@ -2221,7 +2504,7 @@ async function setupStarlight(config) {
|
|
|
2221
2504
|
}
|
|
2222
2505
|
|
|
2223
2506
|
//#endregion
|
|
2224
|
-
//#region src/helpers/
|
|
2507
|
+
//#region src/helpers/addons/tauri-setup.ts
|
|
2225
2508
|
async function setupTauri(config) {
|
|
2226
2509
|
const { packageManager, frontend, projectDir } = config;
|
|
2227
2510
|
const s = spinner();
|
|
@@ -2258,8 +2541,8 @@ async function setupTauri(config) {
|
|
|
2258
2541
|
`--window-title=${path.basename(projectDir)}`,
|
|
2259
2542
|
`--frontend-dist=${frontendDist}`,
|
|
2260
2543
|
`--dev-url=${devUrl}`,
|
|
2261
|
-
`--before-dev-command
|
|
2262
|
-
`--before-build-command
|
|
2544
|
+
`--before-dev-command="${packageManager} run dev"`,
|
|
2545
|
+
`--before-build-command="${packageManager} run build"`
|
|
2263
2546
|
];
|
|
2264
2547
|
const tauriArgsString = tauriArgs.join(" ");
|
|
2265
2548
|
const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
|
|
@@ -2277,7 +2560,7 @@ async function setupTauri(config) {
|
|
|
2277
2560
|
}
|
|
2278
2561
|
|
|
2279
2562
|
//#endregion
|
|
2280
|
-
//#region src/helpers/
|
|
2563
|
+
//#region src/helpers/addons/ultracite-setup.ts
|
|
2281
2564
|
const EDITORS = {
|
|
2282
2565
|
vscode: {
|
|
2283
2566
|
label: "VSCode / Cursor / Windsurf",
|
|
@@ -2346,7 +2629,7 @@ async function setupUltracite(config, hasHusky) {
|
|
|
2346
2629
|
];
|
|
2347
2630
|
if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
|
|
2348
2631
|
if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
|
|
2349
|
-
if (hasHusky) ultraciteArgs.push("--
|
|
2632
|
+
if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
|
|
2350
2633
|
const ultraciteArgsString = ultraciteArgs.join(" ");
|
|
2351
2634
|
const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
|
|
2352
2635
|
const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
@@ -2384,7 +2667,7 @@ function ensureArrayProperty(obj, name) {
|
|
|
2384
2667
|
}
|
|
2385
2668
|
|
|
2386
2669
|
//#endregion
|
|
2387
|
-
//#region src/helpers/
|
|
2670
|
+
//#region src/helpers/addons/vite-pwa-setup.ts
|
|
2388
2671
|
async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
2389
2672
|
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
|
2390
2673
|
if (!sourceFile) throw new Error("vite config not found");
|
|
@@ -2418,7 +2701,7 @@ async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
|
2418
2701
|
}
|
|
2419
2702
|
|
|
2420
2703
|
//#endregion
|
|
2421
|
-
//#region src/helpers/
|
|
2704
|
+
//#region src/helpers/addons/addons-setup.ts
|
|
2422
2705
|
async function setupAddons(config, isAddCommand = false) {
|
|
2423
2706
|
const { addons, frontend, projectDir, packageManager } = config;
|
|
2424
2707
|
const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
|
|
@@ -2552,7 +2835,7 @@ async function setupOxlint(projectDir, packageManager) {
|
|
|
2552
2835
|
}
|
|
2553
2836
|
|
|
2554
2837
|
//#endregion
|
|
2555
|
-
//#region src/helpers/
|
|
2838
|
+
//#region src/helpers/core/detect-project-config.ts
|
|
2556
2839
|
async function detectProjectConfig(projectDir) {
|
|
2557
2840
|
try {
|
|
2558
2841
|
const btsConfig = await readBtsConfig(projectDir);
|
|
@@ -2570,7 +2853,8 @@ async function detectProjectConfig(projectDir) {
|
|
|
2570
2853
|
packageManager: btsConfig.packageManager,
|
|
2571
2854
|
dbSetup: btsConfig.dbSetup,
|
|
2572
2855
|
api: btsConfig.api,
|
|
2573
|
-
webDeploy: btsConfig.webDeploy
|
|
2856
|
+
webDeploy: btsConfig.webDeploy,
|
|
2857
|
+
serverDeploy: btsConfig.serverDeploy
|
|
2574
2858
|
};
|
|
2575
2859
|
return null;
|
|
2576
2860
|
} catch (_error) {
|
|
@@ -2586,7 +2870,7 @@ async function isBetterTStackProject(projectDir) {
|
|
|
2586
2870
|
}
|
|
2587
2871
|
|
|
2588
2872
|
//#endregion
|
|
2589
|
-
//#region src/helpers/
|
|
2873
|
+
//#region src/helpers/core/install-dependencies.ts
|
|
2590
2874
|
async function installDependencies({ projectDir, packageManager }) {
|
|
2591
2875
|
const s = spinner();
|
|
2592
2876
|
try {
|
|
@@ -2603,7 +2887,7 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
2603
2887
|
}
|
|
2604
2888
|
|
|
2605
2889
|
//#endregion
|
|
2606
|
-
//#region src/helpers/
|
|
2890
|
+
//#region src/helpers/core/add-addons.ts
|
|
2607
2891
|
async function addAddonsToProject(input) {
|
|
2608
2892
|
try {
|
|
2609
2893
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -2628,7 +2912,8 @@ async function addAddonsToProject(input) {
|
|
|
2628
2912
|
install: input.install || false,
|
|
2629
2913
|
dbSetup: detectedConfig.dbSetup || "none",
|
|
2630
2914
|
api: detectedConfig.api || "none",
|
|
2631
|
-
webDeploy: detectedConfig.webDeploy || "none"
|
|
2915
|
+
webDeploy: detectedConfig.webDeploy || "none",
|
|
2916
|
+
serverDeploy: detectedConfig.serverDeploy || "none"
|
|
2632
2917
|
};
|
|
2633
2918
|
for (const addon of input.addons) {
|
|
2634
2919
|
const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
|
|
@@ -2651,12 +2936,92 @@ async function addAddonsToProject(input) {
|
|
|
2651
2936
|
}
|
|
2652
2937
|
|
|
2653
2938
|
//#endregion
|
|
2654
|
-
//#region src/helpers/
|
|
2655
|
-
async function
|
|
2939
|
+
//#region src/helpers/deployment/server-deploy-setup.ts
|
|
2940
|
+
async function setupServerDeploy(config) {
|
|
2941
|
+
const { serverDeploy, webDeploy, projectDir } = config;
|
|
2942
|
+
const { packageManager } = config;
|
|
2943
|
+
if (serverDeploy === "none") return;
|
|
2944
|
+
if (serverDeploy === "alchemy" && webDeploy === "alchemy") return;
|
|
2945
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
2946
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2947
|
+
if (serverDeploy === "wrangler") {
|
|
2948
|
+
await setupWorkersServerDeploy(serverDir, packageManager);
|
|
2949
|
+
await generateCloudflareWorkerTypes({
|
|
2950
|
+
serverDir,
|
|
2951
|
+
packageManager
|
|
2952
|
+
});
|
|
2953
|
+
} else if (serverDeploy === "alchemy") await setupAlchemyServerDeploy(serverDir, packageManager);
|
|
2954
|
+
}
|
|
2955
|
+
async function setupWorkersServerDeploy(serverDir, _packageManager) {
|
|
2956
|
+
const packageJsonPath = path.join(serverDir, "package.json");
|
|
2957
|
+
if (!await fs.pathExists(packageJsonPath)) return;
|
|
2958
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
2959
|
+
packageJson.scripts = {
|
|
2960
|
+
...packageJson.scripts,
|
|
2961
|
+
dev: "wrangler dev --port=3000",
|
|
2962
|
+
start: "wrangler dev",
|
|
2963
|
+
deploy: "wrangler deploy",
|
|
2964
|
+
build: "wrangler deploy --dry-run",
|
|
2965
|
+
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
|
|
2966
|
+
};
|
|
2967
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
2968
|
+
await addPackageDependency({
|
|
2969
|
+
devDependencies: [
|
|
2970
|
+
"wrangler",
|
|
2971
|
+
"@types/node",
|
|
2972
|
+
"@cloudflare/workers-types"
|
|
2973
|
+
],
|
|
2974
|
+
projectDir: serverDir
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
async function generateCloudflareWorkerTypes({ serverDir, packageManager }) {
|
|
2978
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2979
|
+
const s = spinner();
|
|
2980
|
+
try {
|
|
2981
|
+
s.start("Generating Cloudflare Workers types...");
|
|
2982
|
+
const runCmd = packageManager === "npm" ? "npm" : packageManager;
|
|
2983
|
+
await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
|
|
2984
|
+
s.stop("Cloudflare Workers types generated successfully!");
|
|
2985
|
+
} catch {
|
|
2986
|
+
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
|
2987
|
+
const managerCmd = `${packageManager} run`;
|
|
2988
|
+
log.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
async function setupAlchemyServerDeploy(serverDir, _packageManager) {
|
|
2992
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2993
|
+
await addPackageDependency({
|
|
2994
|
+
devDependencies: [
|
|
2995
|
+
"alchemy",
|
|
2996
|
+
"wrangler",
|
|
2997
|
+
"@types/node",
|
|
2998
|
+
"@cloudflare/workers-types",
|
|
2999
|
+
"dotenv"
|
|
3000
|
+
],
|
|
3001
|
+
projectDir: serverDir
|
|
3002
|
+
});
|
|
3003
|
+
const packageJsonPath = path.join(serverDir, "package.json");
|
|
3004
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
3005
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
3006
|
+
packageJson.scripts = {
|
|
3007
|
+
...packageJson.scripts,
|
|
3008
|
+
dev: "wrangler dev --port=3000",
|
|
3009
|
+
build: "wrangler deploy --dry-run",
|
|
3010
|
+
deploy: "alchemy deploy",
|
|
3011
|
+
destroy: "alchemy destroy",
|
|
3012
|
+
"alchemy:dev": "alchemy dev"
|
|
3013
|
+
};
|
|
3014
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
//#endregion
|
|
3019
|
+
//#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
|
|
3020
|
+
async function setupNextAlchemyDeploy(projectDir, _packageManager) {
|
|
2656
3021
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2657
3022
|
if (!await fs.pathExists(webAppDir)) return;
|
|
2658
3023
|
await addPackageDependency({
|
|
2659
|
-
devDependencies: ["
|
|
3024
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
2660
3025
|
projectDir: webAppDir
|
|
2661
3026
|
});
|
|
2662
3027
|
const pkgPath = path.join(webAppDir, "package.json");
|
|
@@ -2664,43 +3029,518 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
|
2664
3029
|
const pkg = await fs.readJson(pkgPath);
|
|
2665
3030
|
pkg.scripts = {
|
|
2666
3031
|
...pkg.scripts,
|
|
2667
|
-
deploy:
|
|
2668
|
-
|
|
3032
|
+
deploy: "alchemy deploy",
|
|
3033
|
+
destroy: "alchemy destroy",
|
|
3034
|
+
"alchemy:dev": "alchemy dev"
|
|
3035
|
+
};
|
|
3036
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
//#endregion
|
|
3041
|
+
//#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
|
|
3042
|
+
async function setupNuxtAlchemyDeploy(projectDir, _packageManager) {
|
|
3043
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3044
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3045
|
+
await addPackageDependency({
|
|
3046
|
+
devDependencies: [
|
|
3047
|
+
"alchemy",
|
|
3048
|
+
"nitro-cloudflare-dev",
|
|
3049
|
+
"dotenv"
|
|
3050
|
+
],
|
|
3051
|
+
projectDir: webAppDir
|
|
3052
|
+
});
|
|
3053
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3054
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3055
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3056
|
+
pkg.scripts = {
|
|
3057
|
+
...pkg.scripts,
|
|
3058
|
+
deploy: "alchemy deploy",
|
|
3059
|
+
destroy: "alchemy destroy",
|
|
3060
|
+
"alchemy:dev": "alchemy dev"
|
|
2669
3061
|
};
|
|
2670
3062
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2671
3063
|
}
|
|
2672
3064
|
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
2673
3065
|
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
3066
|
+
try {
|
|
3067
|
+
const project = new Project({ manipulationSettings: {
|
|
3068
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3069
|
+
quoteKind: QuoteKind.Double
|
|
3070
|
+
} });
|
|
3071
|
+
project.addSourceFileAtPath(nuxtConfigPath);
|
|
3072
|
+
const sourceFile = project.getSourceFileOrThrow(nuxtConfigPath);
|
|
3073
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3074
|
+
if (!exportAssignment) return;
|
|
3075
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3076
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
|
|
3077
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3078
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3079
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3080
|
+
if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
|
|
3081
|
+
name: "nitro",
|
|
3082
|
+
initializer: `{
|
|
2691
3083
|
preset: "cloudflare_module",
|
|
2692
3084
|
cloudflare: {
|
|
2693
3085
|
deployConfig: true,
|
|
2694
3086
|
nodeCompat: true
|
|
2695
3087
|
}
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3088
|
+
}`
|
|
3089
|
+
});
|
|
3090
|
+
const modulesProperty = configObject.getProperty("modules");
|
|
3091
|
+
if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
|
|
3092
|
+
const initializer = modulesProperty.getInitializer();
|
|
3093
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3094
|
+
const hasModule = initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'");
|
|
3095
|
+
if (!hasModule) initializer.addElement("\"nitro-cloudflare-dev\"");
|
|
3096
|
+
}
|
|
3097
|
+
} else if (!modulesProperty) configObject.addPropertyAssignment({
|
|
3098
|
+
name: "modules",
|
|
3099
|
+
initializer: "[\"nitro-cloudflare-dev\"]"
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
await project.save();
|
|
3103
|
+
} catch (error) {
|
|
3104
|
+
console.warn("Failed to update nuxt.config.ts:", error);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
//#endregion
|
|
3109
|
+
//#region src/helpers/deployment/alchemy/alchemy-react-router-setup.ts
|
|
3110
|
+
async function setupReactRouterAlchemyDeploy(projectDir, _packageManager) {
|
|
3111
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3112
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3113
|
+
await addPackageDependency({
|
|
3114
|
+
devDependencies: [
|
|
3115
|
+
"alchemy",
|
|
3116
|
+
"@cloudflare/vite-plugin",
|
|
3117
|
+
"dotenv"
|
|
3118
|
+
],
|
|
3119
|
+
projectDir: webAppDir
|
|
3120
|
+
});
|
|
3121
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3122
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3123
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3124
|
+
pkg.scripts = {
|
|
3125
|
+
...pkg.scripts,
|
|
3126
|
+
deploy: "alchemy deploy",
|
|
3127
|
+
destroy: "alchemy destroy",
|
|
3128
|
+
"alchemy:dev": "alchemy dev"
|
|
3129
|
+
};
|
|
3130
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3131
|
+
}
|
|
3132
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3133
|
+
if (await fs.pathExists(viteConfigPath)) try {
|
|
3134
|
+
const project = new Project({ manipulationSettings: {
|
|
3135
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3136
|
+
quoteKind: QuoteKind.Double
|
|
3137
|
+
} });
|
|
3138
|
+
project.addSourceFileAtPath(viteConfigPath);
|
|
3139
|
+
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
|
3140
|
+
const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/react-router");
|
|
3141
|
+
if (!alchemyImport) sourceFile.addImportDeclaration({
|
|
3142
|
+
moduleSpecifier: "alchemy/cloudflare/react-router",
|
|
3143
|
+
defaultImport: "alchemy"
|
|
3144
|
+
});
|
|
3145
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3146
|
+
if (!exportAssignment) return;
|
|
3147
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3148
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
|
|
3149
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3150
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3151
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3152
|
+
const pluginsProperty = configObject.getProperty("plugins");
|
|
3153
|
+
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
|
3154
|
+
const initializer = pluginsProperty.getInitializer();
|
|
3155
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3156
|
+
const hasCloudflarePlugin = initializer.getElements().some((el) => el.getText().includes("cloudflare("));
|
|
3157
|
+
if (!hasCloudflarePlugin) initializer.addElement("alchemy()");
|
|
3158
|
+
}
|
|
3159
|
+
} else if (!pluginsProperty) configObject.addPropertyAssignment({
|
|
3160
|
+
name: "plugins",
|
|
3161
|
+
initializer: "[alchemy()]"
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
await project.save();
|
|
3165
|
+
} catch (error) {
|
|
3166
|
+
console.warn("Failed to update vite.config.ts:", error);
|
|
3167
|
+
}
|
|
3168
|
+
const reactRouterConfigPath = path.join(webAppDir, "react-router.config.ts");
|
|
3169
|
+
if (await fs.pathExists(reactRouterConfigPath)) try {
|
|
3170
|
+
const project = new Project({ manipulationSettings: {
|
|
3171
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3172
|
+
quoteKind: QuoteKind.Double
|
|
3173
|
+
} });
|
|
3174
|
+
project.addSourceFileAtPath(reactRouterConfigPath);
|
|
3175
|
+
const sourceFile = project.getSourceFileOrThrow(reactRouterConfigPath);
|
|
3176
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3177
|
+
if (!exportAssignment) return;
|
|
3178
|
+
const configExpression = exportAssignment.getExpression();
|
|
3179
|
+
let configObject;
|
|
3180
|
+
if (Node.isObjectLiteralExpression(configExpression)) configObject = configExpression;
|
|
3181
|
+
else if (Node.isSatisfiesExpression(configExpression)) {
|
|
3182
|
+
const expression = configExpression.getExpression();
|
|
3183
|
+
if (Node.isObjectLiteralExpression(expression)) configObject = expression;
|
|
3184
|
+
}
|
|
3185
|
+
if (!configObject || !Node.isObjectLiteralExpression(configObject)) return;
|
|
3186
|
+
const futureProperty = configObject.getProperty("future");
|
|
3187
|
+
if (!futureProperty) configObject.addPropertyAssignment({
|
|
3188
|
+
name: "future",
|
|
3189
|
+
initializer: `{
|
|
3190
|
+
unstable_viteEnvironmentApi: true,
|
|
3191
|
+
}`
|
|
3192
|
+
});
|
|
3193
|
+
else if (Node.isPropertyAssignment(futureProperty)) {
|
|
3194
|
+
const futureInitializer = futureProperty.getInitializer();
|
|
3195
|
+
if (Node.isObjectLiteralExpression(futureInitializer)) {
|
|
3196
|
+
const viteEnvApiProp = futureInitializer.getProperty("unstable_viteEnvironmentApi");
|
|
3197
|
+
if (!viteEnvApiProp) futureInitializer.addPropertyAssignment({
|
|
3198
|
+
name: "unstable_viteEnvironmentApi",
|
|
3199
|
+
initializer: "true"
|
|
3200
|
+
});
|
|
3201
|
+
else if (Node.isPropertyAssignment(viteEnvApiProp)) {
|
|
3202
|
+
const value = viteEnvApiProp.getInitializer()?.getText();
|
|
3203
|
+
if (value === "false") viteEnvApiProp.setInitializer("true");
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
await project.save();
|
|
3208
|
+
} catch (error) {
|
|
3209
|
+
console.warn("Failed to update react-router.config.ts:", error);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
//#endregion
|
|
3214
|
+
//#region src/helpers/deployment/alchemy/alchemy-solid-setup.ts
|
|
3215
|
+
async function setupSolidAlchemyDeploy(projectDir, _packageManager) {
|
|
3216
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3217
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3218
|
+
await addPackageDependency({
|
|
3219
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3220
|
+
projectDir: webAppDir
|
|
3221
|
+
});
|
|
3222
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3223
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3224
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3225
|
+
pkg.scripts = {
|
|
3226
|
+
...pkg.scripts,
|
|
3227
|
+
deploy: "alchemy deploy",
|
|
3228
|
+
destroy: "alchemy destroy",
|
|
3229
|
+
"alchemy:dev": "alchemy dev"
|
|
3230
|
+
};
|
|
3231
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
//#endregion
|
|
3236
|
+
//#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
|
|
3237
|
+
async function setupSvelteAlchemyDeploy(projectDir, _packageManager) {
|
|
3238
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3239
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3240
|
+
await addPackageDependency({
|
|
3241
|
+
devDependencies: [
|
|
3242
|
+
"alchemy",
|
|
3243
|
+
"@sveltejs/adapter-cloudflare",
|
|
3244
|
+
"dotenv"
|
|
3245
|
+
],
|
|
3246
|
+
projectDir: webAppDir
|
|
3247
|
+
});
|
|
3248
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3249
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3250
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3251
|
+
pkg.scripts = {
|
|
3252
|
+
...pkg.scripts,
|
|
3253
|
+
deploy: "alchemy deploy",
|
|
3254
|
+
destroy: "alchemy destroy",
|
|
3255
|
+
"alchemy:dev": "alchemy dev"
|
|
3256
|
+
};
|
|
3257
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3258
|
+
}
|
|
3259
|
+
const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
|
|
3260
|
+
if (!await fs.pathExists(svelteConfigPath)) return;
|
|
3261
|
+
try {
|
|
3262
|
+
const project = new Project({ manipulationSettings: {
|
|
3263
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3264
|
+
quoteKind: QuoteKind.Single
|
|
3265
|
+
} });
|
|
3266
|
+
project.addSourceFileAtPath(svelteConfigPath);
|
|
3267
|
+
const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
|
|
3268
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
3269
|
+
const adapterImport = importDeclarations.find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
|
|
3270
|
+
if (adapterImport) {
|
|
3271
|
+
adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
|
|
3272
|
+
adapterImport.removeDefaultImport();
|
|
3273
|
+
adapterImport.setDefaultImport("alchemy");
|
|
3274
|
+
} else sourceFile.insertImportDeclaration(0, {
|
|
3275
|
+
moduleSpecifier: "alchemy/cloudflare/sveltekit",
|
|
3276
|
+
defaultImport: "alchemy"
|
|
3277
|
+
});
|
|
3278
|
+
const configVariable = sourceFile.getVariableDeclaration("config");
|
|
3279
|
+
if (configVariable) {
|
|
3280
|
+
const initializer = configVariable.getInitializer();
|
|
3281
|
+
if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
|
|
3282
|
+
}
|
|
3283
|
+
await project.save();
|
|
3284
|
+
} catch (error) {
|
|
3285
|
+
console.warn("Failed to update svelte.config.js:", error);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
function updateAdapterInConfig(configObject) {
|
|
3289
|
+
if (!Node.isObjectLiteralExpression(configObject)) return;
|
|
3290
|
+
const kitProperty = configObject.getProperty("kit");
|
|
3291
|
+
if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
|
|
3292
|
+
const kitInitializer = kitProperty.getInitializer();
|
|
3293
|
+
if (Node.isObjectLiteralExpression(kitInitializer)) {
|
|
3294
|
+
const adapterProperty = kitInitializer.getProperty("adapter");
|
|
3295
|
+
if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
|
|
3296
|
+
const initializer = adapterProperty.getInitializer();
|
|
3297
|
+
if (Node.isCallExpression(initializer)) {
|
|
3298
|
+
const expression = initializer.getExpression();
|
|
3299
|
+
if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
//#endregion
|
|
3307
|
+
//#region src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts
|
|
3308
|
+
async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager) {
|
|
3309
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3310
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3311
|
+
await addPackageDependency({
|
|
3312
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3313
|
+
projectDir: webAppDir
|
|
3314
|
+
});
|
|
3315
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3316
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3317
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3318
|
+
pkg.scripts = {
|
|
3319
|
+
...pkg.scripts,
|
|
3320
|
+
deploy: "alchemy deploy",
|
|
3321
|
+
destroy: "alchemy destroy",
|
|
3322
|
+
"alchemy:dev": "alchemy dev"
|
|
3323
|
+
};
|
|
3324
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
//#endregion
|
|
3329
|
+
//#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
|
|
3330
|
+
async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager) {
|
|
3331
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3332
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3333
|
+
await addPackageDependency({
|
|
3334
|
+
devDependencies: [
|
|
3335
|
+
"alchemy",
|
|
3336
|
+
"nitropack",
|
|
3337
|
+
"dotenv"
|
|
3338
|
+
],
|
|
3339
|
+
projectDir: webAppDir
|
|
3340
|
+
});
|
|
3341
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3342
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3343
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3344
|
+
pkg.scripts = {
|
|
3345
|
+
...pkg.scripts,
|
|
3346
|
+
deploy: "alchemy deploy",
|
|
3347
|
+
destroy: "alchemy destroy",
|
|
3348
|
+
"alchemy:dev": "alchemy dev"
|
|
3349
|
+
};
|
|
3350
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3351
|
+
}
|
|
3352
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3353
|
+
if (await fs.pathExists(viteConfigPath)) try {
|
|
3354
|
+
const project = new Project({ manipulationSettings: {
|
|
3355
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3356
|
+
quoteKind: QuoteKind.Double
|
|
3357
|
+
} });
|
|
3358
|
+
project.addSourceFileAtPath(viteConfigPath);
|
|
3359
|
+
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
|
3360
|
+
const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
|
|
3361
|
+
if (!alchemyImport) sourceFile.addImportDeclaration({
|
|
3362
|
+
moduleSpecifier: "alchemy/cloudflare/tanstack-start",
|
|
3363
|
+
defaultImport: "alchemy"
|
|
3364
|
+
});
|
|
3365
|
+
else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
|
|
3366
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3367
|
+
if (!exportAssignment) return;
|
|
3368
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3369
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
|
|
3370
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3371
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3372
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3373
|
+
if (!configObject.getProperty("build")) configObject.addPropertyAssignment({
|
|
3374
|
+
name: "build",
|
|
3375
|
+
initializer: `{
|
|
3376
|
+
target: "esnext",
|
|
3377
|
+
rollupOptions: {
|
|
3378
|
+
external: ["node:async_hooks", "cloudflare:workers"],
|
|
3379
|
+
},
|
|
3380
|
+
}`
|
|
3381
|
+
});
|
|
3382
|
+
const pluginsProperty = configObject.getProperty("plugins");
|
|
3383
|
+
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
|
3384
|
+
const initializer = pluginsProperty.getInitializer();
|
|
3385
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3386
|
+
const hasShim = initializer.getElements().some((el) => el.getText().includes("alchemy"));
|
|
3387
|
+
if (!hasShim) initializer.addElement("alchemy()");
|
|
3388
|
+
const tanstackElements = initializer.getElements().filter((el) => el.getText().includes("tanstackStart"));
|
|
3389
|
+
tanstackElements.forEach((element) => {
|
|
3390
|
+
if (Node.isCallExpression(element)) {
|
|
3391
|
+
const args = element.getArguments();
|
|
3392
|
+
if (args.length === 0) element.addArgument(`{
|
|
3393
|
+
target: "cloudflare-module",
|
|
3394
|
+
customViteReactPlugin: true,
|
|
3395
|
+
}`);
|
|
3396
|
+
else if (args.length === 1 && Node.isObjectLiteralExpression(args[0])) {
|
|
3397
|
+
const configObj = args[0];
|
|
3398
|
+
if (!configObj.getProperty("target")) configObj.addPropertyAssignment({
|
|
3399
|
+
name: "target",
|
|
3400
|
+
initializer: "\"cloudflare-module\""
|
|
3401
|
+
});
|
|
3402
|
+
if (!configObj.getProperty("customViteReactPlugin")) configObj.addPropertyAssignment({
|
|
3403
|
+
name: "customViteReactPlugin",
|
|
3404
|
+
initializer: "true"
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
} else configObject.addPropertyAssignment({
|
|
3411
|
+
name: "plugins",
|
|
3412
|
+
initializer: "[alchemy()]"
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
await project.save();
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
console.warn("Failed to update vite.config.ts:", error);
|
|
3418
|
+
}
|
|
3419
|
+
const nitroConfigPath = path.join(webAppDir, "nitro.config.ts");
|
|
3420
|
+
const nitroConfigContent = `import { defineNitroConfig } from "nitropack/config";
|
|
3421
|
+
|
|
3422
|
+
export default defineNitroConfig({
|
|
3423
|
+
preset: "cloudflare-module",
|
|
3424
|
+
cloudflare: {
|
|
3425
|
+
nodeCompat: true,
|
|
3426
|
+
},
|
|
3427
|
+
});
|
|
3428
|
+
`;
|
|
3429
|
+
await fs.writeFile(nitroConfigPath, nitroConfigContent, "utf-8");
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
//#endregion
|
|
3433
|
+
//#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
|
|
3434
|
+
async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
|
|
3435
|
+
await addPackageDependency({
|
|
3436
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3437
|
+
projectDir
|
|
3438
|
+
});
|
|
3439
|
+
const rootPkgPath = path.join(projectDir, "package.json");
|
|
3440
|
+
if (await fs.pathExists(rootPkgPath)) {
|
|
3441
|
+
const pkg = await fs.readJson(rootPkgPath);
|
|
3442
|
+
pkg.scripts = {
|
|
3443
|
+
...pkg.scripts,
|
|
3444
|
+
deploy: "alchemy deploy",
|
|
3445
|
+
destroy: "alchemy destroy",
|
|
3446
|
+
"alchemy:dev": "alchemy dev"
|
|
3447
|
+
};
|
|
3448
|
+
await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
|
|
3449
|
+
}
|
|
3450
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3451
|
+
if (await fs.pathExists(serverDir)) await setupAlchemyServerDeploy(serverDir, packageManager);
|
|
3452
|
+
const frontend = config.frontend;
|
|
3453
|
+
const isNext = frontend.includes("next");
|
|
3454
|
+
const isNuxt = frontend.includes("nuxt");
|
|
3455
|
+
const isSvelte = frontend.includes("svelte");
|
|
3456
|
+
const isTanstackRouter = frontend.includes("tanstack-router");
|
|
3457
|
+
const isTanstackStart = frontend.includes("tanstack-start");
|
|
3458
|
+
const isReactRouter = frontend.includes("react-router");
|
|
3459
|
+
const isSolid = frontend.includes("solid");
|
|
3460
|
+
if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
|
|
3461
|
+
else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
|
3462
|
+
else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
|
3463
|
+
else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
|
3464
|
+
else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
|
3465
|
+
else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
|
3466
|
+
else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
//#endregion
|
|
3470
|
+
//#region src/helpers/deployment/workers/workers-next-setup.ts
|
|
3471
|
+
async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
3472
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3473
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3474
|
+
await addPackageDependency({
|
|
3475
|
+
dependencies: ["@opennextjs/cloudflare"],
|
|
3476
|
+
devDependencies: ["wrangler"],
|
|
3477
|
+
projectDir: webAppDir
|
|
3478
|
+
});
|
|
3479
|
+
const packageJsonPath = path.join(webAppDir, "package.json");
|
|
3480
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
3481
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
3482
|
+
pkg.scripts = {
|
|
3483
|
+
...pkg.scripts,
|
|
3484
|
+
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
3485
|
+
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
3486
|
+
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
|
3487
|
+
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
3488
|
+
};
|
|
3489
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
//#endregion
|
|
3494
|
+
//#region src/helpers/deployment/workers/workers-nuxt-setup.ts
|
|
3495
|
+
async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
3496
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3497
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3498
|
+
await addPackageDependency({
|
|
3499
|
+
devDependencies: ["nitro-cloudflare-dev", "wrangler"],
|
|
3500
|
+
projectDir: webAppDir
|
|
3501
|
+
});
|
|
3502
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3503
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3504
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3505
|
+
pkg.scripts = {
|
|
3506
|
+
...pkg.scripts,
|
|
3507
|
+
deploy: `${packageManager} run build && wrangler deploy`,
|
|
3508
|
+
"cf-typegen": "wrangler types"
|
|
3509
|
+
};
|
|
3510
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3511
|
+
}
|
|
3512
|
+
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
3513
|
+
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
3514
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
|
|
3515
|
+
if (!sourceFile) return;
|
|
3516
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
3517
|
+
const expression = expr.getExpression();
|
|
3518
|
+
return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
|
|
3519
|
+
});
|
|
3520
|
+
if (!defineCall) return;
|
|
3521
|
+
const configObj = defineCall.getArguments()[0];
|
|
3522
|
+
if (!configObj) return;
|
|
3523
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3524
|
+
const compatProp = configObj.getProperty("compatibilityDate");
|
|
3525
|
+
if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
|
|
3526
|
+
else configObj.addPropertyAssignment({
|
|
3527
|
+
name: "compatibilityDate",
|
|
3528
|
+
initializer: `'${today}'`
|
|
3529
|
+
});
|
|
3530
|
+
const nitroInitializer = `{
|
|
3531
|
+
preset: "cloudflare_module",
|
|
3532
|
+
cloudflare: {
|
|
3533
|
+
deployConfig: true,
|
|
3534
|
+
nodeCompat: true
|
|
3535
|
+
}
|
|
3536
|
+
}`;
|
|
3537
|
+
const nitroProp = configObj.getProperty("nitro");
|
|
3538
|
+
if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
|
|
3539
|
+
else configObj.addPropertyAssignment({
|
|
3540
|
+
name: "nitro",
|
|
3541
|
+
initializer: nitroInitializer
|
|
3542
|
+
});
|
|
3543
|
+
const modulesProp = configObj.getProperty("modules");
|
|
2704
3544
|
if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
2705
3545
|
const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
|
|
2706
3546
|
if (arrayExpr) {
|
|
@@ -2715,7 +3555,7 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
|
2715
3555
|
}
|
|
2716
3556
|
|
|
2717
3557
|
//#endregion
|
|
2718
|
-
//#region src/helpers/
|
|
3558
|
+
//#region src/helpers/deployment/workers/workers-svelte-setup.ts
|
|
2719
3559
|
async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
2720
3560
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2721
3561
|
if (!await fs.pathExists(webAppDir)) return;
|
|
@@ -2752,7 +3592,7 @@ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
|
2752
3592
|
}
|
|
2753
3593
|
|
|
2754
3594
|
//#endregion
|
|
2755
|
-
//#region src/helpers/
|
|
3595
|
+
//#region src/helpers/deployment/workers/workers-tanstack-start-setup.ts
|
|
2756
3596
|
async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
2757
3597
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2758
3598
|
if (!await fs.pathExists(webAppDir)) return;
|
|
@@ -2790,7 +3630,7 @@ async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
|
2790
3630
|
}
|
|
2791
3631
|
|
|
2792
3632
|
//#endregion
|
|
2793
|
-
//#region src/helpers/
|
|
3633
|
+
//#region src/helpers/deployment/workers/workers-vite-setup.ts
|
|
2794
3634
|
async function setupWorkersVitePlugin(projectDir) {
|
|
2795
3635
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2796
3636
|
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
@@ -2821,12 +3661,16 @@ async function setupWorkersVitePlugin(projectDir) {
|
|
|
2821
3661
|
}
|
|
2822
3662
|
|
|
2823
3663
|
//#endregion
|
|
2824
|
-
//#region src/helpers/
|
|
3664
|
+
//#region src/helpers/deployment/web-deploy-setup.ts
|
|
2825
3665
|
async function setupWebDeploy(config) {
|
|
2826
|
-
const { webDeploy, frontend, projectDir } = config;
|
|
3666
|
+
const { webDeploy, serverDeploy, frontend, projectDir } = config;
|
|
2827
3667
|
const { packageManager } = config;
|
|
2828
3668
|
if (webDeploy === "none") return;
|
|
2829
|
-
if (webDeploy !== "
|
|
3669
|
+
if (webDeploy !== "wrangler" && webDeploy !== "alchemy") return;
|
|
3670
|
+
if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
|
|
3671
|
+
await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
2830
3674
|
const isNext = frontend.includes("next");
|
|
2831
3675
|
const isNuxt = frontend.includes("nuxt");
|
|
2832
3676
|
const isSvelte = frontend.includes("svelte");
|
|
@@ -2834,11 +3678,21 @@ async function setupWebDeploy(config) {
|
|
|
2834
3678
|
const isTanstackStart = frontend.includes("tanstack-start");
|
|
2835
3679
|
const isReactRouter = frontend.includes("react-router");
|
|
2836
3680
|
const isSolid = frontend.includes("solid");
|
|
2837
|
-
if (
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
3681
|
+
if (webDeploy === "wrangler") {
|
|
3682
|
+
if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
|
|
3683
|
+
else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
|
|
3684
|
+
else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
|
|
3685
|
+
else if (isTanstackStart) await setupTanstackStartWorkersDeploy(projectDir, packageManager);
|
|
3686
|
+
else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
|
|
3687
|
+
} else if (webDeploy === "alchemy") {
|
|
3688
|
+
if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
|
|
3689
|
+
else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
|
3690
|
+
else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
|
3691
|
+
else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
|
3692
|
+
else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
|
3693
|
+
else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
|
3694
|
+
else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
|
|
3695
|
+
}
|
|
2842
3696
|
}
|
|
2843
3697
|
async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
2844
3698
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
@@ -2855,30 +3709,9 @@ async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
|
2855
3709
|
}
|
|
2856
3710
|
await setupWorkersVitePlugin(projectDir);
|
|
2857
3711
|
}
|
|
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
3712
|
|
|
2880
3713
|
//#endregion
|
|
2881
|
-
//#region src/helpers/
|
|
3714
|
+
//#region src/helpers/core/add-deployment.ts
|
|
2882
3715
|
async function addDeploymentToProject(input) {
|
|
2883
3716
|
try {
|
|
2884
3717
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -2886,7 +3719,8 @@ async function addDeploymentToProject(input) {
|
|
|
2886
3719
|
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
3720
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
2888
3721
|
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.`);
|
|
3722
|
+
if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} web deployment is already configured for this project.`);
|
|
3723
|
+
if (input.serverDeploy && detectedConfig.serverDeploy === input.serverDeploy) exitWithError(`${input.serverDeploy} server deployment is already configured for this project.`);
|
|
2890
3724
|
const config = {
|
|
2891
3725
|
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
2892
3726
|
projectDir,
|
|
@@ -2903,224 +3737,79 @@ async function addDeploymentToProject(input) {
|
|
|
2903
3737
|
packageManager: input.packageManager || detectedConfig.packageManager || "npm",
|
|
2904
3738
|
install: input.install || false,
|
|
2905
3739
|
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) {}
|
|
3740
|
+
api: detectedConfig.api || "none",
|
|
3741
|
+
webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
|
|
3742
|
+
serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none"
|
|
3110
3743
|
};
|
|
3111
|
-
if (
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3744
|
+
if (input.webDeploy && input.webDeploy !== "none") log.info(pc.green(`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`));
|
|
3745
|
+
if (input.serverDeploy && input.serverDeploy !== "none") log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
|
|
3746
|
+
await setupDeploymentTemplates(projectDir, config);
|
|
3747
|
+
await setupWebDeploy(config);
|
|
3748
|
+
await setupServerDeploy(config);
|
|
3749
|
+
await updateBtsConfig(projectDir, {
|
|
3750
|
+
webDeploy: input.webDeploy || config.webDeploy,
|
|
3751
|
+
serverDeploy: input.serverDeploy || config.serverDeploy
|
|
3752
|
+
});
|
|
3753
|
+
if (config.install) await installDependencies({
|
|
3754
|
+
projectDir,
|
|
3755
|
+
packageManager: config.packageManager
|
|
3756
|
+
});
|
|
3757
|
+
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
3758
|
+
} catch (error) {
|
|
3759
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3760
|
+
exitWithError(`Error adding deployment: ${message}`);
|
|
3119
3761
|
}
|
|
3120
3762
|
}
|
|
3121
3763
|
|
|
3122
3764
|
//#endregion
|
|
3123
|
-
//#region src/
|
|
3765
|
+
//#region src/utils/format-with-biome.ts
|
|
3766
|
+
async function formatProjectWithBiome(projectDir) {
|
|
3767
|
+
const biome = new Biome();
|
|
3768
|
+
const { projectKey } = biome.openProject(projectDir);
|
|
3769
|
+
biome.applyConfiguration(projectKey, {
|
|
3770
|
+
formatter: {
|
|
3771
|
+
enabled: true,
|
|
3772
|
+
indentStyle: "tab"
|
|
3773
|
+
},
|
|
3774
|
+
javascript: { formatter: { quoteStyle: "double" } }
|
|
3775
|
+
});
|
|
3776
|
+
const files = await glob("**/*", {
|
|
3777
|
+
cwd: projectDir,
|
|
3778
|
+
dot: true,
|
|
3779
|
+
absolute: true,
|
|
3780
|
+
onlyFiles: true
|
|
3781
|
+
});
|
|
3782
|
+
for (const filePath of files) try {
|
|
3783
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
3784
|
+
const supported = new Set([
|
|
3785
|
+
".ts",
|
|
3786
|
+
".tsx",
|
|
3787
|
+
".js",
|
|
3788
|
+
".jsx",
|
|
3789
|
+
".cjs",
|
|
3790
|
+
".mjs",
|
|
3791
|
+
".cts",
|
|
3792
|
+
".mts",
|
|
3793
|
+
".json",
|
|
3794
|
+
".jsonc",
|
|
3795
|
+
".md",
|
|
3796
|
+
".mdx",
|
|
3797
|
+
".css",
|
|
3798
|
+
".scss",
|
|
3799
|
+
".html"
|
|
3800
|
+
]);
|
|
3801
|
+
if (!supported.has(ext)) continue;
|
|
3802
|
+
const original = await fs.readFile(filePath, "utf8");
|
|
3803
|
+
const result = biome.formatContent(projectKey, original, { filePath });
|
|
3804
|
+
const content = result?.content;
|
|
3805
|
+
if (typeof content !== "string") continue;
|
|
3806
|
+
if (content.length === 0 && original.length > 0) continue;
|
|
3807
|
+
if (content !== original) await fs.writeFile(filePath, content);
|
|
3808
|
+
} catch {}
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
//#endregion
|
|
3812
|
+
//#region src/helpers/addons/auth-setup.ts
|
|
3124
3813
|
async function setupAuth(config) {
|
|
3125
3814
|
const { auth, frontend, backend, projectDir } = config;
|
|
3126
3815
|
if (backend === "convex" || !auth) return;
|
|
@@ -3172,7 +3861,217 @@ function generateAuthSecret(length = 32) {
|
|
|
3172
3861
|
}
|
|
3173
3862
|
|
|
3174
3863
|
//#endregion
|
|
3175
|
-
//#region src/helpers/
|
|
3864
|
+
//#region src/helpers/addons/examples-setup.ts
|
|
3865
|
+
async function setupExamples(config) {
|
|
3866
|
+
const { examples, frontend, backend, projectDir } = config;
|
|
3867
|
+
if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
|
|
3868
|
+
if (examples.includes("ai")) {
|
|
3869
|
+
const webClientDir = path.join(projectDir, "apps/web");
|
|
3870
|
+
const nativeClientDir = path.join(projectDir, "apps/native");
|
|
3871
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3872
|
+
const webClientDirExists = await fs.pathExists(webClientDir);
|
|
3873
|
+
const nativeClientDirExists = await fs.pathExists(nativeClientDir);
|
|
3874
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
3875
|
+
const hasNuxt = frontend.includes("nuxt");
|
|
3876
|
+
const hasSvelte = frontend.includes("svelte");
|
|
3877
|
+
const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
|
|
3878
|
+
const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3879
|
+
if (webClientDirExists) {
|
|
3880
|
+
const dependencies = ["ai"];
|
|
3881
|
+
if (hasNuxt) dependencies.push("@ai-sdk/vue");
|
|
3882
|
+
else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
|
|
3883
|
+
else if (hasReactWeb) dependencies.push("@ai-sdk/react");
|
|
3884
|
+
await addPackageDependency({
|
|
3885
|
+
dependencies,
|
|
3886
|
+
projectDir: webClientDir
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
if (nativeClientDirExists && hasReactNative) await addPackageDependency({
|
|
3890
|
+
dependencies: ["ai", "@ai-sdk/react"],
|
|
3891
|
+
projectDir: nativeClientDir
|
|
3892
|
+
});
|
|
3893
|
+
if (serverDirExists && backend !== "none") await addPackageDependency({
|
|
3894
|
+
dependencies: ["ai", "@ai-sdk/google"],
|
|
3895
|
+
projectDir: serverDir
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
//#endregion
|
|
3901
|
+
//#region src/helpers/core/api-setup.ts
|
|
3902
|
+
async function addBackendWorkspaceDependency(projectDir, backendPackageName, workspaceVersion) {
|
|
3903
|
+
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
3904
|
+
try {
|
|
3905
|
+
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
3906
|
+
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
3907
|
+
pkgJson.dependencies[backendPackageName] = workspaceVersion;
|
|
3908
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
3909
|
+
} catch (_error) {}
|
|
3910
|
+
}
|
|
3911
|
+
function getFrontendType(frontend) {
|
|
3912
|
+
const reactBasedFrontends = [
|
|
3913
|
+
"tanstack-router",
|
|
3914
|
+
"react-router",
|
|
3915
|
+
"tanstack-start",
|
|
3916
|
+
"next"
|
|
3917
|
+
];
|
|
3918
|
+
const nativeFrontends = ["native-nativewind", "native-unistyles"];
|
|
3919
|
+
return {
|
|
3920
|
+
hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
|
|
3921
|
+
hasNuxtWeb: frontend.includes("nuxt"),
|
|
3922
|
+
hasSvelteWeb: frontend.includes("svelte"),
|
|
3923
|
+
hasSolidWeb: frontend.includes("solid"),
|
|
3924
|
+
hasNative: frontend.some((f) => nativeFrontends.includes(f))
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
function getApiDependencies(api, frontendType) {
|
|
3928
|
+
const deps = {};
|
|
3929
|
+
if (api === "orpc") deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
|
|
3930
|
+
else if (api === "trpc") deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
|
|
3931
|
+
if (frontendType.hasReactWeb) {
|
|
3932
|
+
if (api === "orpc") deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
|
3933
|
+
else if (api === "trpc") deps.web = { dependencies: [
|
|
3934
|
+
"@trpc/tanstack-react-query",
|
|
3935
|
+
"@trpc/client",
|
|
3936
|
+
"@trpc/server"
|
|
3937
|
+
] };
|
|
3938
|
+
} else if (frontendType.hasNuxtWeb && api === "orpc") deps.web = {
|
|
3939
|
+
dependencies: [
|
|
3940
|
+
"@tanstack/vue-query",
|
|
3941
|
+
"@orpc/tanstack-query",
|
|
3942
|
+
"@orpc/client"
|
|
3943
|
+
],
|
|
3944
|
+
devDependencies: ["@tanstack/vue-query-devtools"]
|
|
3945
|
+
};
|
|
3946
|
+
else if (frontendType.hasSvelteWeb && api === "orpc") deps.web = {
|
|
3947
|
+
dependencies: [
|
|
3948
|
+
"@orpc/tanstack-query",
|
|
3949
|
+
"@orpc/client",
|
|
3950
|
+
"@tanstack/svelte-query"
|
|
3951
|
+
],
|
|
3952
|
+
devDependencies: ["@tanstack/svelte-query-devtools"]
|
|
3953
|
+
};
|
|
3954
|
+
else if (frontendType.hasSolidWeb && api === "orpc") deps.web = {
|
|
3955
|
+
dependencies: [
|
|
3956
|
+
"@orpc/tanstack-query",
|
|
3957
|
+
"@orpc/client",
|
|
3958
|
+
"@tanstack/solid-query"
|
|
3959
|
+
],
|
|
3960
|
+
devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
|
|
3961
|
+
};
|
|
3962
|
+
if (api === "trpc") deps.native = { dependencies: [
|
|
3963
|
+
"@trpc/tanstack-react-query",
|
|
3964
|
+
"@trpc/client",
|
|
3965
|
+
"@trpc/server"
|
|
3966
|
+
] };
|
|
3967
|
+
else if (api === "orpc") deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
|
3968
|
+
return deps;
|
|
3969
|
+
}
|
|
3970
|
+
function getQueryDependencies(frontend) {
|
|
3971
|
+
const reactBasedFrontends = [
|
|
3972
|
+
"react-router",
|
|
3973
|
+
"tanstack-router",
|
|
3974
|
+
"tanstack-start",
|
|
3975
|
+
"next",
|
|
3976
|
+
"native-nativewind",
|
|
3977
|
+
"native-unistyles"
|
|
3978
|
+
];
|
|
3979
|
+
const deps = {};
|
|
3980
|
+
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
|
3981
|
+
if (needsReactQuery) {
|
|
3982
|
+
const hasReactWeb = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
|
|
3983
|
+
const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3984
|
+
if (hasReactWeb) deps.web = {
|
|
3985
|
+
dependencies: ["@tanstack/react-query"],
|
|
3986
|
+
devDependencies: ["@tanstack/react-query-devtools"]
|
|
3987
|
+
};
|
|
3988
|
+
if (hasNative) deps.native = { dependencies: ["@tanstack/react-query"] };
|
|
3989
|
+
}
|
|
3990
|
+
if (frontend.includes("solid")) deps.web = {
|
|
3991
|
+
dependencies: ["@tanstack/solid-query"],
|
|
3992
|
+
devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
|
|
3993
|
+
};
|
|
3994
|
+
return deps;
|
|
3995
|
+
}
|
|
3996
|
+
function getConvexDependencies(frontend) {
|
|
3997
|
+
const deps = {
|
|
3998
|
+
web: { dependencies: ["convex"] },
|
|
3999
|
+
native: { dependencies: ["convex"] }
|
|
4000
|
+
};
|
|
4001
|
+
if (frontend.includes("tanstack-start")) deps.web.dependencies.push("@convex-dev/react-query");
|
|
4002
|
+
if (frontend.includes("svelte")) deps.web.dependencies.push("convex-svelte");
|
|
4003
|
+
if (frontend.includes("nuxt")) deps.web.dependencies.push("convex-nuxt", "convex-vue");
|
|
4004
|
+
return deps;
|
|
4005
|
+
}
|
|
4006
|
+
async function setupApi(config) {
|
|
4007
|
+
const { api, projectName, frontend, backend, packageManager, projectDir } = config;
|
|
4008
|
+
const isConvex = backend === "convex";
|
|
4009
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
4010
|
+
const nativeDir = path.join(projectDir, "apps/native");
|
|
4011
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
4012
|
+
const webDirExists = await fs.pathExists(webDir);
|
|
4013
|
+
const nativeDirExists = await fs.pathExists(nativeDir);
|
|
4014
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
4015
|
+
const frontendType = getFrontendType(frontend);
|
|
4016
|
+
if (!isConvex && api !== "none") {
|
|
4017
|
+
const apiDeps = getApiDependencies(api, frontendType);
|
|
4018
|
+
if (serverDirExists && apiDeps.server) {
|
|
4019
|
+
await addPackageDependency({
|
|
4020
|
+
dependencies: apiDeps.server.dependencies,
|
|
4021
|
+
projectDir: serverDir
|
|
4022
|
+
});
|
|
4023
|
+
if (api === "trpc") {
|
|
4024
|
+
if (backend === "hono") await addPackageDependency({
|
|
4025
|
+
dependencies: ["@hono/trpc-server"],
|
|
4026
|
+
projectDir: serverDir
|
|
4027
|
+
});
|
|
4028
|
+
else if (backend === "elysia") await addPackageDependency({
|
|
4029
|
+
dependencies: ["@elysiajs/trpc"],
|
|
4030
|
+
projectDir: serverDir
|
|
4031
|
+
});
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
if (webDirExists && apiDeps.web) await addPackageDependency({
|
|
4035
|
+
dependencies: apiDeps.web.dependencies,
|
|
4036
|
+
devDependencies: apiDeps.web.devDependencies,
|
|
4037
|
+
projectDir: webDir
|
|
4038
|
+
});
|
|
4039
|
+
if (nativeDirExists && apiDeps.native) await addPackageDependency({
|
|
4040
|
+
dependencies: apiDeps.native.dependencies,
|
|
4041
|
+
projectDir: nativeDir
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
if (!isConvex) {
|
|
4045
|
+
const queryDeps = getQueryDependencies(frontend);
|
|
4046
|
+
if (webDirExists && queryDeps.web) await addPackageDependency({
|
|
4047
|
+
dependencies: queryDeps.web.dependencies,
|
|
4048
|
+
devDependencies: queryDeps.web.devDependencies,
|
|
4049
|
+
projectDir: webDir
|
|
4050
|
+
});
|
|
4051
|
+
if (nativeDirExists && queryDeps.native) await addPackageDependency({
|
|
4052
|
+
dependencies: queryDeps.native.dependencies,
|
|
4053
|
+
projectDir: nativeDir
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
4056
|
+
if (isConvex) {
|
|
4057
|
+
const convexDeps = getConvexDependencies(frontend);
|
|
4058
|
+
if (webDirExists) await addPackageDependency({
|
|
4059
|
+
dependencies: convexDeps.web.dependencies,
|
|
4060
|
+
projectDir: webDir
|
|
4061
|
+
});
|
|
4062
|
+
if (nativeDirExists) await addPackageDependency({
|
|
4063
|
+
dependencies: convexDeps.native.dependencies,
|
|
4064
|
+
projectDir: nativeDir
|
|
4065
|
+
});
|
|
4066
|
+
const backendPackageName = `@${projectName}/backend`;
|
|
4067
|
+
const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
|
|
4068
|
+
if (webDirExists) await addBackendWorkspaceDependency(webDir, backendPackageName, backendWorkspaceVersion);
|
|
4069
|
+
if (nativeDirExists) await addBackendWorkspaceDependency(nativeDir, backendPackageName, backendWorkspaceVersion);
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
//#endregion
|
|
4074
|
+
//#region src/helpers/core/backend-setup.ts
|
|
3176
4075
|
async function setupBackendDependencies(config) {
|
|
3177
4076
|
const { backend, runtime, api, projectDir } = config;
|
|
3178
4077
|
if (backend === "convex") return;
|
|
@@ -3211,7 +4110,7 @@ async function setupBackendDependencies(config) {
|
|
|
3211
4110
|
}
|
|
3212
4111
|
|
|
3213
4112
|
//#endregion
|
|
3214
|
-
//#region src/helpers/
|
|
4113
|
+
//#region src/helpers/core/env-setup.ts
|
|
3215
4114
|
async function addEnvVariablesToFile(filePath, variables) {
|
|
3216
4115
|
await fs.ensureDir(path.dirname(filePath));
|
|
3217
4116
|
let envContent = "";
|
|
@@ -3259,7 +4158,7 @@ async function addEnvVariablesToFile(filePath, variables) {
|
|
|
3259
4158
|
if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
|
|
3260
4159
|
}
|
|
3261
4160
|
async function setupEnvironmentVariables(config) {
|
|
3262
|
-
const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
|
|
4161
|
+
const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
|
|
3263
4162
|
const hasReactRouter = frontend.includes("react-router");
|
|
3264
4163
|
const hasTanStackRouter = frontend.includes("tanstack-router");
|
|
3265
4164
|
const hasTanStackStart = frontend.includes("tanstack-start");
|
|
@@ -3359,39 +4258,69 @@ async function setupEnvironmentVariables(config) {
|
|
|
3359
4258
|
}
|
|
3360
4259
|
];
|
|
3361
4260
|
await addEnvVariablesToFile(envPath, serverVars);
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
4261
|
+
const isUnifiedAlchemy = webDeploy === "alchemy" && serverDeploy === "alchemy";
|
|
4262
|
+
const isIndividualAlchemy = webDeploy === "alchemy" || serverDeploy === "alchemy";
|
|
4263
|
+
if (isUnifiedAlchemy) {
|
|
4264
|
+
const rootEnvPath = path.join(projectDir, ".env");
|
|
4265
|
+
const rootAlchemyVars = [{
|
|
4266
|
+
key: "ALCHEMY_PASSWORD",
|
|
4267
|
+
value: "please-change-this",
|
|
4268
|
+
condition: true
|
|
4269
|
+
}];
|
|
4270
|
+
await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
|
|
4271
|
+
} else if (isIndividualAlchemy) {
|
|
4272
|
+
if (webDeploy === "alchemy") {
|
|
4273
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
4274
|
+
if (await fs.pathExists(webDir)) {
|
|
4275
|
+
const webAlchemyVars = [{
|
|
4276
|
+
key: "ALCHEMY_PASSWORD",
|
|
4277
|
+
value: "please-change-this",
|
|
4278
|
+
condition: true
|
|
4279
|
+
}];
|
|
4280
|
+
await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
if (serverDeploy === "alchemy") {
|
|
4284
|
+
const serverDir$1 = path.join(projectDir, "apps/server");
|
|
4285
|
+
if (await fs.pathExists(serverDir$1)) {
|
|
4286
|
+
const serverAlchemyVars = [{
|
|
4287
|
+
key: "ALCHEMY_PASSWORD",
|
|
4288
|
+
value: "please-change-this",
|
|
4289
|
+
condition: true
|
|
4290
|
+
}];
|
|
4291
|
+
await addEnvVariablesToFile(path.join(serverDir$1, ".env"), serverAlchemyVars);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
3367
4294
|
}
|
|
3368
4295
|
}
|
|
3369
4296
|
|
|
3370
4297
|
//#endregion
|
|
3371
4298
|
//#region src/helpers/database-providers/d1-setup.ts
|
|
3372
4299
|
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
|
-
|
|
4300
|
+
const { projectDir, serverDeploy } = config;
|
|
4301
|
+
if (serverDeploy === "wrangler") {
|
|
4302
|
+
const envPath = path.join(projectDir, "apps/server", ".env");
|
|
4303
|
+
const variables = [
|
|
4304
|
+
{
|
|
4305
|
+
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
4306
|
+
value: "",
|
|
4307
|
+
condition: true
|
|
4308
|
+
},
|
|
4309
|
+
{
|
|
4310
|
+
key: "CLOUDFLARE_DATABASE_ID",
|
|
4311
|
+
value: "",
|
|
4312
|
+
condition: true
|
|
4313
|
+
},
|
|
4314
|
+
{
|
|
4315
|
+
key: "CLOUDFLARE_D1_TOKEN",
|
|
4316
|
+
value: "",
|
|
4317
|
+
condition: true
|
|
4318
|
+
}
|
|
4319
|
+
];
|
|
4320
|
+
try {
|
|
4321
|
+
await addEnvVariablesToFile(envPath, variables);
|
|
4322
|
+
} catch (_err) {}
|
|
4323
|
+
}
|
|
3395
4324
|
}
|
|
3396
4325
|
|
|
3397
4326
|
//#endregion
|
|
@@ -3839,8 +4768,10 @@ async function setupPrismaPostgres(config) {
|
|
|
3839
4768
|
else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
|
|
3840
4769
|
if (prismaConfig) {
|
|
3841
4770
|
await writeEnvFile$1(projectDir, prismaConfig);
|
|
3842
|
-
|
|
3843
|
-
|
|
4771
|
+
if (orm === "prisma") {
|
|
4772
|
+
await addDotenvImportToPrismaConfig(projectDir);
|
|
4773
|
+
await addPrismaAccelerateExtension(serverDir);
|
|
4774
|
+
}
|
|
3844
4775
|
log.success(pc.green("Prisma Postgres database configured successfully!"));
|
|
3845
4776
|
} else {
|
|
3846
4777
|
await writeEnvFile$1(projectDir);
|
|
@@ -4185,7 +5116,7 @@ async function setupTurso(config) {
|
|
|
4185
5116
|
}
|
|
4186
5117
|
|
|
4187
5118
|
//#endregion
|
|
4188
|
-
//#region src/helpers/
|
|
5119
|
+
//#region src/helpers/core/db-setup.ts
|
|
4189
5120
|
async function setupDatabase(config) {
|
|
4190
5121
|
const { database, orm, dbSetup, backend, projectDir } = config;
|
|
4191
5122
|
if (backend === "convex" || database === "none") {
|
|
@@ -4250,44 +5181,7 @@ async function setupDatabase(config) {
|
|
|
4250
5181
|
}
|
|
4251
5182
|
|
|
4252
5183
|
//#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
|
|
5184
|
+
//#region src/helpers/core/runtime-setup.ts
|
|
4291
5185
|
async function setupRuntime(config) {
|
|
4292
5186
|
const { runtime, backend, projectDir } = config;
|
|
4293
5187
|
if (backend === "convex" || backend === "next" || runtime === "none") return;
|
|
@@ -4295,23 +5189,6 @@ async function setupRuntime(config) {
|
|
|
4295
5189
|
if (!await fs.pathExists(serverDir)) return;
|
|
4296
5190
|
if (runtime === "bun") await setupBunRuntime(serverDir, backend);
|
|
4297
5191
|
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
5192
|
}
|
|
4316
5193
|
async function setupBunRuntime(serverDir, _backend) {
|
|
4317
5194
|
const packageJsonPath = path.join(serverDir, "package.json");
|
|
@@ -4351,27 +5228,20 @@ async function setupNodeRuntime(serverDir, backend) {
|
|
|
4351
5228
|
projectDir: serverDir
|
|
4352
5229
|
});
|
|
4353
5230
|
}
|
|
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
|
|
5231
|
+
|
|
5232
|
+
//#endregion
|
|
5233
|
+
//#region src/helpers/core/convex-codegen.ts
|
|
5234
|
+
async function runConvexCodegen(projectDir, packageManager) {
|
|
5235
|
+
const backendDir = path.join(projectDir, "packages/backend");
|
|
5236
|
+
const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
|
|
5237
|
+
await execa(cmd, {
|
|
5238
|
+
cwd: backendDir,
|
|
5239
|
+
shell: true
|
|
4370
5240
|
});
|
|
4371
5241
|
}
|
|
4372
5242
|
|
|
4373
5243
|
//#endregion
|
|
4374
|
-
//#region src/helpers/
|
|
5244
|
+
//#region src/helpers/core/create-readme.ts
|
|
4375
5245
|
async function createReadme(projectDir, options) {
|
|
4376
5246
|
const readmePath = path.join(projectDir, "README.md");
|
|
4377
5247
|
const content = generateReadmeContent(options);
|
|
@@ -4648,7 +5518,7 @@ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNati
|
|
|
4648
5518
|
}
|
|
4649
5519
|
|
|
4650
5520
|
//#endregion
|
|
4651
|
-
//#region src/helpers/
|
|
5521
|
+
//#region src/helpers/core/git.ts
|
|
4652
5522
|
async function initializeGit(projectDir, useGit) {
|
|
4653
5523
|
if (!useGit) return;
|
|
4654
5524
|
const gitVersionResult = await $({
|
|
@@ -4724,20 +5594,21 @@ async function getDockerStatus(database) {
|
|
|
4724
5594
|
}
|
|
4725
5595
|
|
|
4726
5596
|
//#endregion
|
|
4727
|
-
//#region src/helpers/
|
|
5597
|
+
//#region src/helpers/core/post-installation.ts
|
|
4728
5598
|
async function displayPostInstallInstructions(config) {
|
|
4729
|
-
const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy } = config;
|
|
5599
|
+
const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy } = config;
|
|
4730
5600
|
const isConvex = backend === "convex";
|
|
4731
5601
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
4732
5602
|
const cdCmd = `cd ${relativePath}`;
|
|
4733
5603
|
const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
|
|
4734
|
-
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
|
|
5604
|
+
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
|
|
4735
5605
|
const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
|
|
4736
5606
|
const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
|
|
4737
5607
|
const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
|
|
4738
5608
|
const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
|
|
4739
5609
|
const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
|
|
4740
|
-
const
|
|
5610
|
+
const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
5611
|
+
const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
4741
5612
|
const hasWeb = frontend?.some((f) => [
|
|
4742
5613
|
"tanstack-router",
|
|
4743
5614
|
"react-router",
|
|
@@ -4766,8 +5637,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
4766
5637
|
if (runtime === "workers") {
|
|
4767
5638
|
if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
|
|
4768
5639
|
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
4769
|
-
output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd}
|
|
4770
|
-
}
|
|
5640
|
+
if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
|
|
5641
|
+
}
|
|
4771
5642
|
}
|
|
4772
5643
|
output += `${pc.bold("Your project will be available at:")}\n`;
|
|
4773
5644
|
if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
|
|
@@ -4780,7 +5651,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
4780
5651
|
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
|
4781
5652
|
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
|
|
4782
5653
|
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
|
|
4783
|
-
if (
|
|
5654
|
+
if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
|
|
5655
|
+
if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
|
|
4784
5656
|
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
|
4785
5657
|
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
|
4786
5658
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
|
@@ -4801,7 +5673,7 @@ function getNativeInstructions(isConvex) {
|
|
|
4801
5673
|
function getLintingInstructions(runCmd) {
|
|
4802
5674
|
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
|
|
4803
5675
|
}
|
|
4804
|
-
async function getDatabaseInstructions(database, orm, runCmd,
|
|
5676
|
+
async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
|
|
4805
5677
|
const instructions = [];
|
|
4806
5678
|
if (dbSetup === "docker") {
|
|
4807
5679
|
const dockerStatus = await getDockerStatus(database);
|
|
@@ -4810,7 +5682,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4810
5682
|
instructions.push("");
|
|
4811
5683
|
}
|
|
4812
5684
|
}
|
|
4813
|
-
if (
|
|
5685
|
+
if (serverDeploy === "wrangler" && dbSetup === "d1") {
|
|
4814
5686
|
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
|
|
4815
5687
|
instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
|
|
4816
5688
|
instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
|
|
@@ -4818,8 +5690,8 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4818
5690
|
instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white(`cd apps/server && ${packageManager} db:generate`)}`);
|
|
4819
5691
|
instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
|
|
4820
5692
|
instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
|
|
4821
|
-
instructions.push("");
|
|
4822
5693
|
}
|
|
5694
|
+
if (dbSetup === "d1" && serverDeploy === "alchemy") {}
|
|
4823
5695
|
if (orm === "prisma") {
|
|
4824
5696
|
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
5697
|
if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
|
|
@@ -4828,7 +5700,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4828
5700
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
|
4829
5701
|
} else if (orm === "drizzle") {
|
|
4830
5702
|
if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
|
|
4831
|
-
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
5703
|
+
if (dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
4832
5704
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
|
4833
5705
|
if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
|
|
4834
5706
|
} else if (orm === "mongoose") {
|
|
@@ -4851,12 +5723,22 @@ function getNoOrmWarning() {
|
|
|
4851
5723
|
function getBunWebNativeWarning() {
|
|
4852
5724
|
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
|
|
4853
5725
|
}
|
|
4854
|
-
function
|
|
4855
|
-
|
|
5726
|
+
function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5727
|
+
const instructions = [];
|
|
5728
|
+
if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
|
|
5729
|
+
if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
|
|
5730
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
5731
|
+
}
|
|
5732
|
+
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5733
|
+
const instructions = [];
|
|
5734
|
+
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
|
|
5735
|
+
else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
|
|
5736
|
+
else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
|
|
5737
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
4856
5738
|
}
|
|
4857
5739
|
|
|
4858
5740
|
//#endregion
|
|
4859
|
-
//#region src/helpers/
|
|
5741
|
+
//#region src/helpers/core/project-config.ts
|
|
4860
5742
|
async function updatePackageConfigurations(projectDir, options) {
|
|
4861
5743
|
await updateRootPackageJson(projectDir, options);
|
|
4862
5744
|
if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
|
|
@@ -5037,7 +5919,7 @@ async function updateConvexPackageJson(projectDir, options) {
|
|
|
5037
5919
|
}
|
|
5038
5920
|
|
|
5039
5921
|
//#endregion
|
|
5040
|
-
//#region src/helpers/
|
|
5922
|
+
//#region src/helpers/core/create-project.ts
|
|
5041
5923
|
async function createProject(options) {
|
|
5042
5924
|
const projectDir = options.projectDir;
|
|
5043
5925
|
const isConvex = options.backend === "convex";
|
|
@@ -5065,18 +5947,18 @@ async function createProject(options) {
|
|
|
5065
5947
|
if (!isConvex && options.auth) await setupAuth(options);
|
|
5066
5948
|
await handleExtras(projectDir, options);
|
|
5067
5949
|
await setupWebDeploy(options);
|
|
5950
|
+
await setupServerDeploy(options);
|
|
5068
5951
|
await setupEnvironmentVariables(options);
|
|
5069
5952
|
await updatePackageConfigurations(projectDir, options);
|
|
5070
5953
|
await createReadme(projectDir, options);
|
|
5071
5954
|
await writeBtsConfig(options);
|
|
5955
|
+
await formatProjectWithBiome(projectDir);
|
|
5956
|
+
if (isConvex) await runConvexCodegen(projectDir, options.packageManager);
|
|
5072
5957
|
log.success("Project template successfully scaffolded!");
|
|
5073
|
-
if (options.install) {
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
});
|
|
5078
|
-
await generateCloudflareWorkerTypes(options);
|
|
5079
|
-
}
|
|
5958
|
+
if (options.install) await installDependencies({
|
|
5959
|
+
projectDir,
|
|
5960
|
+
packageManager: options.packageManager
|
|
5961
|
+
});
|
|
5080
5962
|
await initializeGit(projectDir, options.git);
|
|
5081
5963
|
await displayPostInstallInstructions({
|
|
5082
5964
|
...options,
|
|
@@ -5095,7 +5977,7 @@ async function createProject(options) {
|
|
|
5095
5977
|
}
|
|
5096
5978
|
|
|
5097
5979
|
//#endregion
|
|
5098
|
-
//#region src/helpers/
|
|
5980
|
+
//#region src/helpers/core/command-handlers.ts
|
|
5099
5981
|
async function createProjectHandler(input) {
|
|
5100
5982
|
const startTime = Date.now();
|
|
5101
5983
|
const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5105,10 +5987,11 @@ async function createProjectHandler(input) {
|
|
|
5105
5987
|
let currentPathInput;
|
|
5106
5988
|
if (input.yes && input.projectName) currentPathInput = input.projectName;
|
|
5107
5989
|
else if (input.yes) {
|
|
5108
|
-
|
|
5990
|
+
const defaultConfig = getDefaultConfig();
|
|
5991
|
+
let defaultName = defaultConfig.relativePath;
|
|
5109
5992
|
let counter = 1;
|
|
5110
|
-
while (fs.
|
|
5111
|
-
defaultName = `${
|
|
5993
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
5994
|
+
defaultName = `${defaultConfig.projectName}-${counter}`;
|
|
5112
5995
|
counter++;
|
|
5113
5996
|
}
|
|
5114
5997
|
currentPathInput = defaultName;
|
|
@@ -5146,7 +6029,8 @@ async function createProjectHandler(input) {
|
|
|
5146
6029
|
install: false,
|
|
5147
6030
|
dbSetup: "none",
|
|
5148
6031
|
api: "none",
|
|
5149
|
-
webDeploy: "none"
|
|
6032
|
+
webDeploy: "none",
|
|
6033
|
+
serverDeploy: "none"
|
|
5150
6034
|
},
|
|
5151
6035
|
reproducibleCommand: "",
|
|
5152
6036
|
timeScaffolded,
|
|
@@ -5162,28 +6046,33 @@ async function createProjectHandler(input) {
|
|
|
5162
6046
|
projectDirectory: input.projectName
|
|
5163
6047
|
};
|
|
5164
6048
|
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
6049
|
let config;
|
|
5173
6050
|
if (input.yes) {
|
|
6051
|
+
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
5174
6052
|
config = {
|
|
5175
|
-
...
|
|
6053
|
+
...getDefaultConfig(),
|
|
5176
6054
|
...flagConfig,
|
|
5177
6055
|
projectName: finalBaseName,
|
|
5178
6056
|
projectDir: finalResolvedPath,
|
|
5179
6057
|
relativePath: finalPathInput
|
|
5180
6058
|
};
|
|
6059
|
+
coerceBackendPresets(config);
|
|
6060
|
+
validateConfigCompatibility(config, providedFlags, cliInput);
|
|
5181
6061
|
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
6062
|
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
6063
|
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
5184
6064
|
log.message(displayConfig(config));
|
|
5185
6065
|
log.message("");
|
|
5186
|
-
} else
|
|
6066
|
+
} else {
|
|
6067
|
+
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
6068
|
+
const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
|
|
6069
|
+
if (Object.keys(otherFlags).length > 0) {
|
|
6070
|
+
log.info(pc.yellow("Using these pre-selected options:"));
|
|
6071
|
+
log.message(displayConfig(otherFlags));
|
|
6072
|
+
log.message("");
|
|
6073
|
+
}
|
|
6074
|
+
config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
|
|
6075
|
+
}
|
|
5187
6076
|
await createProject(config);
|
|
5188
6077
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
5189
6078
|
log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
@@ -5203,11 +6092,11 @@ async function createProjectHandler(input) {
|
|
|
5203
6092
|
}
|
|
5204
6093
|
async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
|
|
5205
6094
|
const currentPath = path.resolve(process.cwd(), currentPathInput);
|
|
5206
|
-
if (!fs.
|
|
6095
|
+
if (!await fs.pathExists(currentPath)) return {
|
|
5207
6096
|
finalPathInput: currentPathInput,
|
|
5208
6097
|
shouldClearDirectory: false
|
|
5209
6098
|
};
|
|
5210
|
-
const dirContents = fs.
|
|
6099
|
+
const dirContents = await fs.readdir(currentPath);
|
|
5211
6100
|
const isNotEmpty = dirContents.length > 0;
|
|
5212
6101
|
if (!isNotEmpty) return {
|
|
5213
6102
|
finalPathInput: currentPathInput,
|
|
@@ -5226,7 +6115,7 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
|
|
|
5226
6115
|
let counter = 1;
|
|
5227
6116
|
const baseName = currentPathInput;
|
|
5228
6117
|
let finalPathInput = `${baseName}-${counter}`;
|
|
5229
|
-
while (fs.
|
|
6118
|
+
while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
|
|
5230
6119
|
counter++;
|
|
5231
6120
|
finalPathInput = `${baseName}-${counter}`;
|
|
5232
6121
|
}
|
|
@@ -5252,6 +6141,10 @@ async function addAddonsHandler(input) {
|
|
|
5252
6141
|
const deploymentPrompt = await getDeploymentToAdd(detectedConfig.frontend || [], detectedConfig.webDeploy);
|
|
5253
6142
|
if (deploymentPrompt !== "none") input.webDeploy = deploymentPrompt;
|
|
5254
6143
|
}
|
|
6144
|
+
if (!input.serverDeploy) {
|
|
6145
|
+
const serverDeploymentPrompt = await getServerDeploymentToAdd(detectedConfig.runtime, detectedConfig.serverDeploy);
|
|
6146
|
+
if (serverDeploymentPrompt !== "none") input.serverDeploy = serverDeploymentPrompt;
|
|
6147
|
+
}
|
|
5255
6148
|
const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
|
|
5256
6149
|
let somethingAdded = false;
|
|
5257
6150
|
if (input.addons && input.addons.length > 0) {
|
|
@@ -5272,6 +6165,15 @@ async function addAddonsHandler(input) {
|
|
|
5272
6165
|
});
|
|
5273
6166
|
somethingAdded = true;
|
|
5274
6167
|
}
|
|
6168
|
+
if (input.serverDeploy && input.serverDeploy !== "none") {
|
|
6169
|
+
await addDeploymentToProject({
|
|
6170
|
+
...input,
|
|
6171
|
+
install: false,
|
|
6172
|
+
suppressInstallMessage: true,
|
|
6173
|
+
serverDeploy: input.serverDeploy
|
|
6174
|
+
});
|
|
6175
|
+
somethingAdded = true;
|
|
6176
|
+
}
|
|
5275
6177
|
if (!somethingAdded) {
|
|
5276
6178
|
outro(pc.yellow("No addons or deployment configurations to add."));
|
|
5277
6179
|
return;
|
|
@@ -5375,6 +6277,7 @@ const router = t.router({
|
|
|
5375
6277
|
runtime: RuntimeSchema.optional(),
|
|
5376
6278
|
api: APISchema.optional(),
|
|
5377
6279
|
webDeploy: WebDeploySchema.optional(),
|
|
6280
|
+
serverDeploy: ServerDeploySchema.optional(),
|
|
5378
6281
|
directoryConflict: DirectoryConflictSchema.optional(),
|
|
5379
6282
|
renderTitle: z.boolean().optional(),
|
|
5380
6283
|
disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics")
|
|
@@ -5390,6 +6293,7 @@ const router = t.router({
|
|
|
5390
6293
|
add: t.procedure.meta({ description: "Add addons or deployment configurations to an existing Better-T Stack project" }).input(z.tuple([z.object({
|
|
5391
6294
|
addons: z.array(AddonsSchema).optional().default([]),
|
|
5392
6295
|
webDeploy: WebDeploySchema.optional(),
|
|
6296
|
+
serverDeploy: ServerDeploySchema.optional(),
|
|
5393
6297
|
projectDir: z.string().optional(),
|
|
5394
6298
|
install: z.boolean().optional().default(false).describe("Install dependencies after adding addons or deployment"),
|
|
5395
6299
|
packageManager: PackageManagerSchema.optional()
|