create-better-t-stack 2.33.8 → 2.33.9-canary.7db03e6d
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-CEmXO3U_.js} +1340 -518
- 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/deploy/alchemy/alchemy.run.ts.hbs +200 -0
- package/templates/deploy/alchemy/env.d.ts.hbs +20 -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 +4 -4
- 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
|
|
@@ -45,7 +46,8 @@ const DEFAULT_CONFIG = {
|
|
|
45
46
|
backend: "hono",
|
|
46
47
|
runtime: "bun",
|
|
47
48
|
api: "trpc",
|
|
48
|
-
webDeploy: "none"
|
|
49
|
+
webDeploy: "none",
|
|
50
|
+
serverDeploy: "none"
|
|
49
51
|
};
|
|
50
52
|
const dependencyVersionMap = {
|
|
51
53
|
"better-auth": "^1.3.4",
|
|
@@ -103,18 +105,24 @@ const dependencyVersionMap = {
|
|
|
103
105
|
"convex-svelte": "^0.0.11",
|
|
104
106
|
"convex-nuxt": "0.1.5",
|
|
105
107
|
"convex-vue": "^0.1.5",
|
|
106
|
-
"@tanstack/svelte-query": "^5.
|
|
108
|
+
"@tanstack/svelte-query": "^5.85.3",
|
|
109
|
+
"@tanstack/svelte-query-devtools": "^5.85.3",
|
|
107
110
|
"@tanstack/vue-query-devtools": "^5.83.0",
|
|
108
111
|
"@tanstack/vue-query": "^5.83.0",
|
|
109
112
|
"@tanstack/react-query-devtools": "^5.80.5",
|
|
110
113
|
"@tanstack/react-query": "^5.80.5",
|
|
111
114
|
"@tanstack/solid-query": "^5.75.0",
|
|
112
115
|
"@tanstack/solid-query-devtools": "^5.75.0",
|
|
116
|
+
"@tanstack/solid-router-devtools": "^1.131.25",
|
|
113
117
|
wrangler: "^4.23.0",
|
|
114
118
|
"@cloudflare/vite-plugin": "^1.9.0",
|
|
115
119
|
"@opennextjs/cloudflare": "^1.3.0",
|
|
116
120
|
"nitro-cloudflare-dev": "^0.2.2",
|
|
117
|
-
"@sveltejs/adapter-cloudflare": "^7.
|
|
121
|
+
"@sveltejs/adapter-cloudflare": "^7.2.1",
|
|
122
|
+
"@cloudflare/workers-types": "^4.20250813.0",
|
|
123
|
+
alchemy: "^0.62.1",
|
|
124
|
+
nitropack: "^2.12.4",
|
|
125
|
+
dotenv: "^17.2.1"
|
|
118
126
|
};
|
|
119
127
|
const ADDON_COMPATIBILITY = {
|
|
120
128
|
pwa: [
|
|
@@ -128,7 +136,8 @@ const ADDON_COMPATIBILITY = {
|
|
|
128
136
|
"react-router",
|
|
129
137
|
"nuxt",
|
|
130
138
|
"svelte",
|
|
131
|
-
"solid"
|
|
139
|
+
"solid",
|
|
140
|
+
"next"
|
|
132
141
|
],
|
|
133
142
|
biome: [],
|
|
134
143
|
husky: [],
|
|
@@ -233,7 +242,16 @@ const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(
|
|
|
233
242
|
];
|
|
234
243
|
return !invalidChars.some((char) => name.includes(char));
|
|
235
244
|
}, "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([
|
|
245
|
+
const WebDeploySchema = z.enum([
|
|
246
|
+
"wrangler",
|
|
247
|
+
"alchemy",
|
|
248
|
+
"none"
|
|
249
|
+
]).describe("Web deployment");
|
|
250
|
+
const ServerDeploySchema = z.enum([
|
|
251
|
+
"wrangler",
|
|
252
|
+
"alchemy",
|
|
253
|
+
"none"
|
|
254
|
+
]).describe("Server deployment");
|
|
237
255
|
const DirectoryConflictSchema = z.enum([
|
|
238
256
|
"merge",
|
|
239
257
|
"overwrite",
|
|
@@ -544,6 +562,23 @@ function isExampleAIAllowed(backend, frontends = []) {
|
|
|
544
562
|
function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
|
|
545
563
|
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
546
564
|
}
|
|
565
|
+
function validateServerDeployRequiresBackend(serverDeploy, backend) {
|
|
566
|
+
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
|
|
567
|
+
}
|
|
568
|
+
function validateAddonsAgainstFrontends(addons = [], frontends = []) {
|
|
569
|
+
for (const addon of addons) {
|
|
570
|
+
if (addon === "none") continue;
|
|
571
|
+
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends);
|
|
572
|
+
if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function validateExamplesCompatibility(examples, backend, database, frontend) {
|
|
576
|
+
const examplesArr = examples ?? [];
|
|
577
|
+
if (examplesArr.length === 0 || examplesArr.includes("none")) return;
|
|
578
|
+
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.");
|
|
579
|
+
if (examplesArr.includes("ai") && backend === "elysia") exitWithError("The 'ai' example is not compatible with the Elysia backend.");
|
|
580
|
+
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
|
|
581
|
+
}
|
|
547
582
|
|
|
548
583
|
//#endregion
|
|
549
584
|
//#region src/prompts/api.ts
|
|
@@ -1004,16 +1039,97 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
1004
1039
|
return response;
|
|
1005
1040
|
}
|
|
1006
1041
|
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/prompts/server-deploy.ts
|
|
1044
|
+
function getDeploymentDisplay$1(deployment) {
|
|
1045
|
+
if (deployment === "wrangler") return {
|
|
1046
|
+
label: "Wrangler",
|
|
1047
|
+
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
1048
|
+
};
|
|
1049
|
+
if (deployment === "alchemy") return {
|
|
1050
|
+
label: "Alchemy",
|
|
1051
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
1052
|
+
};
|
|
1053
|
+
return {
|
|
1054
|
+
label: deployment,
|
|
1055
|
+
hint: `Add ${deployment} deployment`
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
async function getServerDeploymentChoice(deployment, runtime, backend, webDeploy) {
|
|
1059
|
+
if (deployment !== void 0) return deployment;
|
|
1060
|
+
if (backend === "none" || backend === "convex") return "none";
|
|
1061
|
+
const options = [];
|
|
1062
|
+
if (runtime === "workers") ["alchemy", "wrangler"].forEach((deploy) => {
|
|
1063
|
+
const { label, hint } = getDeploymentDisplay$1(deploy);
|
|
1064
|
+
options.unshift({
|
|
1065
|
+
value: deploy,
|
|
1066
|
+
label,
|
|
1067
|
+
hint
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
else options.push({
|
|
1071
|
+
value: "none",
|
|
1072
|
+
label: "None",
|
|
1073
|
+
hint: "Manual setup"
|
|
1074
|
+
});
|
|
1075
|
+
const response = await select({
|
|
1076
|
+
message: "Select server deployment",
|
|
1077
|
+
options,
|
|
1078
|
+
initialValue: webDeploy === "alchemy" ? "alchemy" : runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
|
|
1079
|
+
});
|
|
1080
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1081
|
+
return response;
|
|
1082
|
+
}
|
|
1083
|
+
async function getServerDeploymentToAdd(runtime, existingDeployment) {
|
|
1084
|
+
const options = [];
|
|
1085
|
+
if (runtime === "workers") {
|
|
1086
|
+
if (existingDeployment !== "wrangler") {
|
|
1087
|
+
const { label, hint } = getDeploymentDisplay$1("wrangler");
|
|
1088
|
+
options.push({
|
|
1089
|
+
value: "wrangler",
|
|
1090
|
+
label,
|
|
1091
|
+
hint
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
if (existingDeployment !== "alchemy") {
|
|
1095
|
+
const { label, hint } = getDeploymentDisplay$1("alchemy");
|
|
1096
|
+
options.push({
|
|
1097
|
+
value: "alchemy",
|
|
1098
|
+
label,
|
|
1099
|
+
hint
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (existingDeployment && existingDeployment !== "none") return "none";
|
|
1104
|
+
if (options.length > 0) options.push({
|
|
1105
|
+
value: "none",
|
|
1106
|
+
label: "None",
|
|
1107
|
+
hint: "Skip deployment setup"
|
|
1108
|
+
});
|
|
1109
|
+
if (options.length === 0) return "none";
|
|
1110
|
+
const response = await select({
|
|
1111
|
+
message: "Select server deployment",
|
|
1112
|
+
options,
|
|
1113
|
+
initialValue: runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
|
|
1114
|
+
});
|
|
1115
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1116
|
+
return response;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1007
1119
|
//#endregion
|
|
1008
1120
|
//#region src/prompts/web-deploy.ts
|
|
1009
1121
|
function hasWebFrontend(frontends) {
|
|
1010
1122
|
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
|
|
1011
1123
|
}
|
|
1012
1124
|
function getDeploymentDisplay(deployment) {
|
|
1013
|
-
if (deployment === "
|
|
1014
|
-
label: "
|
|
1125
|
+
if (deployment === "wrangler") return {
|
|
1126
|
+
label: "Wrangler",
|
|
1015
1127
|
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
1016
1128
|
};
|
|
1129
|
+
if (deployment === "alchemy") return {
|
|
1130
|
+
label: "Alchemy",
|
|
1131
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
1132
|
+
};
|
|
1017
1133
|
return {
|
|
1018
1134
|
label: deployment,
|
|
1019
1135
|
hint: `Add ${deployment} deployment`
|
|
@@ -1022,15 +1138,18 @@ function getDeploymentDisplay(deployment) {
|
|
|
1022
1138
|
async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
|
|
1023
1139
|
if (deployment !== void 0) return deployment;
|
|
1024
1140
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1025
|
-
const options = [
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1141
|
+
const options = [
|
|
1142
|
+
"wrangler",
|
|
1143
|
+
"alchemy",
|
|
1144
|
+
"none"
|
|
1145
|
+
].map((deploy) => {
|
|
1146
|
+
const { label, hint } = getDeploymentDisplay(deploy);
|
|
1147
|
+
return {
|
|
1148
|
+
value: deploy,
|
|
1149
|
+
label,
|
|
1150
|
+
hint
|
|
1151
|
+
};
|
|
1152
|
+
});
|
|
1034
1153
|
const response = await select({
|
|
1035
1154
|
message: "Select web deployment",
|
|
1036
1155
|
options,
|
|
@@ -1042,10 +1161,18 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
|
|
|
1042
1161
|
async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
1043
1162
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1044
1163
|
const options = [];
|
|
1045
|
-
if (existingDeployment !== "
|
|
1046
|
-
const { label, hint } = getDeploymentDisplay("
|
|
1164
|
+
if (existingDeployment !== "wrangler") {
|
|
1165
|
+
const { label, hint } = getDeploymentDisplay("wrangler");
|
|
1166
|
+
options.push({
|
|
1167
|
+
value: "wrangler",
|
|
1168
|
+
label,
|
|
1169
|
+
hint
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
if (existingDeployment !== "alchemy") {
|
|
1173
|
+
const { label, hint } = getDeploymentDisplay("alchemy");
|
|
1047
1174
|
options.push({
|
|
1048
|
-
value: "
|
|
1175
|
+
value: "alchemy",
|
|
1049
1176
|
label,
|
|
1050
1177
|
hint
|
|
1051
1178
|
});
|
|
@@ -1081,6 +1208,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1081
1208
|
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
|
|
1082
1209
|
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
|
|
1083
1210
|
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend),
|
|
1211
|
+
serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
|
|
1084
1212
|
git: () => getGitChoice(flags.git),
|
|
1085
1213
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
|
1086
1214
|
install: () => getinstallChoice(flags.install)
|
|
@@ -1120,7 +1248,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1120
1248
|
install: result.install,
|
|
1121
1249
|
dbSetup: result.dbSetup,
|
|
1122
1250
|
api: result.api,
|
|
1123
|
-
webDeploy: result.webDeploy
|
|
1251
|
+
webDeploy: result.webDeploy,
|
|
1252
|
+
serverDeploy: result.serverDeploy
|
|
1124
1253
|
};
|
|
1125
1254
|
}
|
|
1126
1255
|
|
|
@@ -1152,7 +1281,7 @@ async function getProjectName(initialName) {
|
|
|
1152
1281
|
let projectPath = "";
|
|
1153
1282
|
let defaultName = DEFAULT_CONFIG.projectName;
|
|
1154
1283
|
let counter = 1;
|
|
1155
|
-
while (fs.
|
|
1284
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
1156
1285
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
|
1157
1286
|
counter++;
|
|
1158
1287
|
}
|
|
@@ -1199,7 +1328,7 @@ const getLatestCLIVersion = () => {
|
|
|
1199
1328
|
*/
|
|
1200
1329
|
function isTelemetryEnabled() {
|
|
1201
1330
|
const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
|
|
1202
|
-
const BTS_TELEMETRY = "
|
|
1331
|
+
const BTS_TELEMETRY = "0";
|
|
1203
1332
|
if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
|
|
1204
1333
|
if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
|
|
1205
1334
|
return true;
|
|
@@ -1207,11 +1336,16 @@ function isTelemetryEnabled() {
|
|
|
1207
1336
|
|
|
1208
1337
|
//#endregion
|
|
1209
1338
|
//#region src/utils/analytics.ts
|
|
1210
|
-
const POSTHOG_API_KEY = "
|
|
1211
|
-
const POSTHOG_HOST = "
|
|
1339
|
+
const POSTHOG_API_KEY = "random";
|
|
1340
|
+
const POSTHOG_HOST = "random";
|
|
1341
|
+
function generateSessionId() {
|
|
1342
|
+
const rand = Math.random().toString(36).slice(2);
|
|
1343
|
+
const now = Date.now().toString(36);
|
|
1344
|
+
return `cli_${now}${rand}`;
|
|
1345
|
+
}
|
|
1212
1346
|
async function trackProjectCreation(config, disableAnalytics = false) {
|
|
1213
1347
|
if (!isTelemetryEnabled() || disableAnalytics) return;
|
|
1214
|
-
const sessionId =
|
|
1348
|
+
const sessionId = generateSessionId();
|
|
1215
1349
|
const { projectName, projectDir, relativePath,...safeConfig } = config;
|
|
1216
1350
|
const payload = {
|
|
1217
1351
|
api_key: POSTHOG_API_KEY,
|
|
@@ -1219,8 +1353,8 @@ async function trackProjectCreation(config, disableAnalytics = false) {
|
|
|
1219
1353
|
properties: {
|
|
1220
1354
|
...safeConfig,
|
|
1221
1355
|
cli_version: getLatestCLIVersion(),
|
|
1222
|
-
node_version: process.version,
|
|
1223
|
-
platform: process.platform,
|
|
1356
|
+
node_version: typeof process !== "undefined" ? process.version : "",
|
|
1357
|
+
platform: typeof process !== "undefined" ? process.platform : "",
|
|
1224
1358
|
$ip: null
|
|
1225
1359
|
},
|
|
1226
1360
|
distinct_id: sessionId
|
|
@@ -1274,6 +1408,7 @@ function displayConfig(config) {
|
|
|
1274
1408
|
}
|
|
1275
1409
|
if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
|
|
1276
1410
|
if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
|
|
1411
|
+
if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
|
|
1277
1412
|
if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
|
|
1278
1413
|
return configDisplay.join("\n");
|
|
1279
1414
|
}
|
|
@@ -1296,6 +1431,7 @@ function generateReproducibleCommand(config) {
|
|
|
1296
1431
|
else flags.push("--examples none");
|
|
1297
1432
|
flags.push(`--db-setup ${config.dbSetup}`);
|
|
1298
1433
|
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
1434
|
+
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
1299
1435
|
flags.push(config.git ? "--git" : "--no-git");
|
|
1300
1436
|
flags.push(`--package-manager ${config.packageManager}`);
|
|
1301
1437
|
flags.push(config.install ? "--install" : "--no-install");
|
|
@@ -1313,8 +1449,8 @@ function generateReproducibleCommand(config) {
|
|
|
1313
1449
|
async function handleDirectoryConflict(currentPathInput, silent = false) {
|
|
1314
1450
|
while (true) {
|
|
1315
1451
|
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
|
1316
|
-
const dirExists = fs.
|
|
1317
|
-
const dirIsNotEmpty = dirExists && fs.
|
|
1452
|
+
const dirExists = await fs.pathExists(resolvedPath);
|
|
1453
|
+
const dirIsNotEmpty = dirExists && (await fs.readdir(resolvedPath)).length > 0;
|
|
1318
1454
|
if (!dirIsNotEmpty) return {
|
|
1319
1455
|
finalPathInput: currentPathInput,
|
|
1320
1456
|
shouldClearDirectory: false
|
|
@@ -1476,6 +1612,7 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1476
1612
|
if (options.dbSetup) config.dbSetup = options.dbSetup;
|
|
1477
1613
|
if (options.packageManager) config.packageManager = options.packageManager;
|
|
1478
1614
|
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1615
|
+
if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
|
|
1479
1616
|
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
|
1480
1617
|
if (derivedName) {
|
|
1481
1618
|
const nameToValidate = projectName ? path.basename(projectName) : derivedName;
|
|
@@ -1536,6 +1673,44 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1536
1673
|
validateWorkersCompatibility(providedFlags, options, config);
|
|
1537
1674
|
const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
|
|
1538
1675
|
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
1676
|
+
validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
|
1677
|
+
return config;
|
|
1678
|
+
}
|
|
1679
|
+
function validateConfigCompatibility(config) {
|
|
1680
|
+
const effectiveDatabase = config.database;
|
|
1681
|
+
const effectiveBackend = config.backend;
|
|
1682
|
+
const effectiveFrontend = config.frontend;
|
|
1683
|
+
const effectiveApi = config.api;
|
|
1684
|
+
validateApiFrontendCompatibility(effectiveApi, effectiveFrontend);
|
|
1685
|
+
if (config.addons && config.addons.length > 0) {
|
|
1686
|
+
validateAddonsAgainstFrontends(config.addons, effectiveFrontend);
|
|
1687
|
+
config.addons = [...new Set(config.addons)];
|
|
1688
|
+
}
|
|
1689
|
+
validateExamplesCompatibility(config.examples ?? [], effectiveBackend, effectiveDatabase, effectiveFrontend ?? []);
|
|
1690
|
+
}
|
|
1691
|
+
function processProvidedFlagsWithoutValidation(options, projectName) {
|
|
1692
|
+
const config = {};
|
|
1693
|
+
if (options.api) config.api = options.api;
|
|
1694
|
+
if (options.backend) config.backend = options.backend;
|
|
1695
|
+
if (options.database) config.database = options.database;
|
|
1696
|
+
if (options.orm) config.orm = options.orm;
|
|
1697
|
+
if (options.auth !== void 0) config.auth = options.auth;
|
|
1698
|
+
if (options.git !== void 0) config.git = options.git;
|
|
1699
|
+
if (options.install !== void 0) config.install = options.install;
|
|
1700
|
+
if (options.runtime) config.runtime = options.runtime;
|
|
1701
|
+
if (options.dbSetup) config.dbSetup = options.dbSetup;
|
|
1702
|
+
if (options.packageManager) config.packageManager = options.packageManager;
|
|
1703
|
+
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1704
|
+
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
|
1705
|
+
if (derivedName) {
|
|
1706
|
+
const nameToValidate = projectName ? path.basename(projectName) : derivedName;
|
|
1707
|
+
const result = ProjectNameSchema.safeParse(nameToValidate);
|
|
1708
|
+
if (!result.success) throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
|
|
1709
|
+
config.projectName = projectName || derivedName;
|
|
1710
|
+
}
|
|
1711
|
+
if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
|
|
1712
|
+
if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
|
|
1713
|
+
if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
|
|
1539
1714
|
return config;
|
|
1540
1715
|
}
|
|
1541
1716
|
function getProvidedFlags(options) {
|
|
@@ -1560,7 +1735,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1560
1735
|
packageManager: projectConfig.packageManager,
|
|
1561
1736
|
dbSetup: projectConfig.dbSetup,
|
|
1562
1737
|
api: projectConfig.api,
|
|
1563
|
-
webDeploy: projectConfig.webDeploy
|
|
1738
|
+
webDeploy: projectConfig.webDeploy,
|
|
1739
|
+
serverDeploy: projectConfig.serverDeploy
|
|
1564
1740
|
};
|
|
1565
1741
|
const baseContent = {
|
|
1566
1742
|
$schema: "https://r2.better-t-stack.dev/schema.json",
|
|
@@ -1577,7 +1753,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1577
1753
|
packageManager: btsConfig.packageManager,
|
|
1578
1754
|
dbSetup: btsConfig.dbSetup,
|
|
1579
1755
|
api: btsConfig.api,
|
|
1580
|
-
webDeploy: btsConfig.webDeploy
|
|
1756
|
+
webDeploy: btsConfig.webDeploy,
|
|
1757
|
+
serverDeploy: btsConfig.serverDeploy
|
|
1581
1758
|
};
|
|
1582
1759
|
let configContent = JSON.stringify(baseContent);
|
|
1583
1760
|
const formatResult = JSONC.format(configContent, void 0, {
|
|
@@ -1670,7 +1847,7 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
|
|
|
1670
1847
|
}
|
|
1671
1848
|
|
|
1672
1849
|
//#endregion
|
|
1673
|
-
//#region src/helpers/
|
|
1850
|
+
//#region src/helpers/addons/fumadocs-setup.ts
|
|
1674
1851
|
const TEMPLATES = {
|
|
1675
1852
|
"next-mdx": {
|
|
1676
1853
|
label: "Next.js: Fumadocs MDX",
|
|
@@ -1757,9 +1934,9 @@ handlebars.registerHelper("or", (a, b) => a || b);
|
|
|
1757
1934
|
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
1758
1935
|
|
|
1759
1936
|
//#endregion
|
|
1760
|
-
//#region src/helpers/
|
|
1937
|
+
//#region src/helpers/core/template-manager.ts
|
|
1761
1938
|
async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
|
|
1762
|
-
const sourceFiles = await
|
|
1939
|
+
const sourceFiles = await glob(sourcePattern, {
|
|
1763
1940
|
cwd: baseSourceDir,
|
|
1764
1941
|
dot: true,
|
|
1765
1942
|
onlyFiles: true,
|
|
@@ -2073,10 +2250,6 @@ async function handleExtras(projectDir, context) {
|
|
|
2073
2250
|
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
|
|
2074
2251
|
if (await fs.pathExists(npmrcTemplateSrc)) await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
|
|
2075
2252
|
}
|
|
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
2253
|
}
|
|
2081
2254
|
async function setupDockerComposeTemplates(projectDir, context) {
|
|
2082
2255
|
if (context.dbSetup !== "docker" || context.database === "none") return;
|
|
@@ -2085,29 +2258,58 @@ async function setupDockerComposeTemplates(projectDir, context) {
|
|
|
2085
2258
|
if (await fs.pathExists(dockerSrcDir)) await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
|
|
2086
2259
|
}
|
|
2087
2260
|
async function setupDeploymentTemplates(projectDir, context) {
|
|
2088
|
-
if (context.webDeploy === "
|
|
2089
|
-
|
|
2261
|
+
if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
|
|
2262
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2263
|
+
if (await fs.pathExists(alchemyTemplateSrc)) {
|
|
2264
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, projectDir, context);
|
|
2265
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2266
|
+
if (await fs.pathExists(serverAppDir)) await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2267
|
+
}
|
|
2268
|
+
} else {
|
|
2269
|
+
if (context.webDeploy === "alchemy") {
|
|
2270
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2271
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2272
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
|
|
2273
|
+
}
|
|
2274
|
+
if (context.serverDeploy === "alchemy") {
|
|
2275
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2276
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2277
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
|
|
2278
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2279
|
+
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
|
|
2090
2284
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2285
|
+
if (await fs.pathExists(webAppDir)) {
|
|
2286
|
+
const frontends = context.frontend;
|
|
2287
|
+
const templateMap = {
|
|
2288
|
+
"tanstack-router": "react/tanstack-router",
|
|
2289
|
+
"tanstack-start": "react/tanstack-start",
|
|
2290
|
+
"react-router": "react/react-router",
|
|
2291
|
+
solid: "solid",
|
|
2292
|
+
next: "react/next",
|
|
2293
|
+
nuxt: "nuxt",
|
|
2294
|
+
svelte: "svelte"
|
|
2295
|
+
};
|
|
2296
|
+
for (const f of frontends) if (templateMap[f]) {
|
|
2297
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
|
|
2298
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
|
|
2303
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2304
|
+
if (await fs.pathExists(serverAppDir)) {
|
|
2305
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
|
|
2306
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
|
|
2105
2307
|
}
|
|
2106
2308
|
}
|
|
2107
2309
|
}
|
|
2108
2310
|
|
|
2109
2311
|
//#endregion
|
|
2110
|
-
//#region src/helpers/
|
|
2312
|
+
//#region src/helpers/addons/ruler-setup.ts
|
|
2111
2313
|
async function setupVibeRules(config) {
|
|
2112
2314
|
const { packageManager, projectDir } = config;
|
|
2113
2315
|
try {
|
|
@@ -2189,7 +2391,7 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
|
|
|
2189
2391
|
}
|
|
2190
2392
|
|
|
2191
2393
|
//#endregion
|
|
2192
|
-
//#region src/helpers/
|
|
2394
|
+
//#region src/helpers/addons/starlight-setup.ts
|
|
2193
2395
|
async function setupStarlight(config) {
|
|
2194
2396
|
const { packageManager, projectDir } = config;
|
|
2195
2397
|
const s = spinner();
|
|
@@ -2221,7 +2423,7 @@ async function setupStarlight(config) {
|
|
|
2221
2423
|
}
|
|
2222
2424
|
|
|
2223
2425
|
//#endregion
|
|
2224
|
-
//#region src/helpers/
|
|
2426
|
+
//#region src/helpers/addons/tauri-setup.ts
|
|
2225
2427
|
async function setupTauri(config) {
|
|
2226
2428
|
const { packageManager, frontend, projectDir } = config;
|
|
2227
2429
|
const s = spinner();
|
|
@@ -2258,8 +2460,8 @@ async function setupTauri(config) {
|
|
|
2258
2460
|
`--window-title=${path.basename(projectDir)}`,
|
|
2259
2461
|
`--frontend-dist=${frontendDist}`,
|
|
2260
2462
|
`--dev-url=${devUrl}`,
|
|
2261
|
-
`--before-dev-command
|
|
2262
|
-
`--before-build-command
|
|
2463
|
+
`--before-dev-command="${packageManager} run dev"`,
|
|
2464
|
+
`--before-build-command="${packageManager} run build"`
|
|
2263
2465
|
];
|
|
2264
2466
|
const tauriArgsString = tauriArgs.join(" ");
|
|
2265
2467
|
const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
|
|
@@ -2277,7 +2479,7 @@ async function setupTauri(config) {
|
|
|
2277
2479
|
}
|
|
2278
2480
|
|
|
2279
2481
|
//#endregion
|
|
2280
|
-
//#region src/helpers/
|
|
2482
|
+
//#region src/helpers/addons/ultracite-setup.ts
|
|
2281
2483
|
const EDITORS = {
|
|
2282
2484
|
vscode: {
|
|
2283
2485
|
label: "VSCode / Cursor / Windsurf",
|
|
@@ -2346,7 +2548,7 @@ async function setupUltracite(config, hasHusky) {
|
|
|
2346
2548
|
];
|
|
2347
2549
|
if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
|
|
2348
2550
|
if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
|
|
2349
|
-
if (hasHusky) ultraciteArgs.push("--
|
|
2551
|
+
if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
|
|
2350
2552
|
const ultraciteArgsString = ultraciteArgs.join(" ");
|
|
2351
2553
|
const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
|
|
2352
2554
|
const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
@@ -2384,7 +2586,7 @@ function ensureArrayProperty(obj, name) {
|
|
|
2384
2586
|
}
|
|
2385
2587
|
|
|
2386
2588
|
//#endregion
|
|
2387
|
-
//#region src/helpers/
|
|
2589
|
+
//#region src/helpers/addons/vite-pwa-setup.ts
|
|
2388
2590
|
async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
2389
2591
|
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
|
2390
2592
|
if (!sourceFile) throw new Error("vite config not found");
|
|
@@ -2418,7 +2620,7 @@ async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
|
2418
2620
|
}
|
|
2419
2621
|
|
|
2420
2622
|
//#endregion
|
|
2421
|
-
//#region src/helpers/
|
|
2623
|
+
//#region src/helpers/addons/addons-setup.ts
|
|
2422
2624
|
async function setupAddons(config, isAddCommand = false) {
|
|
2423
2625
|
const { addons, frontend, projectDir, packageManager } = config;
|
|
2424
2626
|
const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
|
|
@@ -2552,7 +2754,7 @@ async function setupOxlint(projectDir, packageManager) {
|
|
|
2552
2754
|
}
|
|
2553
2755
|
|
|
2554
2756
|
//#endregion
|
|
2555
|
-
//#region src/helpers/
|
|
2757
|
+
//#region src/helpers/core/detect-project-config.ts
|
|
2556
2758
|
async function detectProjectConfig(projectDir) {
|
|
2557
2759
|
try {
|
|
2558
2760
|
const btsConfig = await readBtsConfig(projectDir);
|
|
@@ -2570,7 +2772,8 @@ async function detectProjectConfig(projectDir) {
|
|
|
2570
2772
|
packageManager: btsConfig.packageManager,
|
|
2571
2773
|
dbSetup: btsConfig.dbSetup,
|
|
2572
2774
|
api: btsConfig.api,
|
|
2573
|
-
webDeploy: btsConfig.webDeploy
|
|
2775
|
+
webDeploy: btsConfig.webDeploy,
|
|
2776
|
+
serverDeploy: btsConfig.serverDeploy
|
|
2574
2777
|
};
|
|
2575
2778
|
return null;
|
|
2576
2779
|
} catch (_error) {
|
|
@@ -2586,7 +2789,7 @@ async function isBetterTStackProject(projectDir) {
|
|
|
2586
2789
|
}
|
|
2587
2790
|
|
|
2588
2791
|
//#endregion
|
|
2589
|
-
//#region src/helpers/
|
|
2792
|
+
//#region src/helpers/core/install-dependencies.ts
|
|
2590
2793
|
async function installDependencies({ projectDir, packageManager }) {
|
|
2591
2794
|
const s = spinner();
|
|
2592
2795
|
try {
|
|
@@ -2603,7 +2806,7 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
2603
2806
|
}
|
|
2604
2807
|
|
|
2605
2808
|
//#endregion
|
|
2606
|
-
//#region src/helpers/
|
|
2809
|
+
//#region src/helpers/core/add-addons.ts
|
|
2607
2810
|
async function addAddonsToProject(input) {
|
|
2608
2811
|
try {
|
|
2609
2812
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -2628,7 +2831,8 @@ async function addAddonsToProject(input) {
|
|
|
2628
2831
|
install: input.install || false,
|
|
2629
2832
|
dbSetup: detectedConfig.dbSetup || "none",
|
|
2630
2833
|
api: detectedConfig.api || "none",
|
|
2631
|
-
webDeploy: detectedConfig.webDeploy || "none"
|
|
2834
|
+
webDeploy: detectedConfig.webDeploy || "none",
|
|
2835
|
+
serverDeploy: detectedConfig.serverDeploy || "none"
|
|
2632
2836
|
};
|
|
2633
2837
|
for (const addon of input.addons) {
|
|
2634
2838
|
const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
|
|
@@ -2651,12 +2855,92 @@ async function addAddonsToProject(input) {
|
|
|
2651
2855
|
}
|
|
2652
2856
|
|
|
2653
2857
|
//#endregion
|
|
2654
|
-
//#region src/helpers/
|
|
2655
|
-
async function
|
|
2858
|
+
//#region src/helpers/deployment/server-deploy-setup.ts
|
|
2859
|
+
async function setupServerDeploy(config) {
|
|
2860
|
+
const { serverDeploy, webDeploy, projectDir } = config;
|
|
2861
|
+
const { packageManager } = config;
|
|
2862
|
+
if (serverDeploy === "none") return;
|
|
2863
|
+
if (serverDeploy === "alchemy" && webDeploy === "alchemy") return;
|
|
2864
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
2865
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2866
|
+
if (serverDeploy === "wrangler") {
|
|
2867
|
+
await setupWorkersServerDeploy(serverDir, packageManager);
|
|
2868
|
+
await generateCloudflareWorkerTypes({
|
|
2869
|
+
serverDir,
|
|
2870
|
+
packageManager
|
|
2871
|
+
});
|
|
2872
|
+
} else if (serverDeploy === "alchemy") await setupAlchemyServerDeploy(serverDir, packageManager);
|
|
2873
|
+
}
|
|
2874
|
+
async function setupWorkersServerDeploy(serverDir, _packageManager) {
|
|
2875
|
+
const packageJsonPath = path.join(serverDir, "package.json");
|
|
2876
|
+
if (!await fs.pathExists(packageJsonPath)) return;
|
|
2877
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
2878
|
+
packageJson.scripts = {
|
|
2879
|
+
...packageJson.scripts,
|
|
2880
|
+
dev: "wrangler dev --port=3000",
|
|
2881
|
+
start: "wrangler dev",
|
|
2882
|
+
deploy: "wrangler deploy",
|
|
2883
|
+
build: "wrangler deploy --dry-run",
|
|
2884
|
+
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
|
|
2885
|
+
};
|
|
2886
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
2887
|
+
await addPackageDependency({
|
|
2888
|
+
devDependencies: [
|
|
2889
|
+
"wrangler",
|
|
2890
|
+
"@types/node",
|
|
2891
|
+
"@cloudflare/workers-types"
|
|
2892
|
+
],
|
|
2893
|
+
projectDir: serverDir
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
async function generateCloudflareWorkerTypes({ serverDir, packageManager }) {
|
|
2897
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2898
|
+
const s = spinner();
|
|
2899
|
+
try {
|
|
2900
|
+
s.start("Generating Cloudflare Workers types...");
|
|
2901
|
+
const runCmd = packageManager === "npm" ? "npm" : packageManager;
|
|
2902
|
+
await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
|
|
2903
|
+
s.stop("Cloudflare Workers types generated successfully!");
|
|
2904
|
+
} catch {
|
|
2905
|
+
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
|
2906
|
+
const managerCmd = `${packageManager} run`;
|
|
2907
|
+
log.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
async function setupAlchemyServerDeploy(serverDir, _packageManager) {
|
|
2911
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2912
|
+
await addPackageDependency({
|
|
2913
|
+
devDependencies: [
|
|
2914
|
+
"alchemy",
|
|
2915
|
+
"wrangler",
|
|
2916
|
+
"@types/node",
|
|
2917
|
+
"@cloudflare/workers-types",
|
|
2918
|
+
"dotenv"
|
|
2919
|
+
],
|
|
2920
|
+
projectDir: serverDir
|
|
2921
|
+
});
|
|
2922
|
+
const packageJsonPath = path.join(serverDir, "package.json");
|
|
2923
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
2924
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
2925
|
+
packageJson.scripts = {
|
|
2926
|
+
...packageJson.scripts,
|
|
2927
|
+
dev: "wrangler dev --port=3000",
|
|
2928
|
+
build: "wrangler deploy --dry-run",
|
|
2929
|
+
deploy: "alchemy deploy",
|
|
2930
|
+
destroy: "alchemy destroy",
|
|
2931
|
+
"alchemy:dev": "alchemy dev"
|
|
2932
|
+
};
|
|
2933
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
//#endregion
|
|
2938
|
+
//#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
|
|
2939
|
+
async function setupNextAlchemyDeploy(projectDir, _packageManager) {
|
|
2656
2940
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2657
2941
|
if (!await fs.pathExists(webAppDir)) return;
|
|
2658
2942
|
await addPackageDependency({
|
|
2659
|
-
devDependencies: ["
|
|
2943
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
2660
2944
|
projectDir: webAppDir
|
|
2661
2945
|
});
|
|
2662
2946
|
const pkgPath = path.join(webAppDir, "package.json");
|
|
@@ -2664,63 +2948,93 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
|
2664
2948
|
const pkg = await fs.readJson(pkgPath);
|
|
2665
2949
|
pkg.scripts = {
|
|
2666
2950
|
...pkg.scripts,
|
|
2667
|
-
deploy:
|
|
2668
|
-
|
|
2951
|
+
deploy: "alchemy deploy",
|
|
2952
|
+
destroy: "alchemy destroy",
|
|
2953
|
+
"alchemy:dev": "alchemy dev"
|
|
2954
|
+
};
|
|
2955
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
//#endregion
|
|
2960
|
+
//#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
|
|
2961
|
+
async function setupNuxtAlchemyDeploy(projectDir, _packageManager) {
|
|
2962
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2963
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
2964
|
+
await addPackageDependency({
|
|
2965
|
+
devDependencies: [
|
|
2966
|
+
"alchemy",
|
|
2967
|
+
"nitro-cloudflare-dev",
|
|
2968
|
+
"dotenv"
|
|
2969
|
+
],
|
|
2970
|
+
projectDir: webAppDir
|
|
2971
|
+
});
|
|
2972
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
2973
|
+
if (await fs.pathExists(pkgPath)) {
|
|
2974
|
+
const pkg = await fs.readJson(pkgPath);
|
|
2975
|
+
pkg.scripts = {
|
|
2976
|
+
...pkg.scripts,
|
|
2977
|
+
deploy: "alchemy deploy",
|
|
2978
|
+
destroy: "alchemy destroy",
|
|
2979
|
+
"alchemy:dev": "alchemy dev"
|
|
2669
2980
|
};
|
|
2670
2981
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2671
2982
|
}
|
|
2672
2983
|
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
2673
2984
|
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2985
|
+
try {
|
|
2986
|
+
const project = new Project({ manipulationSettings: {
|
|
2987
|
+
indentationText: IndentationText.TwoSpaces,
|
|
2988
|
+
quoteKind: QuoteKind.Double
|
|
2989
|
+
} });
|
|
2990
|
+
project.addSourceFileAtPath(nuxtConfigPath);
|
|
2991
|
+
const sourceFile = project.getSourceFileOrThrow(nuxtConfigPath);
|
|
2992
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
2993
|
+
if (!exportAssignment) return;
|
|
2994
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
2995
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
|
|
2996
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
2997
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
2998
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
2999
|
+
if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
|
|
3000
|
+
name: "nitro",
|
|
3001
|
+
initializer: `{
|
|
2691
3002
|
preset: "cloudflare_module",
|
|
2692
3003
|
cloudflare: {
|
|
2693
3004
|
deployConfig: true,
|
|
2694
3005
|
nodeCompat: true
|
|
2695
3006
|
}
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3007
|
+
}`
|
|
3008
|
+
});
|
|
3009
|
+
const modulesProperty = configObject.getProperty("modules");
|
|
3010
|
+
if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
|
|
3011
|
+
const initializer = modulesProperty.getInitializer();
|
|
3012
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3013
|
+
const hasModule = initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'");
|
|
3014
|
+
if (!hasModule) initializer.addElement("\"nitro-cloudflare-dev\"");
|
|
3015
|
+
}
|
|
3016
|
+
} else if (!modulesProperty) configObject.addPropertyAssignment({
|
|
3017
|
+
name: "modules",
|
|
3018
|
+
initializer: "[\"nitro-cloudflare-dev\"]"
|
|
3019
|
+
});
|
|
2709
3020
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
}
|
|
2714
|
-
await tsProject.save();
|
|
3021
|
+
await project.save();
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
console.warn("Failed to update nuxt.config.ts:", error);
|
|
3024
|
+
}
|
|
2715
3025
|
}
|
|
2716
3026
|
|
|
2717
3027
|
//#endregion
|
|
2718
|
-
//#region src/helpers/
|
|
2719
|
-
async function
|
|
3028
|
+
//#region src/helpers/deployment/alchemy/alchemy-react-router-setup.ts
|
|
3029
|
+
async function setupReactRouterAlchemyDeploy(projectDir, _packageManager) {
|
|
2720
3030
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2721
3031
|
if (!await fs.pathExists(webAppDir)) return;
|
|
2722
3032
|
await addPackageDependency({
|
|
2723
|
-
devDependencies: [
|
|
3033
|
+
devDependencies: [
|
|
3034
|
+
"alchemy",
|
|
3035
|
+
"@cloudflare/vite-plugin",
|
|
3036
|
+
"dotenv"
|
|
3037
|
+
],
|
|
2724
3038
|
projectDir: webAppDir
|
|
2725
3039
|
});
|
|
2726
3040
|
const pkgPath = path.join(webAppDir, "package.json");
|
|
@@ -2728,36 +3042,100 @@ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
|
2728
3042
|
const pkg = await fs.readJson(pkgPath);
|
|
2729
3043
|
pkg.scripts = {
|
|
2730
3044
|
...pkg.scripts,
|
|
2731
|
-
deploy:
|
|
2732
|
-
|
|
3045
|
+
deploy: "alchemy deploy",
|
|
3046
|
+
destroy: "alchemy destroy",
|
|
3047
|
+
"alchemy:dev": "alchemy dev"
|
|
2733
3048
|
};
|
|
2734
3049
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2735
3050
|
}
|
|
2736
|
-
const
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
3051
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3052
|
+
if (await fs.pathExists(viteConfigPath)) try {
|
|
3053
|
+
const project = new Project({ manipulationSettings: {
|
|
3054
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3055
|
+
quoteKind: QuoteKind.Double
|
|
3056
|
+
} });
|
|
3057
|
+
project.addSourceFileAtPath(viteConfigPath);
|
|
3058
|
+
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
|
3059
|
+
const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/react-router");
|
|
3060
|
+
if (!alchemyImport) sourceFile.addImportDeclaration({
|
|
3061
|
+
moduleSpecifier: "alchemy/cloudflare/react-router",
|
|
3062
|
+
defaultImport: "alchemy"
|
|
3063
|
+
});
|
|
3064
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3065
|
+
if (!exportAssignment) return;
|
|
3066
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3067
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
|
|
3068
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3069
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3070
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3071
|
+
const pluginsProperty = configObject.getProperty("plugins");
|
|
3072
|
+
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
|
3073
|
+
const initializer = pluginsProperty.getInitializer();
|
|
3074
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3075
|
+
const hasCloudflarePlugin = initializer.getElements().some((el) => el.getText().includes("cloudflare("));
|
|
3076
|
+
if (!hasCloudflarePlugin) initializer.addElement("alchemy()");
|
|
3077
|
+
}
|
|
3078
|
+
} else if (!pluginsProperty) configObject.addPropertyAssignment({
|
|
3079
|
+
name: "plugins",
|
|
3080
|
+
initializer: "[alchemy()]"
|
|
2748
3081
|
});
|
|
2749
3082
|
}
|
|
2750
|
-
await
|
|
3083
|
+
await project.save();
|
|
3084
|
+
} catch (error) {
|
|
3085
|
+
console.warn("Failed to update vite.config.ts:", error);
|
|
3086
|
+
}
|
|
3087
|
+
const reactRouterConfigPath = path.join(webAppDir, "react-router.config.ts");
|
|
3088
|
+
if (await fs.pathExists(reactRouterConfigPath)) try {
|
|
3089
|
+
const project = new Project({ manipulationSettings: {
|
|
3090
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3091
|
+
quoteKind: QuoteKind.Double
|
|
3092
|
+
} });
|
|
3093
|
+
project.addSourceFileAtPath(reactRouterConfigPath);
|
|
3094
|
+
const sourceFile = project.getSourceFileOrThrow(reactRouterConfigPath);
|
|
3095
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3096
|
+
if (!exportAssignment) return;
|
|
3097
|
+
const configExpression = exportAssignment.getExpression();
|
|
3098
|
+
let configObject;
|
|
3099
|
+
if (Node.isObjectLiteralExpression(configExpression)) configObject = configExpression;
|
|
3100
|
+
else if (Node.isSatisfiesExpression(configExpression)) {
|
|
3101
|
+
const expression = configExpression.getExpression();
|
|
3102
|
+
if (Node.isObjectLiteralExpression(expression)) configObject = expression;
|
|
3103
|
+
}
|
|
3104
|
+
if (!configObject || !Node.isObjectLiteralExpression(configObject)) return;
|
|
3105
|
+
const futureProperty = configObject.getProperty("future");
|
|
3106
|
+
if (!futureProperty) configObject.addPropertyAssignment({
|
|
3107
|
+
name: "future",
|
|
3108
|
+
initializer: `{
|
|
3109
|
+
unstable_viteEnvironmentApi: true,
|
|
3110
|
+
}`
|
|
3111
|
+
});
|
|
3112
|
+
else if (Node.isPropertyAssignment(futureProperty)) {
|
|
3113
|
+
const futureInitializer = futureProperty.getInitializer();
|
|
3114
|
+
if (Node.isObjectLiteralExpression(futureInitializer)) {
|
|
3115
|
+
const viteEnvApiProp = futureInitializer.getProperty("unstable_viteEnvironmentApi");
|
|
3116
|
+
if (!viteEnvApiProp) futureInitializer.addPropertyAssignment({
|
|
3117
|
+
name: "unstable_viteEnvironmentApi",
|
|
3118
|
+
initializer: "true"
|
|
3119
|
+
});
|
|
3120
|
+
else if (Node.isPropertyAssignment(viteEnvApiProp)) {
|
|
3121
|
+
const value = viteEnvApiProp.getInitializer()?.getText();
|
|
3122
|
+
if (value === "false") viteEnvApiProp.setInitializer("true");
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
await project.save();
|
|
3127
|
+
} catch (error) {
|
|
3128
|
+
console.warn("Failed to update react-router.config.ts:", error);
|
|
2751
3129
|
}
|
|
2752
3130
|
}
|
|
2753
3131
|
|
|
2754
3132
|
//#endregion
|
|
2755
|
-
//#region src/helpers/
|
|
2756
|
-
async function
|
|
3133
|
+
//#region src/helpers/deployment/alchemy/alchemy-solid-setup.ts
|
|
3134
|
+
async function setupSolidAlchemyDeploy(projectDir, _packageManager) {
|
|
2757
3135
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2758
3136
|
if (!await fs.pathExists(webAppDir)) return;
|
|
2759
3137
|
await addPackageDependency({
|
|
2760
|
-
devDependencies: ["
|
|
3138
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
2761
3139
|
projectDir: webAppDir
|
|
2762
3140
|
});
|
|
2763
3141
|
const pkgPath = path.join(webAppDir, "package.json");
|
|
@@ -2765,13 +3143,394 @@ async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
|
2765
3143
|
const pkg = await fs.readJson(pkgPath);
|
|
2766
3144
|
pkg.scripts = {
|
|
2767
3145
|
...pkg.scripts,
|
|
2768
|
-
deploy:
|
|
2769
|
-
|
|
3146
|
+
deploy: "alchemy deploy",
|
|
3147
|
+
destroy: "alchemy destroy",
|
|
3148
|
+
"alchemy:dev": "alchemy dev"
|
|
2770
3149
|
};
|
|
2771
3150
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2772
3151
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
//#endregion
|
|
3155
|
+
//#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
|
|
3156
|
+
async function setupSvelteAlchemyDeploy(projectDir, _packageManager) {
|
|
3157
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3158
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3159
|
+
await addPackageDependency({
|
|
3160
|
+
devDependencies: [
|
|
3161
|
+
"alchemy",
|
|
3162
|
+
"@sveltejs/adapter-cloudflare",
|
|
3163
|
+
"dotenv"
|
|
3164
|
+
],
|
|
3165
|
+
projectDir: webAppDir
|
|
3166
|
+
});
|
|
3167
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3168
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3169
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3170
|
+
pkg.scripts = {
|
|
3171
|
+
...pkg.scripts,
|
|
3172
|
+
deploy: "alchemy deploy",
|
|
3173
|
+
destroy: "alchemy destroy",
|
|
3174
|
+
"alchemy:dev": "alchemy dev"
|
|
3175
|
+
};
|
|
3176
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3177
|
+
}
|
|
3178
|
+
const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
|
|
3179
|
+
if (!await fs.pathExists(svelteConfigPath)) return;
|
|
3180
|
+
try {
|
|
3181
|
+
const project = new Project({ manipulationSettings: {
|
|
3182
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3183
|
+
quoteKind: QuoteKind.Single
|
|
3184
|
+
} });
|
|
3185
|
+
project.addSourceFileAtPath(svelteConfigPath);
|
|
3186
|
+
const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
|
|
3187
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
3188
|
+
const adapterImport = importDeclarations.find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
|
|
3189
|
+
if (adapterImport) {
|
|
3190
|
+
adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
|
|
3191
|
+
adapterImport.removeDefaultImport();
|
|
3192
|
+
adapterImport.setDefaultImport("alchemy");
|
|
3193
|
+
} else sourceFile.insertImportDeclaration(0, {
|
|
3194
|
+
moduleSpecifier: "alchemy/cloudflare/sveltekit",
|
|
3195
|
+
defaultImport: "alchemy"
|
|
3196
|
+
});
|
|
3197
|
+
const configVariable = sourceFile.getVariableDeclaration("config");
|
|
3198
|
+
if (configVariable) {
|
|
3199
|
+
const initializer = configVariable.getInitializer();
|
|
3200
|
+
if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
|
|
3201
|
+
}
|
|
3202
|
+
await project.save();
|
|
3203
|
+
} catch (error) {
|
|
3204
|
+
console.warn("Failed to update svelte.config.js:", error);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
function updateAdapterInConfig(configObject) {
|
|
3208
|
+
if (!Node.isObjectLiteralExpression(configObject)) return;
|
|
3209
|
+
const kitProperty = configObject.getProperty("kit");
|
|
3210
|
+
if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
|
|
3211
|
+
const kitInitializer = kitProperty.getInitializer();
|
|
3212
|
+
if (Node.isObjectLiteralExpression(kitInitializer)) {
|
|
3213
|
+
const adapterProperty = kitInitializer.getProperty("adapter");
|
|
3214
|
+
if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
|
|
3215
|
+
const initializer = adapterProperty.getInitializer();
|
|
3216
|
+
if (Node.isCallExpression(initializer)) {
|
|
3217
|
+
const expression = initializer.getExpression();
|
|
3218
|
+
if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
//#endregion
|
|
3226
|
+
//#region src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts
|
|
3227
|
+
async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager) {
|
|
3228
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3229
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3230
|
+
await addPackageDependency({
|
|
3231
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3232
|
+
projectDir: webAppDir
|
|
3233
|
+
});
|
|
3234
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3235
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3236
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3237
|
+
pkg.scripts = {
|
|
3238
|
+
...pkg.scripts,
|
|
3239
|
+
deploy: "alchemy deploy",
|
|
3240
|
+
destroy: "alchemy destroy",
|
|
3241
|
+
"alchemy:dev": "alchemy dev"
|
|
3242
|
+
};
|
|
3243
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
//#endregion
|
|
3248
|
+
//#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
|
|
3249
|
+
async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager) {
|
|
3250
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3251
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3252
|
+
await addPackageDependency({
|
|
3253
|
+
devDependencies: [
|
|
3254
|
+
"alchemy",
|
|
3255
|
+
"nitropack",
|
|
3256
|
+
"dotenv"
|
|
3257
|
+
],
|
|
3258
|
+
projectDir: webAppDir
|
|
3259
|
+
});
|
|
3260
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3261
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3262
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3263
|
+
pkg.scripts = {
|
|
3264
|
+
...pkg.scripts,
|
|
3265
|
+
deploy: "alchemy deploy",
|
|
3266
|
+
destroy: "alchemy destroy",
|
|
3267
|
+
"alchemy:dev": "alchemy dev"
|
|
3268
|
+
};
|
|
3269
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3270
|
+
}
|
|
3271
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3272
|
+
if (await fs.pathExists(viteConfigPath)) try {
|
|
3273
|
+
const project = new Project({ manipulationSettings: {
|
|
3274
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3275
|
+
quoteKind: QuoteKind.Double
|
|
3276
|
+
} });
|
|
3277
|
+
project.addSourceFileAtPath(viteConfigPath);
|
|
3278
|
+
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
|
3279
|
+
const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
|
|
3280
|
+
if (!alchemyImport) sourceFile.addImportDeclaration({
|
|
3281
|
+
moduleSpecifier: "alchemy/cloudflare/tanstack-start",
|
|
3282
|
+
defaultImport: "alchemy"
|
|
3283
|
+
});
|
|
3284
|
+
else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
|
|
3285
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3286
|
+
if (!exportAssignment) return;
|
|
3287
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3288
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
|
|
3289
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3290
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3291
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3292
|
+
if (!configObject.getProperty("build")) configObject.addPropertyAssignment({
|
|
3293
|
+
name: "build",
|
|
3294
|
+
initializer: `{
|
|
3295
|
+
target: "esnext",
|
|
3296
|
+
rollupOptions: {
|
|
3297
|
+
external: ["node:async_hooks", "cloudflare:workers"],
|
|
3298
|
+
},
|
|
3299
|
+
}`
|
|
3300
|
+
});
|
|
3301
|
+
const pluginsProperty = configObject.getProperty("plugins");
|
|
3302
|
+
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
|
3303
|
+
const initializer = pluginsProperty.getInitializer();
|
|
3304
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3305
|
+
const hasShim = initializer.getElements().some((el) => el.getText().includes("alchemy"));
|
|
3306
|
+
if (!hasShim) initializer.addElement("alchemy()");
|
|
3307
|
+
const tanstackElements = initializer.getElements().filter((el) => el.getText().includes("tanstackStart"));
|
|
3308
|
+
tanstackElements.forEach((element) => {
|
|
3309
|
+
if (Node.isCallExpression(element)) {
|
|
3310
|
+
const args = element.getArguments();
|
|
3311
|
+
if (args.length === 0) element.addArgument(`{
|
|
3312
|
+
target: "cloudflare-module",
|
|
3313
|
+
customViteReactPlugin: true,
|
|
3314
|
+
}`);
|
|
3315
|
+
else if (args.length === 1 && Node.isObjectLiteralExpression(args[0])) {
|
|
3316
|
+
const configObj = args[0];
|
|
3317
|
+
if (!configObj.getProperty("target")) configObj.addPropertyAssignment({
|
|
3318
|
+
name: "target",
|
|
3319
|
+
initializer: "\"cloudflare-module\""
|
|
3320
|
+
});
|
|
3321
|
+
if (!configObj.getProperty("customViteReactPlugin")) configObj.addPropertyAssignment({
|
|
3322
|
+
name: "customViteReactPlugin",
|
|
3323
|
+
initializer: "true"
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
} else configObject.addPropertyAssignment({
|
|
3330
|
+
name: "plugins",
|
|
3331
|
+
initializer: "[alchemy()]"
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
await project.save();
|
|
3335
|
+
} catch (error) {
|
|
3336
|
+
console.warn("Failed to update vite.config.ts:", error);
|
|
3337
|
+
}
|
|
3338
|
+
const nitroConfigPath = path.join(webAppDir, "nitro.config.ts");
|
|
3339
|
+
const nitroConfigContent = `import { defineNitroConfig } from "nitropack/config";
|
|
3340
|
+
|
|
3341
|
+
export default defineNitroConfig({
|
|
3342
|
+
preset: "cloudflare-module",
|
|
3343
|
+
cloudflare: {
|
|
3344
|
+
nodeCompat: true,
|
|
3345
|
+
},
|
|
3346
|
+
});
|
|
3347
|
+
`;
|
|
3348
|
+
await fs.writeFile(nitroConfigPath, nitroConfigContent, "utf-8");
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
//#endregion
|
|
3352
|
+
//#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
|
|
3353
|
+
async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
|
|
3354
|
+
await addPackageDependency({
|
|
3355
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3356
|
+
projectDir
|
|
3357
|
+
});
|
|
3358
|
+
const rootPkgPath = path.join(projectDir, "package.json");
|
|
3359
|
+
if (await fs.pathExists(rootPkgPath)) {
|
|
3360
|
+
const pkg = await fs.readJson(rootPkgPath);
|
|
3361
|
+
pkg.scripts = {
|
|
3362
|
+
...pkg.scripts,
|
|
3363
|
+
deploy: "alchemy deploy",
|
|
3364
|
+
destroy: "alchemy destroy",
|
|
3365
|
+
"alchemy:dev": "alchemy dev"
|
|
3366
|
+
};
|
|
3367
|
+
await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
|
|
3368
|
+
}
|
|
3369
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3370
|
+
if (await fs.pathExists(serverDir)) await setupAlchemyServerDeploy(serverDir, packageManager);
|
|
3371
|
+
const frontend = config.frontend;
|
|
3372
|
+
const isNext = frontend.includes("next");
|
|
3373
|
+
const isNuxt = frontend.includes("nuxt");
|
|
3374
|
+
const isSvelte = frontend.includes("svelte");
|
|
3375
|
+
const isTanstackRouter = frontend.includes("tanstack-router");
|
|
3376
|
+
const isTanstackStart = frontend.includes("tanstack-start");
|
|
3377
|
+
const isReactRouter = frontend.includes("react-router");
|
|
3378
|
+
const isSolid = frontend.includes("solid");
|
|
3379
|
+
if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
|
|
3380
|
+
else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
|
3381
|
+
else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
|
3382
|
+
else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
|
3383
|
+
else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
|
3384
|
+
else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
|
3385
|
+
else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
//#endregion
|
|
3389
|
+
//#region src/helpers/deployment/workers/workers-next-setup.ts
|
|
3390
|
+
async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
3391
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3392
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3393
|
+
await addPackageDependency({
|
|
3394
|
+
dependencies: ["@opennextjs/cloudflare"],
|
|
3395
|
+
devDependencies: ["wrangler"],
|
|
3396
|
+
projectDir: webAppDir
|
|
3397
|
+
});
|
|
3398
|
+
const packageJsonPath = path.join(webAppDir, "package.json");
|
|
3399
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
3400
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
3401
|
+
pkg.scripts = {
|
|
3402
|
+
...pkg.scripts,
|
|
3403
|
+
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
3404
|
+
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
3405
|
+
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
|
3406
|
+
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
3407
|
+
};
|
|
3408
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
//#endregion
|
|
3413
|
+
//#region src/helpers/deployment/workers/workers-nuxt-setup.ts
|
|
3414
|
+
async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
3415
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3416
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3417
|
+
await addPackageDependency({
|
|
3418
|
+
devDependencies: ["nitro-cloudflare-dev", "wrangler"],
|
|
3419
|
+
projectDir: webAppDir
|
|
3420
|
+
});
|
|
3421
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3422
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3423
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3424
|
+
pkg.scripts = {
|
|
3425
|
+
...pkg.scripts,
|
|
3426
|
+
deploy: `${packageManager} run build && wrangler deploy`,
|
|
3427
|
+
"cf-typegen": "wrangler types"
|
|
3428
|
+
};
|
|
3429
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3430
|
+
}
|
|
3431
|
+
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
3432
|
+
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
3433
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
|
|
3434
|
+
if (!sourceFile) return;
|
|
3435
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
3436
|
+
const expression = expr.getExpression();
|
|
3437
|
+
return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
|
|
3438
|
+
});
|
|
3439
|
+
if (!defineCall) return;
|
|
3440
|
+
const configObj = defineCall.getArguments()[0];
|
|
3441
|
+
if (!configObj) return;
|
|
3442
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3443
|
+
const compatProp = configObj.getProperty("compatibilityDate");
|
|
3444
|
+
if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
|
|
3445
|
+
else configObj.addPropertyAssignment({
|
|
3446
|
+
name: "compatibilityDate",
|
|
3447
|
+
initializer: `'${today}'`
|
|
3448
|
+
});
|
|
3449
|
+
const nitroInitializer = `{
|
|
3450
|
+
preset: "cloudflare_module",
|
|
3451
|
+
cloudflare: {
|
|
3452
|
+
deployConfig: true,
|
|
3453
|
+
nodeCompat: true
|
|
3454
|
+
}
|
|
3455
|
+
}`;
|
|
3456
|
+
const nitroProp = configObj.getProperty("nitro");
|
|
3457
|
+
if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
|
|
3458
|
+
else configObj.addPropertyAssignment({
|
|
3459
|
+
name: "nitro",
|
|
3460
|
+
initializer: nitroInitializer
|
|
3461
|
+
});
|
|
3462
|
+
const modulesProp = configObj.getProperty("modules");
|
|
3463
|
+
if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
3464
|
+
const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
|
|
3465
|
+
if (arrayExpr) {
|
|
3466
|
+
const alreadyHas = arrayExpr.getElements().some((el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev");
|
|
3467
|
+
if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'");
|
|
3468
|
+
}
|
|
3469
|
+
} else configObj.addPropertyAssignment({
|
|
3470
|
+
name: "modules",
|
|
3471
|
+
initializer: "['nitro-cloudflare-dev']"
|
|
3472
|
+
});
|
|
3473
|
+
await tsProject.save();
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
//#endregion
|
|
3477
|
+
//#region src/helpers/deployment/workers/workers-svelte-setup.ts
|
|
3478
|
+
async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
3479
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3480
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3481
|
+
await addPackageDependency({
|
|
3482
|
+
devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"],
|
|
3483
|
+
projectDir: webAppDir
|
|
3484
|
+
});
|
|
3485
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3486
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3487
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3488
|
+
pkg.scripts = {
|
|
3489
|
+
...pkg.scripts,
|
|
3490
|
+
deploy: `${packageManager} run build && wrangler deploy`,
|
|
3491
|
+
"cf-typegen": "wrangler types ./src/worker-configuration.d.ts"
|
|
3492
|
+
};
|
|
3493
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3494
|
+
}
|
|
3495
|
+
const possibleConfigFiles = [path.join(webAppDir, "svelte.config.js"), path.join(webAppDir, "svelte.config.ts")];
|
|
3496
|
+
const existingConfigPath = (await Promise.all(possibleConfigFiles.map(async (p) => await fs.pathExists(p) ? p : ""))).find((p) => p);
|
|
3497
|
+
if (existingConfigPath) {
|
|
3498
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(existingConfigPath);
|
|
3499
|
+
if (!sourceFile) return;
|
|
3500
|
+
const adapterImport = sourceFile.getImportDeclarations().find((imp) => ["@sveltejs/adapter-auto", "@sveltejs/adapter-node"].includes(imp.getModuleSpecifierValue()));
|
|
3501
|
+
if (adapterImport) adapterImport.setModuleSpecifier("@sveltejs/adapter-cloudflare");
|
|
3502
|
+
else {
|
|
3503
|
+
const alreadyHasCloudflare = sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "@sveltejs/adapter-cloudflare");
|
|
3504
|
+
if (!alreadyHasCloudflare) sourceFile.insertImportDeclaration(0, {
|
|
3505
|
+
defaultImport: "adapter",
|
|
3506
|
+
moduleSpecifier: "@sveltejs/adapter-cloudflare"
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
await tsProject.save();
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
//#endregion
|
|
3514
|
+
//#region src/helpers/deployment/workers/workers-tanstack-start-setup.ts
|
|
3515
|
+
async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
3516
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3517
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3518
|
+
await addPackageDependency({
|
|
3519
|
+
devDependencies: ["wrangler"],
|
|
3520
|
+
projectDir: webAppDir
|
|
3521
|
+
});
|
|
3522
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3523
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3524
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3525
|
+
pkg.scripts = {
|
|
3526
|
+
...pkg.scripts,
|
|
3527
|
+
deploy: `${packageManager} run build && wrangler deploy`,
|
|
3528
|
+
"cf-typegen": "wrangler types --env-interface Env"
|
|
3529
|
+
};
|
|
3530
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3531
|
+
}
|
|
3532
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3533
|
+
if (!await fs.pathExists(viteConfigPath)) return;
|
|
2775
3534
|
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
|
2776
3535
|
if (!sourceFile) return;
|
|
2777
3536
|
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
@@ -2790,7 +3549,7 @@ async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
|
2790
3549
|
}
|
|
2791
3550
|
|
|
2792
3551
|
//#endregion
|
|
2793
|
-
//#region src/helpers/
|
|
3552
|
+
//#region src/helpers/deployment/workers/workers-vite-setup.ts
|
|
2794
3553
|
async function setupWorkersVitePlugin(projectDir) {
|
|
2795
3554
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2796
3555
|
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
@@ -2821,12 +3580,16 @@ async function setupWorkersVitePlugin(projectDir) {
|
|
|
2821
3580
|
}
|
|
2822
3581
|
|
|
2823
3582
|
//#endregion
|
|
2824
|
-
//#region src/helpers/
|
|
3583
|
+
//#region src/helpers/deployment/web-deploy-setup.ts
|
|
2825
3584
|
async function setupWebDeploy(config) {
|
|
2826
|
-
const { webDeploy, frontend, projectDir } = config;
|
|
3585
|
+
const { webDeploy, serverDeploy, frontend, projectDir } = config;
|
|
2827
3586
|
const { packageManager } = config;
|
|
2828
3587
|
if (webDeploy === "none") return;
|
|
2829
|
-
if (webDeploy !== "
|
|
3588
|
+
if (webDeploy !== "wrangler" && webDeploy !== "alchemy") return;
|
|
3589
|
+
if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
|
|
3590
|
+
await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
2830
3593
|
const isNext = frontend.includes("next");
|
|
2831
3594
|
const isNuxt = frontend.includes("nuxt");
|
|
2832
3595
|
const isSvelte = frontend.includes("svelte");
|
|
@@ -2834,11 +3597,21 @@ async function setupWebDeploy(config) {
|
|
|
2834
3597
|
const isTanstackStart = frontend.includes("tanstack-start");
|
|
2835
3598
|
const isReactRouter = frontend.includes("react-router");
|
|
2836
3599
|
const isSolid = frontend.includes("solid");
|
|
2837
|
-
if (
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
3600
|
+
if (webDeploy === "wrangler") {
|
|
3601
|
+
if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
|
|
3602
|
+
else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
|
|
3603
|
+
else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
|
|
3604
|
+
else if (isTanstackStart) await setupTanstackStartWorkersDeploy(projectDir, packageManager);
|
|
3605
|
+
else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
|
|
3606
|
+
} else if (webDeploy === "alchemy") {
|
|
3607
|
+
if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
|
|
3608
|
+
else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
|
3609
|
+
else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
|
3610
|
+
else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
|
3611
|
+
else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
|
3612
|
+
else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
|
3613
|
+
else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
|
|
3614
|
+
}
|
|
2842
3615
|
}
|
|
2843
3616
|
async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
2844
3617
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
@@ -2855,30 +3628,9 @@ async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
|
2855
3628
|
}
|
|
2856
3629
|
await setupWorkersVitePlugin(projectDir);
|
|
2857
3630
|
}
|
|
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
3631
|
|
|
2880
3632
|
//#endregion
|
|
2881
|
-
//#region src/helpers/
|
|
3633
|
+
//#region src/helpers/core/add-deployment.ts
|
|
2882
3634
|
async function addDeploymentToProject(input) {
|
|
2883
3635
|
try {
|
|
2884
3636
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -2886,7 +3638,8 @@ async function addDeploymentToProject(input) {
|
|
|
2886
3638
|
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
3639
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
2888
3640
|
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.`);
|
|
3641
|
+
if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} web deployment is already configured for this project.`);
|
|
3642
|
+
if (input.serverDeploy && detectedConfig.serverDeploy === input.serverDeploy) exitWithError(`${input.serverDeploy} server deployment is already configured for this project.`);
|
|
2890
3643
|
const config = {
|
|
2891
3644
|
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
2892
3645
|
projectDir,
|
|
@@ -2901,226 +3654,81 @@ async function addDeploymentToProject(input) {
|
|
|
2901
3654
|
auth: detectedConfig.auth || false,
|
|
2902
3655
|
git: false,
|
|
2903
3656
|
packageManager: input.packageManager || detectedConfig.packageManager || "npm",
|
|
2904
|
-
install: input.install || false,
|
|
2905
|
-
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) {}
|
|
3657
|
+
install: input.install || false,
|
|
3658
|
+
dbSetup: detectedConfig.dbSetup || "none",
|
|
3659
|
+
api: detectedConfig.api || "none",
|
|
3660
|
+
webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
|
|
3661
|
+
serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none"
|
|
3110
3662
|
};
|
|
3111
|
-
if (
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3663
|
+
if (input.webDeploy && input.webDeploy !== "none") log.info(pc.green(`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`));
|
|
3664
|
+
if (input.serverDeploy && input.serverDeploy !== "none") log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
|
|
3665
|
+
await setupDeploymentTemplates(projectDir, config);
|
|
3666
|
+
await setupWebDeploy(config);
|
|
3667
|
+
await setupServerDeploy(config);
|
|
3668
|
+
await updateBtsConfig(projectDir, {
|
|
3669
|
+
webDeploy: input.webDeploy || config.webDeploy,
|
|
3670
|
+
serverDeploy: input.serverDeploy || config.serverDeploy
|
|
3671
|
+
});
|
|
3672
|
+
if (config.install) await installDependencies({
|
|
3673
|
+
projectDir,
|
|
3674
|
+
packageManager: config.packageManager
|
|
3675
|
+
});
|
|
3676
|
+
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
3677
|
+
} catch (error) {
|
|
3678
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3679
|
+
exitWithError(`Error adding deployment: ${message}`);
|
|
3119
3680
|
}
|
|
3120
3681
|
}
|
|
3121
3682
|
|
|
3122
3683
|
//#endregion
|
|
3123
|
-
//#region src/
|
|
3684
|
+
//#region src/utils/format-with-biome.ts
|
|
3685
|
+
async function formatProjectWithBiome(projectDir) {
|
|
3686
|
+
const biome = new Biome();
|
|
3687
|
+
const { projectKey } = biome.openProject(projectDir);
|
|
3688
|
+
biome.applyConfiguration(projectKey, {
|
|
3689
|
+
formatter: {
|
|
3690
|
+
enabled: true,
|
|
3691
|
+
indentStyle: "tab"
|
|
3692
|
+
},
|
|
3693
|
+
javascript: { formatter: { quoteStyle: "double" } }
|
|
3694
|
+
});
|
|
3695
|
+
const files = await glob("**/*", {
|
|
3696
|
+
cwd: projectDir,
|
|
3697
|
+
dot: true,
|
|
3698
|
+
absolute: true,
|
|
3699
|
+
onlyFiles: true
|
|
3700
|
+
});
|
|
3701
|
+
for (const filePath of files) try {
|
|
3702
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
3703
|
+
const supported = new Set([
|
|
3704
|
+
".ts",
|
|
3705
|
+
".tsx",
|
|
3706
|
+
".js",
|
|
3707
|
+
".jsx",
|
|
3708
|
+
".cjs",
|
|
3709
|
+
".mjs",
|
|
3710
|
+
".cts",
|
|
3711
|
+
".mts",
|
|
3712
|
+
".json",
|
|
3713
|
+
".jsonc",
|
|
3714
|
+
".md",
|
|
3715
|
+
".mdx",
|
|
3716
|
+
".css",
|
|
3717
|
+
".scss",
|
|
3718
|
+
".html"
|
|
3719
|
+
]);
|
|
3720
|
+
if (!supported.has(ext)) continue;
|
|
3721
|
+
const original = await fs.readFile(filePath, "utf8");
|
|
3722
|
+
const result = biome.formatContent(projectKey, original, { filePath });
|
|
3723
|
+
const content = result?.content;
|
|
3724
|
+
if (typeof content !== "string") continue;
|
|
3725
|
+
if (content.length === 0 && original.length > 0) continue;
|
|
3726
|
+
if (content !== original) await fs.writeFile(filePath, content);
|
|
3727
|
+
} catch {}
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
//#endregion
|
|
3731
|
+
//#region src/helpers/addons/auth-setup.ts
|
|
3124
3732
|
async function setupAuth(config) {
|
|
3125
3733
|
const { auth, frontend, backend, projectDir } = config;
|
|
3126
3734
|
if (backend === "convex" || !auth) return;
|
|
@@ -3172,7 +3780,217 @@ function generateAuthSecret(length = 32) {
|
|
|
3172
3780
|
}
|
|
3173
3781
|
|
|
3174
3782
|
//#endregion
|
|
3175
|
-
//#region src/helpers/
|
|
3783
|
+
//#region src/helpers/addons/examples-setup.ts
|
|
3784
|
+
async function setupExamples(config) {
|
|
3785
|
+
const { examples, frontend, backend, projectDir } = config;
|
|
3786
|
+
if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
|
|
3787
|
+
if (examples.includes("ai")) {
|
|
3788
|
+
const webClientDir = path.join(projectDir, "apps/web");
|
|
3789
|
+
const nativeClientDir = path.join(projectDir, "apps/native");
|
|
3790
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3791
|
+
const webClientDirExists = await fs.pathExists(webClientDir);
|
|
3792
|
+
const nativeClientDirExists = await fs.pathExists(nativeClientDir);
|
|
3793
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
3794
|
+
const hasNuxt = frontend.includes("nuxt");
|
|
3795
|
+
const hasSvelte = frontend.includes("svelte");
|
|
3796
|
+
const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
|
|
3797
|
+
const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3798
|
+
if (webClientDirExists) {
|
|
3799
|
+
const dependencies = ["ai"];
|
|
3800
|
+
if (hasNuxt) dependencies.push("@ai-sdk/vue");
|
|
3801
|
+
else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
|
|
3802
|
+
else if (hasReactWeb) dependencies.push("@ai-sdk/react");
|
|
3803
|
+
await addPackageDependency({
|
|
3804
|
+
dependencies,
|
|
3805
|
+
projectDir: webClientDir
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
if (nativeClientDirExists && hasReactNative) await addPackageDependency({
|
|
3809
|
+
dependencies: ["ai", "@ai-sdk/react"],
|
|
3810
|
+
projectDir: nativeClientDir
|
|
3811
|
+
});
|
|
3812
|
+
if (serverDirExists && backend !== "none") await addPackageDependency({
|
|
3813
|
+
dependencies: ["ai", "@ai-sdk/google"],
|
|
3814
|
+
projectDir: serverDir
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
//#endregion
|
|
3820
|
+
//#region src/helpers/core/api-setup.ts
|
|
3821
|
+
async function addBackendWorkspaceDependency(projectDir, backendPackageName, workspaceVersion) {
|
|
3822
|
+
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
3823
|
+
try {
|
|
3824
|
+
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
3825
|
+
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
3826
|
+
pkgJson.dependencies[backendPackageName] = workspaceVersion;
|
|
3827
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
3828
|
+
} catch (_error) {}
|
|
3829
|
+
}
|
|
3830
|
+
function getFrontendType(frontend) {
|
|
3831
|
+
const reactBasedFrontends = [
|
|
3832
|
+
"tanstack-router",
|
|
3833
|
+
"react-router",
|
|
3834
|
+
"tanstack-start",
|
|
3835
|
+
"next"
|
|
3836
|
+
];
|
|
3837
|
+
const nativeFrontends = ["native-nativewind", "native-unistyles"];
|
|
3838
|
+
return {
|
|
3839
|
+
hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
|
|
3840
|
+
hasNuxtWeb: frontend.includes("nuxt"),
|
|
3841
|
+
hasSvelteWeb: frontend.includes("svelte"),
|
|
3842
|
+
hasSolidWeb: frontend.includes("solid"),
|
|
3843
|
+
hasNative: frontend.some((f) => nativeFrontends.includes(f))
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
function getApiDependencies(api, frontendType) {
|
|
3847
|
+
const deps = {};
|
|
3848
|
+
if (api === "orpc") deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
|
|
3849
|
+
else if (api === "trpc") deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
|
|
3850
|
+
if (frontendType.hasReactWeb) {
|
|
3851
|
+
if (api === "orpc") deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
|
3852
|
+
else if (api === "trpc") deps.web = { dependencies: [
|
|
3853
|
+
"@trpc/tanstack-react-query",
|
|
3854
|
+
"@trpc/client",
|
|
3855
|
+
"@trpc/server"
|
|
3856
|
+
] };
|
|
3857
|
+
} else if (frontendType.hasNuxtWeb && api === "orpc") deps.web = {
|
|
3858
|
+
dependencies: [
|
|
3859
|
+
"@tanstack/vue-query",
|
|
3860
|
+
"@orpc/tanstack-query",
|
|
3861
|
+
"@orpc/client"
|
|
3862
|
+
],
|
|
3863
|
+
devDependencies: ["@tanstack/vue-query-devtools"]
|
|
3864
|
+
};
|
|
3865
|
+
else if (frontendType.hasSvelteWeb && api === "orpc") deps.web = {
|
|
3866
|
+
dependencies: [
|
|
3867
|
+
"@orpc/tanstack-query",
|
|
3868
|
+
"@orpc/client",
|
|
3869
|
+
"@tanstack/svelte-query"
|
|
3870
|
+
],
|
|
3871
|
+
devDependencies: ["@tanstack/svelte-query-devtools"]
|
|
3872
|
+
};
|
|
3873
|
+
else if (frontendType.hasSolidWeb && api === "orpc") deps.web = {
|
|
3874
|
+
dependencies: [
|
|
3875
|
+
"@orpc/tanstack-query",
|
|
3876
|
+
"@orpc/client",
|
|
3877
|
+
"@tanstack/solid-query"
|
|
3878
|
+
],
|
|
3879
|
+
devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
|
|
3880
|
+
};
|
|
3881
|
+
if (api === "trpc") deps.native = { dependencies: [
|
|
3882
|
+
"@trpc/tanstack-react-query",
|
|
3883
|
+
"@trpc/client",
|
|
3884
|
+
"@trpc/server"
|
|
3885
|
+
] };
|
|
3886
|
+
else if (api === "orpc") deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
|
3887
|
+
return deps;
|
|
3888
|
+
}
|
|
3889
|
+
function getQueryDependencies(frontend) {
|
|
3890
|
+
const reactBasedFrontends = [
|
|
3891
|
+
"react-router",
|
|
3892
|
+
"tanstack-router",
|
|
3893
|
+
"tanstack-start",
|
|
3894
|
+
"next",
|
|
3895
|
+
"native-nativewind",
|
|
3896
|
+
"native-unistyles"
|
|
3897
|
+
];
|
|
3898
|
+
const deps = {};
|
|
3899
|
+
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
|
3900
|
+
if (needsReactQuery) {
|
|
3901
|
+
const hasReactWeb = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
|
|
3902
|
+
const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3903
|
+
if (hasReactWeb) deps.web = {
|
|
3904
|
+
dependencies: ["@tanstack/react-query"],
|
|
3905
|
+
devDependencies: ["@tanstack/react-query-devtools"]
|
|
3906
|
+
};
|
|
3907
|
+
if (hasNative) deps.native = { dependencies: ["@tanstack/react-query"] };
|
|
3908
|
+
}
|
|
3909
|
+
if (frontend.includes("solid")) deps.web = {
|
|
3910
|
+
dependencies: ["@tanstack/solid-query"],
|
|
3911
|
+
devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
|
|
3912
|
+
};
|
|
3913
|
+
return deps;
|
|
3914
|
+
}
|
|
3915
|
+
function getConvexDependencies(frontend) {
|
|
3916
|
+
const deps = {
|
|
3917
|
+
web: { dependencies: ["convex"] },
|
|
3918
|
+
native: { dependencies: ["convex"] }
|
|
3919
|
+
};
|
|
3920
|
+
if (frontend.includes("tanstack-start")) deps.web.dependencies.push("@convex-dev/react-query");
|
|
3921
|
+
if (frontend.includes("svelte")) deps.web.dependencies.push("convex-svelte");
|
|
3922
|
+
if (frontend.includes("nuxt")) deps.web.dependencies.push("convex-nuxt", "convex-vue");
|
|
3923
|
+
return deps;
|
|
3924
|
+
}
|
|
3925
|
+
async function setupApi(config) {
|
|
3926
|
+
const { api, projectName, frontend, backend, packageManager, projectDir } = config;
|
|
3927
|
+
const isConvex = backend === "convex";
|
|
3928
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
3929
|
+
const nativeDir = path.join(projectDir, "apps/native");
|
|
3930
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3931
|
+
const webDirExists = await fs.pathExists(webDir);
|
|
3932
|
+
const nativeDirExists = await fs.pathExists(nativeDir);
|
|
3933
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
3934
|
+
const frontendType = getFrontendType(frontend);
|
|
3935
|
+
if (!isConvex && api !== "none") {
|
|
3936
|
+
const apiDeps = getApiDependencies(api, frontendType);
|
|
3937
|
+
if (serverDirExists && apiDeps.server) {
|
|
3938
|
+
await addPackageDependency({
|
|
3939
|
+
dependencies: apiDeps.server.dependencies,
|
|
3940
|
+
projectDir: serverDir
|
|
3941
|
+
});
|
|
3942
|
+
if (api === "trpc") {
|
|
3943
|
+
if (backend === "hono") await addPackageDependency({
|
|
3944
|
+
dependencies: ["@hono/trpc-server"],
|
|
3945
|
+
projectDir: serverDir
|
|
3946
|
+
});
|
|
3947
|
+
else if (backend === "elysia") await addPackageDependency({
|
|
3948
|
+
dependencies: ["@elysiajs/trpc"],
|
|
3949
|
+
projectDir: serverDir
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
if (webDirExists && apiDeps.web) await addPackageDependency({
|
|
3954
|
+
dependencies: apiDeps.web.dependencies,
|
|
3955
|
+
devDependencies: apiDeps.web.devDependencies,
|
|
3956
|
+
projectDir: webDir
|
|
3957
|
+
});
|
|
3958
|
+
if (nativeDirExists && apiDeps.native) await addPackageDependency({
|
|
3959
|
+
dependencies: apiDeps.native.dependencies,
|
|
3960
|
+
projectDir: nativeDir
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
if (!isConvex) {
|
|
3964
|
+
const queryDeps = getQueryDependencies(frontend);
|
|
3965
|
+
if (webDirExists && queryDeps.web) await addPackageDependency({
|
|
3966
|
+
dependencies: queryDeps.web.dependencies,
|
|
3967
|
+
devDependencies: queryDeps.web.devDependencies,
|
|
3968
|
+
projectDir: webDir
|
|
3969
|
+
});
|
|
3970
|
+
if (nativeDirExists && queryDeps.native) await addPackageDependency({
|
|
3971
|
+
dependencies: queryDeps.native.dependencies,
|
|
3972
|
+
projectDir: nativeDir
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3975
|
+
if (isConvex) {
|
|
3976
|
+
const convexDeps = getConvexDependencies(frontend);
|
|
3977
|
+
if (webDirExists) await addPackageDependency({
|
|
3978
|
+
dependencies: convexDeps.web.dependencies,
|
|
3979
|
+
projectDir: webDir
|
|
3980
|
+
});
|
|
3981
|
+
if (nativeDirExists) await addPackageDependency({
|
|
3982
|
+
dependencies: convexDeps.native.dependencies,
|
|
3983
|
+
projectDir: nativeDir
|
|
3984
|
+
});
|
|
3985
|
+
const backendPackageName = `@${projectName}/backend`;
|
|
3986
|
+
const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
|
|
3987
|
+
if (webDirExists) await addBackendWorkspaceDependency(webDir, backendPackageName, backendWorkspaceVersion);
|
|
3988
|
+
if (nativeDirExists) await addBackendWorkspaceDependency(nativeDir, backendPackageName, backendWorkspaceVersion);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
//#endregion
|
|
3993
|
+
//#region src/helpers/core/backend-setup.ts
|
|
3176
3994
|
async function setupBackendDependencies(config) {
|
|
3177
3995
|
const { backend, runtime, api, projectDir } = config;
|
|
3178
3996
|
if (backend === "convex") return;
|
|
@@ -3211,7 +4029,7 @@ async function setupBackendDependencies(config) {
|
|
|
3211
4029
|
}
|
|
3212
4030
|
|
|
3213
4031
|
//#endregion
|
|
3214
|
-
//#region src/helpers/
|
|
4032
|
+
//#region src/helpers/core/env-setup.ts
|
|
3215
4033
|
async function addEnvVariablesToFile(filePath, variables) {
|
|
3216
4034
|
await fs.ensureDir(path.dirname(filePath));
|
|
3217
4035
|
let envContent = "";
|
|
@@ -3259,7 +4077,7 @@ async function addEnvVariablesToFile(filePath, variables) {
|
|
|
3259
4077
|
if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
|
|
3260
4078
|
}
|
|
3261
4079
|
async function setupEnvironmentVariables(config) {
|
|
3262
|
-
const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
|
|
4080
|
+
const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
|
|
3263
4081
|
const hasReactRouter = frontend.includes("react-router");
|
|
3264
4082
|
const hasTanStackRouter = frontend.includes("tanstack-router");
|
|
3265
4083
|
const hasTanStackStart = frontend.includes("tanstack-start");
|
|
@@ -3359,39 +4177,69 @@ async function setupEnvironmentVariables(config) {
|
|
|
3359
4177
|
}
|
|
3360
4178
|
];
|
|
3361
4179
|
await addEnvVariablesToFile(envPath, serverVars);
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
4180
|
+
const isUnifiedAlchemy = webDeploy === "alchemy" && serverDeploy === "alchemy";
|
|
4181
|
+
const isIndividualAlchemy = webDeploy === "alchemy" || serverDeploy === "alchemy";
|
|
4182
|
+
if (isUnifiedAlchemy) {
|
|
4183
|
+
const rootEnvPath = path.join(projectDir, ".env");
|
|
4184
|
+
const rootAlchemyVars = [{
|
|
4185
|
+
key: "ALCHEMY_PASSWORD",
|
|
4186
|
+
value: "please-change-this",
|
|
4187
|
+
condition: true
|
|
4188
|
+
}];
|
|
4189
|
+
await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
|
|
4190
|
+
} else if (isIndividualAlchemy) {
|
|
4191
|
+
if (webDeploy === "alchemy") {
|
|
4192
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
4193
|
+
if (await fs.pathExists(webDir)) {
|
|
4194
|
+
const webAlchemyVars = [{
|
|
4195
|
+
key: "ALCHEMY_PASSWORD",
|
|
4196
|
+
value: "please-change-this",
|
|
4197
|
+
condition: true
|
|
4198
|
+
}];
|
|
4199
|
+
await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
if (serverDeploy === "alchemy") {
|
|
4203
|
+
const serverDir$1 = path.join(projectDir, "apps/server");
|
|
4204
|
+
if (await fs.pathExists(serverDir$1)) {
|
|
4205
|
+
const serverAlchemyVars = [{
|
|
4206
|
+
key: "ALCHEMY_PASSWORD",
|
|
4207
|
+
value: "please-change-this",
|
|
4208
|
+
condition: true
|
|
4209
|
+
}];
|
|
4210
|
+
await addEnvVariablesToFile(path.join(serverDir$1, ".env"), serverAlchemyVars);
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
3367
4213
|
}
|
|
3368
4214
|
}
|
|
3369
4215
|
|
|
3370
4216
|
//#endregion
|
|
3371
4217
|
//#region src/helpers/database-providers/d1-setup.ts
|
|
3372
4218
|
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
|
-
|
|
4219
|
+
const { projectDir, serverDeploy } = config;
|
|
4220
|
+
if (serverDeploy === "wrangler") {
|
|
4221
|
+
const envPath = path.join(projectDir, "apps/server", ".env");
|
|
4222
|
+
const variables = [
|
|
4223
|
+
{
|
|
4224
|
+
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
4225
|
+
value: "",
|
|
4226
|
+
condition: true
|
|
4227
|
+
},
|
|
4228
|
+
{
|
|
4229
|
+
key: "CLOUDFLARE_DATABASE_ID",
|
|
4230
|
+
value: "",
|
|
4231
|
+
condition: true
|
|
4232
|
+
},
|
|
4233
|
+
{
|
|
4234
|
+
key: "CLOUDFLARE_D1_TOKEN",
|
|
4235
|
+
value: "",
|
|
4236
|
+
condition: true
|
|
4237
|
+
}
|
|
4238
|
+
];
|
|
4239
|
+
try {
|
|
4240
|
+
await addEnvVariablesToFile(envPath, variables);
|
|
4241
|
+
} catch (_err) {}
|
|
4242
|
+
}
|
|
3395
4243
|
}
|
|
3396
4244
|
|
|
3397
4245
|
//#endregion
|
|
@@ -4185,7 +5033,7 @@ async function setupTurso(config) {
|
|
|
4185
5033
|
}
|
|
4186
5034
|
|
|
4187
5035
|
//#endregion
|
|
4188
|
-
//#region src/helpers/
|
|
5036
|
+
//#region src/helpers/core/db-setup.ts
|
|
4189
5037
|
async function setupDatabase(config) {
|
|
4190
5038
|
const { database, orm, dbSetup, backend, projectDir } = config;
|
|
4191
5039
|
if (backend === "convex" || database === "none") {
|
|
@@ -4250,44 +5098,7 @@ async function setupDatabase(config) {
|
|
|
4250
5098
|
}
|
|
4251
5099
|
|
|
4252
5100
|
//#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
|
|
5101
|
+
//#region src/helpers/core/runtime-setup.ts
|
|
4291
5102
|
async function setupRuntime(config) {
|
|
4292
5103
|
const { runtime, backend, projectDir } = config;
|
|
4293
5104
|
if (backend === "convex" || backend === "next" || runtime === "none") return;
|
|
@@ -4295,23 +5106,6 @@ async function setupRuntime(config) {
|
|
|
4295
5106
|
if (!await fs.pathExists(serverDir)) return;
|
|
4296
5107
|
if (runtime === "bun") await setupBunRuntime(serverDir, backend);
|
|
4297
5108
|
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
5109
|
}
|
|
4316
5110
|
async function setupBunRuntime(serverDir, _backend) {
|
|
4317
5111
|
const packageJsonPath = path.join(serverDir, "package.json");
|
|
@@ -4351,27 +5145,20 @@ async function setupNodeRuntime(serverDir, backend) {
|
|
|
4351
5145
|
projectDir: serverDir
|
|
4352
5146
|
});
|
|
4353
5147
|
}
|
|
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
|
|
5148
|
+
|
|
5149
|
+
//#endregion
|
|
5150
|
+
//#region src/helpers/core/convex-codegen.ts
|
|
5151
|
+
async function runConvexCodegen(projectDir, packageManager) {
|
|
5152
|
+
const backendDir = path.join(projectDir, "packages/backend");
|
|
5153
|
+
const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
|
|
5154
|
+
await execa(cmd, {
|
|
5155
|
+
cwd: backendDir,
|
|
5156
|
+
shell: true
|
|
4370
5157
|
});
|
|
4371
5158
|
}
|
|
4372
5159
|
|
|
4373
5160
|
//#endregion
|
|
4374
|
-
//#region src/helpers/
|
|
5161
|
+
//#region src/helpers/core/create-readme.ts
|
|
4375
5162
|
async function createReadme(projectDir, options) {
|
|
4376
5163
|
const readmePath = path.join(projectDir, "README.md");
|
|
4377
5164
|
const content = generateReadmeContent(options);
|
|
@@ -4648,7 +5435,7 @@ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNati
|
|
|
4648
5435
|
}
|
|
4649
5436
|
|
|
4650
5437
|
//#endregion
|
|
4651
|
-
//#region src/helpers/
|
|
5438
|
+
//#region src/helpers/core/git.ts
|
|
4652
5439
|
async function initializeGit(projectDir, useGit) {
|
|
4653
5440
|
if (!useGit) return;
|
|
4654
5441
|
const gitVersionResult = await $({
|
|
@@ -4724,20 +5511,21 @@ async function getDockerStatus(database) {
|
|
|
4724
5511
|
}
|
|
4725
5512
|
|
|
4726
5513
|
//#endregion
|
|
4727
|
-
//#region src/helpers/
|
|
5514
|
+
//#region src/helpers/core/post-installation.ts
|
|
4728
5515
|
async function displayPostInstallInstructions(config) {
|
|
4729
|
-
const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy } = config;
|
|
5516
|
+
const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy } = config;
|
|
4730
5517
|
const isConvex = backend === "convex";
|
|
4731
5518
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
4732
5519
|
const cdCmd = `cd ${relativePath}`;
|
|
4733
5520
|
const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
|
|
4734
|
-
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
|
|
5521
|
+
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
|
|
4735
5522
|
const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
|
|
4736
5523
|
const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
|
|
4737
5524
|
const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
|
|
4738
5525
|
const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
|
|
4739
5526
|
const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
|
|
4740
|
-
const
|
|
5527
|
+
const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
5528
|
+
const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
4741
5529
|
const hasWeb = frontend?.some((f) => [
|
|
4742
5530
|
"tanstack-router",
|
|
4743
5531
|
"react-router",
|
|
@@ -4766,7 +5554,7 @@ async function displayPostInstallInstructions(config) {
|
|
|
4766
5554
|
if (runtime === "workers") {
|
|
4767
5555
|
if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
|
|
4768
5556
|
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
4769
|
-
output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd}
|
|
5557
|
+
if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n\n`;
|
|
4770
5558
|
} else output += "\n";
|
|
4771
5559
|
}
|
|
4772
5560
|
output += `${pc.bold("Your project will be available at:")}\n`;
|
|
@@ -4780,7 +5568,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
4780
5568
|
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
|
4781
5569
|
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
|
|
4782
5570
|
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
|
|
4783
|
-
if (
|
|
5571
|
+
if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
|
|
5572
|
+
if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
|
|
4784
5573
|
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
|
4785
5574
|
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
|
4786
5575
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
|
@@ -4801,7 +5590,7 @@ function getNativeInstructions(isConvex) {
|
|
|
4801
5590
|
function getLintingInstructions(runCmd) {
|
|
4802
5591
|
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
|
|
4803
5592
|
}
|
|
4804
|
-
async function getDatabaseInstructions(database, orm, runCmd,
|
|
5593
|
+
async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
|
|
4805
5594
|
const instructions = [];
|
|
4806
5595
|
if (dbSetup === "docker") {
|
|
4807
5596
|
const dockerStatus = await getDockerStatus(database);
|
|
@@ -4810,7 +5599,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4810
5599
|
instructions.push("");
|
|
4811
5600
|
}
|
|
4812
5601
|
}
|
|
4813
|
-
if (
|
|
5602
|
+
if (serverDeploy === "wrangler" && dbSetup === "d1") {
|
|
4814
5603
|
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
|
|
4815
5604
|
instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
|
|
4816
5605
|
instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
|
|
@@ -4820,6 +5609,10 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4820
5609
|
instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
|
|
4821
5610
|
instructions.push("");
|
|
4822
5611
|
}
|
|
5612
|
+
if (dbSetup === "d1" && serverDeploy === "alchemy") {
|
|
5613
|
+
instructions.push(`${pc.cyan("•")} Generate migrations: ${pc.white(`${runCmd} db:generate`)}`);
|
|
5614
|
+
instructions.push("");
|
|
5615
|
+
}
|
|
4823
5616
|
if (orm === "prisma") {
|
|
4824
5617
|
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
5618
|
if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
|
|
@@ -4851,12 +5644,22 @@ function getNoOrmWarning() {
|
|
|
4851
5644
|
function getBunWebNativeWarning() {
|
|
4852
5645
|
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
|
|
4853
5646
|
}
|
|
4854
|
-
function
|
|
4855
|
-
|
|
5647
|
+
function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5648
|
+
const instructions = [];
|
|
5649
|
+
if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
|
|
5650
|
+
if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
|
|
5651
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
5652
|
+
}
|
|
5653
|
+
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5654
|
+
const instructions = [];
|
|
5655
|
+
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
|
|
5656
|
+
else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
|
|
5657
|
+
else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
|
|
5658
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
4856
5659
|
}
|
|
4857
5660
|
|
|
4858
5661
|
//#endregion
|
|
4859
|
-
//#region src/helpers/
|
|
5662
|
+
//#region src/helpers/core/project-config.ts
|
|
4860
5663
|
async function updatePackageConfigurations(projectDir, options) {
|
|
4861
5664
|
await updateRootPackageJson(projectDir, options);
|
|
4862
5665
|
if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
|
|
@@ -5037,7 +5840,7 @@ async function updateConvexPackageJson(projectDir, options) {
|
|
|
5037
5840
|
}
|
|
5038
5841
|
|
|
5039
5842
|
//#endregion
|
|
5040
|
-
//#region src/helpers/
|
|
5843
|
+
//#region src/helpers/core/create-project.ts
|
|
5041
5844
|
async function createProject(options) {
|
|
5042
5845
|
const projectDir = options.projectDir;
|
|
5043
5846
|
const isConvex = options.backend === "convex";
|
|
@@ -5069,14 +5872,13 @@ async function createProject(options) {
|
|
|
5069
5872
|
await updatePackageConfigurations(projectDir, options);
|
|
5070
5873
|
await createReadme(projectDir, options);
|
|
5071
5874
|
await writeBtsConfig(options);
|
|
5875
|
+
await formatProjectWithBiome(projectDir);
|
|
5876
|
+
if (isConvex) await runConvexCodegen(projectDir, options.packageManager);
|
|
5072
5877
|
log.success("Project template successfully scaffolded!");
|
|
5073
|
-
if (options.install) {
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
});
|
|
5078
|
-
await generateCloudflareWorkerTypes(options);
|
|
5079
|
-
}
|
|
5878
|
+
if (options.install) await installDependencies({
|
|
5879
|
+
projectDir,
|
|
5880
|
+
packageManager: options.packageManager
|
|
5881
|
+
});
|
|
5080
5882
|
await initializeGit(projectDir, options.git);
|
|
5081
5883
|
await displayPostInstallInstructions({
|
|
5082
5884
|
...options,
|
|
@@ -5095,7 +5897,7 @@ async function createProject(options) {
|
|
|
5095
5897
|
}
|
|
5096
5898
|
|
|
5097
5899
|
//#endregion
|
|
5098
|
-
//#region src/helpers/
|
|
5900
|
+
//#region src/helpers/core/command-handlers.ts
|
|
5099
5901
|
async function createProjectHandler(input) {
|
|
5100
5902
|
const startTime = Date.now();
|
|
5101
5903
|
const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5107,7 +5909,7 @@ async function createProjectHandler(input) {
|
|
|
5107
5909
|
else if (input.yes) {
|
|
5108
5910
|
let defaultName = DEFAULT_CONFIG.relativePath;
|
|
5109
5911
|
let counter = 1;
|
|
5110
|
-
while (fs.
|
|
5912
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
5111
5913
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
|
5112
5914
|
counter++;
|
|
5113
5915
|
}
|
|
@@ -5146,7 +5948,8 @@ async function createProjectHandler(input) {
|
|
|
5146
5948
|
install: false,
|
|
5147
5949
|
dbSetup: "none",
|
|
5148
5950
|
api: "none",
|
|
5149
|
-
webDeploy: "none"
|
|
5951
|
+
webDeploy: "none",
|
|
5952
|
+
serverDeploy: "none"
|
|
5150
5953
|
},
|
|
5151
5954
|
reproducibleCommand: "",
|
|
5152
5955
|
timeScaffolded,
|
|
@@ -5162,15 +5965,9 @@ async function createProjectHandler(input) {
|
|
|
5162
5965
|
projectDirectory: input.projectName
|
|
5163
5966
|
};
|
|
5164
5967
|
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
5968
|
let config;
|
|
5173
5969
|
if (input.yes) {
|
|
5970
|
+
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
5174
5971
|
config = {
|
|
5175
5972
|
...DEFAULT_CONFIG,
|
|
5176
5973
|
...flagConfig,
|
|
@@ -5178,12 +5975,22 @@ async function createProjectHandler(input) {
|
|
|
5178
5975
|
projectDir: finalResolvedPath,
|
|
5179
5976
|
relativePath: finalPathInput
|
|
5180
5977
|
};
|
|
5978
|
+
validateConfigCompatibility(config);
|
|
5181
5979
|
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
5980
|
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
5981
|
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
5184
5982
|
log.message(displayConfig(config));
|
|
5185
5983
|
log.message("");
|
|
5186
|
-
} else
|
|
5984
|
+
} else {
|
|
5985
|
+
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
5986
|
+
const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
|
|
5987
|
+
if (Object.keys(otherFlags).length > 0) {
|
|
5988
|
+
log.info(pc.yellow("Using these pre-selected options:"));
|
|
5989
|
+
log.message(displayConfig(otherFlags));
|
|
5990
|
+
log.message("");
|
|
5991
|
+
}
|
|
5992
|
+
config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
|
|
5993
|
+
}
|
|
5187
5994
|
await createProject(config);
|
|
5188
5995
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
5189
5996
|
log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
@@ -5203,11 +6010,11 @@ async function createProjectHandler(input) {
|
|
|
5203
6010
|
}
|
|
5204
6011
|
async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
|
|
5205
6012
|
const currentPath = path.resolve(process.cwd(), currentPathInput);
|
|
5206
|
-
if (!fs.
|
|
6013
|
+
if (!await fs.pathExists(currentPath)) return {
|
|
5207
6014
|
finalPathInput: currentPathInput,
|
|
5208
6015
|
shouldClearDirectory: false
|
|
5209
6016
|
};
|
|
5210
|
-
const dirContents = fs.
|
|
6017
|
+
const dirContents = await fs.readdir(currentPath);
|
|
5211
6018
|
const isNotEmpty = dirContents.length > 0;
|
|
5212
6019
|
if (!isNotEmpty) return {
|
|
5213
6020
|
finalPathInput: currentPathInput,
|
|
@@ -5226,7 +6033,7 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
|
|
|
5226
6033
|
let counter = 1;
|
|
5227
6034
|
const baseName = currentPathInput;
|
|
5228
6035
|
let finalPathInput = `${baseName}-${counter}`;
|
|
5229
|
-
while (fs.
|
|
6036
|
+
while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
|
|
5230
6037
|
counter++;
|
|
5231
6038
|
finalPathInput = `${baseName}-${counter}`;
|
|
5232
6039
|
}
|
|
@@ -5252,6 +6059,10 @@ async function addAddonsHandler(input) {
|
|
|
5252
6059
|
const deploymentPrompt = await getDeploymentToAdd(detectedConfig.frontend || [], detectedConfig.webDeploy);
|
|
5253
6060
|
if (deploymentPrompt !== "none") input.webDeploy = deploymentPrompt;
|
|
5254
6061
|
}
|
|
6062
|
+
if (!input.serverDeploy) {
|
|
6063
|
+
const serverDeploymentPrompt = await getServerDeploymentToAdd(detectedConfig.runtime, detectedConfig.serverDeploy);
|
|
6064
|
+
if (serverDeploymentPrompt !== "none") input.serverDeploy = serverDeploymentPrompt;
|
|
6065
|
+
}
|
|
5255
6066
|
const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
|
|
5256
6067
|
let somethingAdded = false;
|
|
5257
6068
|
if (input.addons && input.addons.length > 0) {
|
|
@@ -5272,6 +6083,15 @@ async function addAddonsHandler(input) {
|
|
|
5272
6083
|
});
|
|
5273
6084
|
somethingAdded = true;
|
|
5274
6085
|
}
|
|
6086
|
+
if (input.serverDeploy && input.serverDeploy !== "none") {
|
|
6087
|
+
await addDeploymentToProject({
|
|
6088
|
+
...input,
|
|
6089
|
+
install: false,
|
|
6090
|
+
suppressInstallMessage: true,
|
|
6091
|
+
serverDeploy: input.serverDeploy
|
|
6092
|
+
});
|
|
6093
|
+
somethingAdded = true;
|
|
6094
|
+
}
|
|
5275
6095
|
if (!somethingAdded) {
|
|
5276
6096
|
outro(pc.yellow("No addons or deployment configurations to add."));
|
|
5277
6097
|
return;
|
|
@@ -5375,6 +6195,7 @@ const router = t.router({
|
|
|
5375
6195
|
runtime: RuntimeSchema.optional(),
|
|
5376
6196
|
api: APISchema.optional(),
|
|
5377
6197
|
webDeploy: WebDeploySchema.optional(),
|
|
6198
|
+
serverDeploy: ServerDeploySchema.optional(),
|
|
5378
6199
|
directoryConflict: DirectoryConflictSchema.optional(),
|
|
5379
6200
|
renderTitle: z.boolean().optional(),
|
|
5380
6201
|
disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics")
|
|
@@ -5390,6 +6211,7 @@ const router = t.router({
|
|
|
5390
6211
|
add: t.procedure.meta({ description: "Add addons or deployment configurations to an existing Better-T Stack project" }).input(z.tuple([z.object({
|
|
5391
6212
|
addons: z.array(AddonsSchema).optional().default([]),
|
|
5392
6213
|
webDeploy: WebDeploySchema.optional(),
|
|
6214
|
+
serverDeploy: ServerDeploySchema.optional(),
|
|
5393
6215
|
projectDir: z.string().optional(),
|
|
5394
6216
|
install: z.boolean().optional().default(false).describe("Install dependencies after adding addons or deployment"),
|
|
5395
6217
|
packageManager: PackageManagerSchema.optional()
|