create-better-t-stack 2.33.8 → 2.33.9-canary.38a6c011
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-pVOzV3_5.js} +1479 -582
- 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/alchemy/wrangler.jsonc.hbs +11 -0
- package/templates/deploy/{web → wrangler/web}/nuxt/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/react/next/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/react/react-router/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/react/tanstack-router/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/react/tanstack-start/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/solid/wrangler.jsonc.hbs +1 -1
- package/templates/deploy/{web → wrangler/web}/svelte/wrangler.jsonc.hbs +1 -1
- package/templates/frontend/nuxt/_gitignore +3 -0
- package/templates/frontend/nuxt/tsconfig.json.hbs +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
|
|
@@ -28,9 +29,8 @@ const getUserPkgManager = () => {
|
|
|
28
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
30
|
const distPath = path.dirname(__filename);
|
|
30
31
|
const PKG_ROOT = path.join(distPath, "../");
|
|
31
|
-
const
|
|
32
|
+
const DEFAULT_CONFIG_BASE = {
|
|
32
33
|
projectName: "my-better-t-app",
|
|
33
|
-
projectDir: path.resolve(process.cwd(), "my-better-t-app"),
|
|
34
34
|
relativePath: "my-better-t-app",
|
|
35
35
|
frontend: ["tanstack-router"],
|
|
36
36
|
database: "sqlite",
|
|
@@ -39,14 +39,25 @@ const DEFAULT_CONFIG = {
|
|
|
39
39
|
addons: ["turborepo"],
|
|
40
40
|
examples: [],
|
|
41
41
|
git: true,
|
|
42
|
-
packageManager: getUserPkgManager(),
|
|
43
42
|
install: true,
|
|
44
43
|
dbSetup: "none",
|
|
45
44
|
backend: "hono",
|
|
46
45
|
runtime: "bun",
|
|
47
46
|
api: "trpc",
|
|
48
|
-
webDeploy: "none"
|
|
47
|
+
webDeploy: "none",
|
|
48
|
+
serverDeploy: "none"
|
|
49
49
|
};
|
|
50
|
+
function getDefaultConfig() {
|
|
51
|
+
return {
|
|
52
|
+
...DEFAULT_CONFIG_BASE,
|
|
53
|
+
projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
|
|
54
|
+
packageManager: getUserPkgManager(),
|
|
55
|
+
frontend: [...DEFAULT_CONFIG_BASE.frontend],
|
|
56
|
+
addons: [...DEFAULT_CONFIG_BASE.addons],
|
|
57
|
+
examples: [...DEFAULT_CONFIG_BASE.examples]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const DEFAULT_CONFIG = getDefaultConfig();
|
|
50
61
|
const dependencyVersionMap = {
|
|
51
62
|
"better-auth": "^1.3.4",
|
|
52
63
|
"@better-auth/expo": "^1.3.4",
|
|
@@ -103,18 +114,24 @@ const dependencyVersionMap = {
|
|
|
103
114
|
"convex-svelte": "^0.0.11",
|
|
104
115
|
"convex-nuxt": "0.1.5",
|
|
105
116
|
"convex-vue": "^0.1.5",
|
|
106
|
-
"@tanstack/svelte-query": "^5.
|
|
117
|
+
"@tanstack/svelte-query": "^5.85.3",
|
|
118
|
+
"@tanstack/svelte-query-devtools": "^5.85.3",
|
|
107
119
|
"@tanstack/vue-query-devtools": "^5.83.0",
|
|
108
120
|
"@tanstack/vue-query": "^5.83.0",
|
|
109
121
|
"@tanstack/react-query-devtools": "^5.80.5",
|
|
110
122
|
"@tanstack/react-query": "^5.80.5",
|
|
111
123
|
"@tanstack/solid-query": "^5.75.0",
|
|
112
124
|
"@tanstack/solid-query-devtools": "^5.75.0",
|
|
125
|
+
"@tanstack/solid-router-devtools": "^1.131.25",
|
|
113
126
|
wrangler: "^4.23.0",
|
|
114
127
|
"@cloudflare/vite-plugin": "^1.9.0",
|
|
115
128
|
"@opennextjs/cloudflare": "^1.3.0",
|
|
116
129
|
"nitro-cloudflare-dev": "^0.2.2",
|
|
117
|
-
"@sveltejs/adapter-cloudflare": "^7.
|
|
130
|
+
"@sveltejs/adapter-cloudflare": "^7.2.1",
|
|
131
|
+
"@cloudflare/workers-types": "^4.20250813.0",
|
|
132
|
+
alchemy: "^0.62.1",
|
|
133
|
+
nitropack: "^2.12.4",
|
|
134
|
+
dotenv: "^17.2.1"
|
|
118
135
|
};
|
|
119
136
|
const ADDON_COMPATIBILITY = {
|
|
120
137
|
pwa: [
|
|
@@ -128,7 +145,8 @@ const ADDON_COMPATIBILITY = {
|
|
|
128
145
|
"react-router",
|
|
129
146
|
"nuxt",
|
|
130
147
|
"svelte",
|
|
131
|
-
"solid"
|
|
148
|
+
"solid",
|
|
149
|
+
"next"
|
|
132
150
|
],
|
|
133
151
|
biome: [],
|
|
134
152
|
husky: [],
|
|
@@ -233,7 +251,16 @@ const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(
|
|
|
233
251
|
];
|
|
234
252
|
return !invalidChars.some((char) => name.includes(char));
|
|
235
253
|
}, "Project name contains invalid characters").refine((name) => name.toLowerCase() !== "node_modules", "Project name is reserved").describe("Project name or path");
|
|
236
|
-
const WebDeploySchema = z.enum([
|
|
254
|
+
const WebDeploySchema = z.enum([
|
|
255
|
+
"wrangler",
|
|
256
|
+
"alchemy",
|
|
257
|
+
"none"
|
|
258
|
+
]).describe("Web deployment");
|
|
259
|
+
const ServerDeploySchema = z.enum([
|
|
260
|
+
"wrangler",
|
|
261
|
+
"alchemy",
|
|
262
|
+
"none"
|
|
263
|
+
]).describe("Server deployment");
|
|
237
264
|
const DirectoryConflictSchema = z.enum([
|
|
238
265
|
"merge",
|
|
239
266
|
"overwrite",
|
|
@@ -544,6 +571,23 @@ function isExampleAIAllowed(backend, frontends = []) {
|
|
|
544
571
|
function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
|
|
545
572
|
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
546
573
|
}
|
|
574
|
+
function validateServerDeployRequiresBackend(serverDeploy, backend) {
|
|
575
|
+
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
|
|
576
|
+
}
|
|
577
|
+
function validateAddonsAgainstFrontends(addons = [], frontends = []) {
|
|
578
|
+
for (const addon of addons) {
|
|
579
|
+
if (addon === "none") continue;
|
|
580
|
+
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends);
|
|
581
|
+
if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function validateExamplesCompatibility(examples, backend, database, frontend) {
|
|
585
|
+
const examplesArr = examples ?? [];
|
|
586
|
+
if (examplesArr.length === 0 || examplesArr.includes("none")) return;
|
|
587
|
+
if (examplesArr.includes("todo") && backend !== "convex" && backend !== "none" && database === "none") exitWithError("The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.");
|
|
588
|
+
if (examplesArr.includes("ai") && backend === "elysia") exitWithError("The 'ai' example is not compatible with the Elysia backend.");
|
|
589
|
+
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
|
|
590
|
+
}
|
|
547
591
|
|
|
548
592
|
//#endregion
|
|
549
593
|
//#region src/prompts/api.ts
|
|
@@ -1004,16 +1048,97 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
1004
1048
|
return response;
|
|
1005
1049
|
}
|
|
1006
1050
|
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/prompts/server-deploy.ts
|
|
1053
|
+
function getDeploymentDisplay$1(deployment) {
|
|
1054
|
+
if (deployment === "wrangler") return {
|
|
1055
|
+
label: "Wrangler",
|
|
1056
|
+
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
1057
|
+
};
|
|
1058
|
+
if (deployment === "alchemy") return {
|
|
1059
|
+
label: "Alchemy",
|
|
1060
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
1061
|
+
};
|
|
1062
|
+
return {
|
|
1063
|
+
label: deployment,
|
|
1064
|
+
hint: `Add ${deployment} deployment`
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
async function getServerDeploymentChoice(deployment, runtime, backend, webDeploy) {
|
|
1068
|
+
if (deployment !== void 0) return deployment;
|
|
1069
|
+
if (backend === "none" || backend === "convex") return "none";
|
|
1070
|
+
const options = [];
|
|
1071
|
+
if (runtime === "workers") ["alchemy", "wrangler"].forEach((deploy) => {
|
|
1072
|
+
const { label, hint } = getDeploymentDisplay$1(deploy);
|
|
1073
|
+
options.unshift({
|
|
1074
|
+
value: deploy,
|
|
1075
|
+
label,
|
|
1076
|
+
hint
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
else options.push({
|
|
1080
|
+
value: "none",
|
|
1081
|
+
label: "None",
|
|
1082
|
+
hint: "Manual setup"
|
|
1083
|
+
});
|
|
1084
|
+
const response = await select({
|
|
1085
|
+
message: "Select server deployment",
|
|
1086
|
+
options,
|
|
1087
|
+
initialValue: webDeploy === "alchemy" ? "alchemy" : runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
|
|
1088
|
+
});
|
|
1089
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1090
|
+
return response;
|
|
1091
|
+
}
|
|
1092
|
+
async function getServerDeploymentToAdd(runtime, existingDeployment) {
|
|
1093
|
+
const options = [];
|
|
1094
|
+
if (runtime === "workers") {
|
|
1095
|
+
if (existingDeployment !== "wrangler") {
|
|
1096
|
+
const { label, hint } = getDeploymentDisplay$1("wrangler");
|
|
1097
|
+
options.push({
|
|
1098
|
+
value: "wrangler",
|
|
1099
|
+
label,
|
|
1100
|
+
hint
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
if (existingDeployment !== "alchemy") {
|
|
1104
|
+
const { label, hint } = getDeploymentDisplay$1("alchemy");
|
|
1105
|
+
options.push({
|
|
1106
|
+
value: "alchemy",
|
|
1107
|
+
label,
|
|
1108
|
+
hint
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (existingDeployment && existingDeployment !== "none") return "none";
|
|
1113
|
+
if (options.length > 0) options.push({
|
|
1114
|
+
value: "none",
|
|
1115
|
+
label: "None",
|
|
1116
|
+
hint: "Skip deployment setup"
|
|
1117
|
+
});
|
|
1118
|
+
if (options.length === 0) return "none";
|
|
1119
|
+
const response = await select({
|
|
1120
|
+
message: "Select server deployment",
|
|
1121
|
+
options,
|
|
1122
|
+
initialValue: runtime === "workers" ? "wrangler" : DEFAULT_CONFIG.serverDeploy
|
|
1123
|
+
});
|
|
1124
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1125
|
+
return response;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1007
1128
|
//#endregion
|
|
1008
1129
|
//#region src/prompts/web-deploy.ts
|
|
1009
1130
|
function hasWebFrontend(frontends) {
|
|
1010
1131
|
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
|
|
1011
1132
|
}
|
|
1012
1133
|
function getDeploymentDisplay(deployment) {
|
|
1013
|
-
if (deployment === "
|
|
1014
|
-
label: "
|
|
1134
|
+
if (deployment === "wrangler") return {
|
|
1135
|
+
label: "Wrangler",
|
|
1015
1136
|
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
1016
1137
|
};
|
|
1138
|
+
if (deployment === "alchemy") return {
|
|
1139
|
+
label: "Alchemy",
|
|
1140
|
+
hint: "Deploy to Cloudflare Workers using Alchemy"
|
|
1141
|
+
};
|
|
1017
1142
|
return {
|
|
1018
1143
|
label: deployment,
|
|
1019
1144
|
hint: `Add ${deployment} deployment`
|
|
@@ -1022,15 +1147,18 @@ function getDeploymentDisplay(deployment) {
|
|
|
1022
1147
|
async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
|
|
1023
1148
|
if (deployment !== void 0) return deployment;
|
|
1024
1149
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1025
|
-
const options = [
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1150
|
+
const options = [
|
|
1151
|
+
"wrangler",
|
|
1152
|
+
"alchemy",
|
|
1153
|
+
"none"
|
|
1154
|
+
].map((deploy) => {
|
|
1155
|
+
const { label, hint } = getDeploymentDisplay(deploy);
|
|
1156
|
+
return {
|
|
1157
|
+
value: deploy,
|
|
1158
|
+
label,
|
|
1159
|
+
hint
|
|
1160
|
+
};
|
|
1161
|
+
});
|
|
1034
1162
|
const response = await select({
|
|
1035
1163
|
message: "Select web deployment",
|
|
1036
1164
|
options,
|
|
@@ -1042,10 +1170,18 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
|
|
|
1042
1170
|
async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
1043
1171
|
if (!hasWebFrontend(frontend)) return "none";
|
|
1044
1172
|
const options = [];
|
|
1045
|
-
if (existingDeployment !== "
|
|
1046
|
-
const { label, hint } = getDeploymentDisplay("
|
|
1173
|
+
if (existingDeployment !== "wrangler") {
|
|
1174
|
+
const { label, hint } = getDeploymentDisplay("wrangler");
|
|
1175
|
+
options.push({
|
|
1176
|
+
value: "wrangler",
|
|
1177
|
+
label,
|
|
1178
|
+
hint
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
if (existingDeployment !== "alchemy") {
|
|
1182
|
+
const { label, hint } = getDeploymentDisplay("alchemy");
|
|
1047
1183
|
options.push({
|
|
1048
|
-
value: "
|
|
1184
|
+
value: "alchemy",
|
|
1049
1185
|
label,
|
|
1050
1186
|
hint
|
|
1051
1187
|
});
|
|
@@ -1081,6 +1217,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1081
1217
|
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
|
|
1082
1218
|
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
|
|
1083
1219
|
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend),
|
|
1220
|
+
serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
|
|
1084
1221
|
git: () => getGitChoice(flags.git),
|
|
1085
1222
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
|
1086
1223
|
install: () => getinstallChoice(flags.install)
|
|
@@ -1120,7 +1257,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1120
1257
|
install: result.install,
|
|
1121
1258
|
dbSetup: result.dbSetup,
|
|
1122
1259
|
api: result.api,
|
|
1123
|
-
webDeploy: result.webDeploy
|
|
1260
|
+
webDeploy: result.webDeploy,
|
|
1261
|
+
serverDeploy: result.serverDeploy
|
|
1124
1262
|
};
|
|
1125
1263
|
}
|
|
1126
1264
|
|
|
@@ -1152,7 +1290,7 @@ async function getProjectName(initialName) {
|
|
|
1152
1290
|
let projectPath = "";
|
|
1153
1291
|
let defaultName = DEFAULT_CONFIG.projectName;
|
|
1154
1292
|
let counter = 1;
|
|
1155
|
-
while (fs.
|
|
1293
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
1156
1294
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
|
1157
1295
|
counter++;
|
|
1158
1296
|
}
|
|
@@ -1199,7 +1337,7 @@ const getLatestCLIVersion = () => {
|
|
|
1199
1337
|
*/
|
|
1200
1338
|
function isTelemetryEnabled() {
|
|
1201
1339
|
const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
|
|
1202
|
-
const BTS_TELEMETRY = "
|
|
1340
|
+
const BTS_TELEMETRY = "0";
|
|
1203
1341
|
if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
|
|
1204
1342
|
if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
|
|
1205
1343
|
return true;
|
|
@@ -1207,11 +1345,16 @@ function isTelemetryEnabled() {
|
|
|
1207
1345
|
|
|
1208
1346
|
//#endregion
|
|
1209
1347
|
//#region src/utils/analytics.ts
|
|
1210
|
-
const POSTHOG_API_KEY = "
|
|
1211
|
-
const POSTHOG_HOST = "
|
|
1348
|
+
const POSTHOG_API_KEY = "random";
|
|
1349
|
+
const POSTHOG_HOST = "random";
|
|
1350
|
+
function generateSessionId() {
|
|
1351
|
+
const rand = Math.random().toString(36).slice(2);
|
|
1352
|
+
const now = Date.now().toString(36);
|
|
1353
|
+
return `cli_${now}${rand}`;
|
|
1354
|
+
}
|
|
1212
1355
|
async function trackProjectCreation(config, disableAnalytics = false) {
|
|
1213
1356
|
if (!isTelemetryEnabled() || disableAnalytics) return;
|
|
1214
|
-
const sessionId =
|
|
1357
|
+
const sessionId = generateSessionId();
|
|
1215
1358
|
const { projectName, projectDir, relativePath,...safeConfig } = config;
|
|
1216
1359
|
const payload = {
|
|
1217
1360
|
api_key: POSTHOG_API_KEY,
|
|
@@ -1219,8 +1362,8 @@ async function trackProjectCreation(config, disableAnalytics = false) {
|
|
|
1219
1362
|
properties: {
|
|
1220
1363
|
...safeConfig,
|
|
1221
1364
|
cli_version: getLatestCLIVersion(),
|
|
1222
|
-
node_version: process.version,
|
|
1223
|
-
platform: process.platform,
|
|
1365
|
+
node_version: typeof process !== "undefined" ? process.version : "",
|
|
1366
|
+
platform: typeof process !== "undefined" ? process.platform : "",
|
|
1224
1367
|
$ip: null
|
|
1225
1368
|
},
|
|
1226
1369
|
distinct_id: sessionId
|
|
@@ -1274,6 +1417,7 @@ function displayConfig(config) {
|
|
|
1274
1417
|
}
|
|
1275
1418
|
if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
|
|
1276
1419
|
if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
|
|
1420
|
+
if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
|
|
1277
1421
|
if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
|
|
1278
1422
|
return configDisplay.join("\n");
|
|
1279
1423
|
}
|
|
@@ -1296,6 +1440,7 @@ function generateReproducibleCommand(config) {
|
|
|
1296
1440
|
else flags.push("--examples none");
|
|
1297
1441
|
flags.push(`--db-setup ${config.dbSetup}`);
|
|
1298
1442
|
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
1443
|
+
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
1299
1444
|
flags.push(config.git ? "--git" : "--no-git");
|
|
1300
1445
|
flags.push(`--package-manager ${config.packageManager}`);
|
|
1301
1446
|
flags.push(config.install ? "--install" : "--no-install");
|
|
@@ -1313,8 +1458,8 @@ function generateReproducibleCommand(config) {
|
|
|
1313
1458
|
async function handleDirectoryConflict(currentPathInput, silent = false) {
|
|
1314
1459
|
while (true) {
|
|
1315
1460
|
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
|
1316
|
-
const dirExists = fs.
|
|
1317
|
-
const dirIsNotEmpty = dirExists && fs.
|
|
1461
|
+
const dirExists = await fs.pathExists(resolvedPath);
|
|
1462
|
+
const dirIsNotEmpty = dirExists && (await fs.readdir(resolvedPath)).length > 0;
|
|
1318
1463
|
if (!dirIsNotEmpty) return {
|
|
1319
1464
|
finalPathInput: currentPathInput,
|
|
1320
1465
|
shouldClearDirectory: false
|
|
@@ -1440,7 +1585,7 @@ const renderTitle = () => {
|
|
|
1440
1585
|
};
|
|
1441
1586
|
|
|
1442
1587
|
//#endregion
|
|
1443
|
-
//#region src/
|
|
1588
|
+
//#region src/utils/config-processing.ts
|
|
1444
1589
|
function processArrayOption(options) {
|
|
1445
1590
|
if (!options || options.length === 0) return [];
|
|
1446
1591
|
if (options.includes("none")) return [];
|
|
@@ -1451,22 +1596,10 @@ function deriveProjectName(projectName, projectDirectory) {
|
|
|
1451
1596
|
if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
|
|
1452
1597
|
return "";
|
|
1453
1598
|
}
|
|
1454
|
-
function
|
|
1455
|
-
const result = ProjectNameSchema.safeParse(name);
|
|
1456
|
-
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1457
|
-
}
|
|
1458
|
-
function processAndValidateFlags(options, providedFlags, projectName) {
|
|
1599
|
+
function processFlags(options, projectName) {
|
|
1459
1600
|
const config = {};
|
|
1460
|
-
if (options.api)
|
|
1461
|
-
config.api = options.api;
|
|
1462
|
-
if (options.api === "none") {
|
|
1463
|
-
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1601
|
+
if (options.api) config.api = options.api;
|
|
1466
1602
|
if (options.backend) config.backend = options.backend;
|
|
1467
|
-
if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
|
|
1468
|
-
if (providedFlags.has("runtime") && options.runtime === "none") exitWithError(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
|
|
1469
|
-
}
|
|
1470
1603
|
if (options.database) config.database = options.database;
|
|
1471
1604
|
if (options.orm) config.orm = options.orm;
|
|
1472
1605
|
if (options.auth !== void 0) config.auth = options.auth;
|
|
@@ -1476,70 +1609,182 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1476
1609
|
if (options.dbSetup) config.dbSetup = options.dbSetup;
|
|
1477
1610
|
if (options.packageManager) config.packageManager = options.packageManager;
|
|
1478
1611
|
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1612
|
+
if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
|
|
1479
1613
|
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
|
1480
|
-
if (derivedName)
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1614
|
+
if (derivedName) config.projectName = projectName || derivedName;
|
|
1615
|
+
if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
|
|
1616
|
+
if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
|
|
1617
|
+
if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
|
|
1618
|
+
return config;
|
|
1619
|
+
}
|
|
1620
|
+
function getProvidedFlags(options) {
|
|
1621
|
+
return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
|
|
1622
|
+
}
|
|
1623
|
+
function validateNoneExclusivity(options, optionName) {
|
|
1624
|
+
if (!options || options.length === 0) return;
|
|
1625
|
+
if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
|
|
1626
|
+
}
|
|
1627
|
+
function validateArrayOptions(options) {
|
|
1628
|
+
validateNoneExclusivity(options.frontend, "frontend options");
|
|
1629
|
+
validateNoneExclusivity(options.addons, "addons");
|
|
1630
|
+
validateNoneExclusivity(options.examples, "examples");
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/utils/config-validation.ts
|
|
1635
|
+
function validateDatabaseOrmAuth(cfg, flags) {
|
|
1636
|
+
const db = cfg.database;
|
|
1637
|
+
const orm = cfg.orm;
|
|
1638
|
+
const has = (k) => flags ? flags.has(k) : true;
|
|
1639
|
+
if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
|
|
1640
|
+
if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1641
|
+
if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1642
|
+
if (has("database") && has("orm") && db && db !== "none" && orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
|
|
1643
|
+
if (has("orm") && has("database") && orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1644
|
+
if (has("auth") && has("database") && cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1645
|
+
if (cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1646
|
+
if (orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1647
|
+
}
|
|
1648
|
+
function validateDatabaseSetup(config, providedFlags) {
|
|
1649
|
+
const { dbSetup, database, runtime } = config;
|
|
1650
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("database") && dbSetup && dbSetup !== "none" && database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
|
|
1651
|
+
const setupValidations = {
|
|
1652
|
+
turso: {
|
|
1653
|
+
database: "sqlite",
|
|
1654
|
+
errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
|
|
1655
|
+
},
|
|
1656
|
+
neon: {
|
|
1657
|
+
database: "postgres",
|
|
1658
|
+
errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1659
|
+
},
|
|
1660
|
+
"prisma-postgres": {
|
|
1661
|
+
database: "postgres",
|
|
1662
|
+
errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1663
|
+
},
|
|
1664
|
+
"mongodb-atlas": {
|
|
1665
|
+
database: "mongodb",
|
|
1666
|
+
errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
|
|
1667
|
+
},
|
|
1668
|
+
supabase: {
|
|
1669
|
+
database: "postgres",
|
|
1670
|
+
errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
1671
|
+
},
|
|
1672
|
+
d1: {
|
|
1673
|
+
database: "sqlite",
|
|
1674
|
+
runtime: "workers",
|
|
1675
|
+
errorMessage: "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime."
|
|
1676
|
+
},
|
|
1677
|
+
docker: { errorMessage: "Docker setup is not compatible with SQLite database or Cloudflare Workers runtime." },
|
|
1678
|
+
none: { errorMessage: "" }
|
|
1679
|
+
};
|
|
1680
|
+
if (dbSetup && dbSetup !== "none") {
|
|
1681
|
+
const validation = setupValidations[dbSetup];
|
|
1682
|
+
if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
|
|
1683
|
+
if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
|
|
1684
|
+
if (dbSetup === "docker") {
|
|
1685
|
+
if (database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
|
|
1686
|
+
if (runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
function validateBackendConstraints(config, providedFlags, options) {
|
|
1691
|
+
const { backend } = config;
|
|
1692
|
+
if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none") {
|
|
1693
|
+
if (providedFlags.has("runtime") && options.runtime === "none") exitWithError("'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.");
|
|
1504
1694
|
}
|
|
1505
|
-
if (
|
|
1506
|
-
const incompatibleFlags = incompatibleFlagsForBackend(
|
|
1507
|
-
if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${
|
|
1508
|
-
if (
|
|
1695
|
+
if (backend === "convex" || backend === "none") {
|
|
1696
|
+
const incompatibleFlags = incompatibleFlagsForBackend(backend, providedFlags, options);
|
|
1697
|
+
if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1698
|
+
if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
|
|
1509
1699
|
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
|
1510
1700
|
if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1511
1701
|
}
|
|
1512
1702
|
coerceBackendPresets(config);
|
|
1513
1703
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
if (
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
if (
|
|
1526
|
-
|
|
1527
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
|
|
1528
|
-
if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1529
|
-
}
|
|
1530
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
|
|
1531
|
-
if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
|
|
1532
|
-
}
|
|
1704
|
+
}
|
|
1705
|
+
function validateFrontendConstraints(config, providedFlags) {
|
|
1706
|
+
const { frontend } = config;
|
|
1707
|
+
if (frontend && frontend.length > 0) {
|
|
1708
|
+
ensureSingleWebAndNative(frontend);
|
|
1709
|
+
if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend);
|
|
1710
|
+
const hasWebFrontendFlag = frontend.some((f) => isWebFrontend(f));
|
|
1711
|
+
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
function validateApiConstraints(config, options) {
|
|
1715
|
+
if (config.api === "none") {
|
|
1716
|
+
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
|
|
1533
1717
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1718
|
+
}
|
|
1719
|
+
function validateFullConfig(config, providedFlags, options) {
|
|
1720
|
+
validateDatabaseOrmAuth(config, providedFlags);
|
|
1721
|
+
validateDatabaseSetup(config, providedFlags);
|
|
1722
|
+
validateBackendConstraints(config, providedFlags, options);
|
|
1723
|
+
validateFrontendConstraints(config, providedFlags);
|
|
1724
|
+
validateApiConstraints(config, options);
|
|
1725
|
+
validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
|
1536
1726
|
validateWorkersCompatibility(providedFlags, options, config);
|
|
1537
|
-
|
|
1538
|
-
|
|
1727
|
+
if (config.addons && config.addons.length > 0) {
|
|
1728
|
+
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
|
1729
|
+
config.addons = [...new Set(config.addons)];
|
|
1730
|
+
}
|
|
1731
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
1732
|
+
}
|
|
1733
|
+
function validateConfigForProgrammaticUse(config) {
|
|
1734
|
+
try {
|
|
1735
|
+
validateDatabaseOrmAuth(config);
|
|
1736
|
+
if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
|
|
1737
|
+
validateApiFrontendCompatibility(config.api, config.frontend);
|
|
1738
|
+
if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend);
|
|
1739
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
if (error instanceof Error) throw error;
|
|
1742
|
+
throw new Error(String(error));
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
//#endregion
|
|
1747
|
+
//#region src/utils/project-name-validation.ts
|
|
1748
|
+
function validateProjectName(name) {
|
|
1749
|
+
const result = ProjectNameSchema.safeParse(name);
|
|
1750
|
+
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1751
|
+
}
|
|
1752
|
+
function validateProjectNameThrow(name) {
|
|
1753
|
+
const result = ProjectNameSchema.safeParse(name);
|
|
1754
|
+
if (!result.success) throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
|
|
1755
|
+
}
|
|
1756
|
+
function extractAndValidateProjectName(projectName, projectDirectory, throwOnError = false) {
|
|
1757
|
+
const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
|
|
1758
|
+
if (!derivedName) return "";
|
|
1759
|
+
const nameToValidate = projectName ? path.basename(projectName) : derivedName;
|
|
1760
|
+
if (throwOnError) validateProjectNameThrow(nameToValidate);
|
|
1761
|
+
else validateProjectName(nameToValidate);
|
|
1762
|
+
return projectName || derivedName;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/validation.ts
|
|
1767
|
+
function processAndValidateFlags(options, providedFlags, projectName) {
|
|
1768
|
+
try {
|
|
1769
|
+
validateArrayOptions(options);
|
|
1770
|
+
} catch (error) {
|
|
1771
|
+
exitWithError(error instanceof Error ? error.message : String(error));
|
|
1772
|
+
}
|
|
1773
|
+
const config = processFlags(options, projectName);
|
|
1774
|
+
const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, false);
|
|
1775
|
+
if (validatedProjectName) config.projectName = validatedProjectName;
|
|
1776
|
+
validateFullConfig(config, providedFlags, options);
|
|
1539
1777
|
return config;
|
|
1540
1778
|
}
|
|
1541
|
-
function
|
|
1542
|
-
|
|
1779
|
+
function processProvidedFlagsWithoutValidation(options, projectName) {
|
|
1780
|
+
const config = processFlags(options, projectName);
|
|
1781
|
+
const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, true);
|
|
1782
|
+
if (validatedProjectName) config.projectName = validatedProjectName;
|
|
1783
|
+
return config;
|
|
1784
|
+
}
|
|
1785
|
+
function validateConfigCompatibility(config, providedFlags, options) {
|
|
1786
|
+
if (options && providedFlags) validateFullConfig(config, providedFlags, options);
|
|
1787
|
+
else validateConfigForProgrammaticUse(config);
|
|
1543
1788
|
}
|
|
1544
1789
|
|
|
1545
1790
|
//#endregion
|
|
@@ -1560,7 +1805,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1560
1805
|
packageManager: projectConfig.packageManager,
|
|
1561
1806
|
dbSetup: projectConfig.dbSetup,
|
|
1562
1807
|
api: projectConfig.api,
|
|
1563
|
-
webDeploy: projectConfig.webDeploy
|
|
1808
|
+
webDeploy: projectConfig.webDeploy,
|
|
1809
|
+
serverDeploy: projectConfig.serverDeploy
|
|
1564
1810
|
};
|
|
1565
1811
|
const baseContent = {
|
|
1566
1812
|
$schema: "https://r2.better-t-stack.dev/schema.json",
|
|
@@ -1577,7 +1823,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1577
1823
|
packageManager: btsConfig.packageManager,
|
|
1578
1824
|
dbSetup: btsConfig.dbSetup,
|
|
1579
1825
|
api: btsConfig.api,
|
|
1580
|
-
webDeploy: btsConfig.webDeploy
|
|
1826
|
+
webDeploy: btsConfig.webDeploy,
|
|
1827
|
+
serverDeploy: btsConfig.serverDeploy
|
|
1581
1828
|
};
|
|
1582
1829
|
let configContent = JSON.stringify(baseContent);
|
|
1583
1830
|
const formatResult = JSONC.format(configContent, void 0, {
|
|
@@ -1670,7 +1917,7 @@ function getPackageExecutionCommand(packageManager, commandWithArgs) {
|
|
|
1670
1917
|
}
|
|
1671
1918
|
|
|
1672
1919
|
//#endregion
|
|
1673
|
-
//#region src/helpers/
|
|
1920
|
+
//#region src/helpers/addons/fumadocs-setup.ts
|
|
1674
1921
|
const TEMPLATES = {
|
|
1675
1922
|
"next-mdx": {
|
|
1676
1923
|
label: "Next.js: Fumadocs MDX",
|
|
@@ -1757,9 +2004,9 @@ handlebars.registerHelper("or", (a, b) => a || b);
|
|
|
1757
2004
|
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
1758
2005
|
|
|
1759
2006
|
//#endregion
|
|
1760
|
-
//#region src/helpers/
|
|
2007
|
+
//#region src/helpers/core/template-manager.ts
|
|
1761
2008
|
async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
|
|
1762
|
-
const sourceFiles = await
|
|
2009
|
+
const sourceFiles = await glob(sourcePattern, {
|
|
1763
2010
|
cwd: baseSourceDir,
|
|
1764
2011
|
dot: true,
|
|
1765
2012
|
onlyFiles: true,
|
|
@@ -2073,10 +2320,6 @@ async function handleExtras(projectDir, context) {
|
|
|
2073
2320
|
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
|
|
2074
2321
|
if (await fs.pathExists(npmrcTemplateSrc)) await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
|
|
2075
2322
|
}
|
|
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
2323
|
}
|
|
2081
2324
|
async function setupDockerComposeTemplates(projectDir, context) {
|
|
2082
2325
|
if (context.dbSetup !== "docker" || context.database === "none") return;
|
|
@@ -2085,29 +2328,62 @@ async function setupDockerComposeTemplates(projectDir, context) {
|
|
|
2085
2328
|
if (await fs.pathExists(dockerSrcDir)) await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
|
|
2086
2329
|
}
|
|
2087
2330
|
async function setupDeploymentTemplates(projectDir, context) {
|
|
2088
|
-
if (context.webDeploy === "
|
|
2089
|
-
|
|
2331
|
+
if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
|
|
2332
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2333
|
+
if (await fs.pathExists(alchemyTemplateSrc)) {
|
|
2334
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, projectDir, context);
|
|
2335
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2336
|
+
if (await fs.pathExists(serverAppDir)) {
|
|
2337
|
+
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2338
|
+
await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
} else {
|
|
2342
|
+
if (context.webDeploy === "alchemy") {
|
|
2343
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2344
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2345
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
|
|
2346
|
+
}
|
|
2347
|
+
if (context.serverDeploy === "alchemy") {
|
|
2348
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2349
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2350
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
|
|
2351
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2352
|
+
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2353
|
+
await processAndCopyFiles("wrangler.jsonc.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
|
|
2090
2358
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2359
|
+
if (await fs.pathExists(webAppDir)) {
|
|
2360
|
+
const frontends = context.frontend;
|
|
2361
|
+
const templateMap = {
|
|
2362
|
+
"tanstack-router": "react/tanstack-router",
|
|
2363
|
+
"tanstack-start": "react/tanstack-start",
|
|
2364
|
+
"react-router": "react/react-router",
|
|
2365
|
+
solid: "solid",
|
|
2366
|
+
next: "react/next",
|
|
2367
|
+
nuxt: "nuxt",
|
|
2368
|
+
svelte: "svelte"
|
|
2369
|
+
};
|
|
2370
|
+
for (const f of frontends) if (templateMap[f]) {
|
|
2371
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
|
|
2372
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
|
|
2377
|
+
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2378
|
+
if (await fs.pathExists(serverAppDir)) {
|
|
2379
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
|
|
2380
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
|
|
2105
2381
|
}
|
|
2106
2382
|
}
|
|
2107
2383
|
}
|
|
2108
2384
|
|
|
2109
2385
|
//#endregion
|
|
2110
|
-
//#region src/helpers/
|
|
2386
|
+
//#region src/helpers/addons/ruler-setup.ts
|
|
2111
2387
|
async function setupVibeRules(config) {
|
|
2112
2388
|
const { packageManager, projectDir } = config;
|
|
2113
2389
|
try {
|
|
@@ -2189,7 +2465,7 @@ async function addRulerScriptToPackageJson(projectDir, packageManager) {
|
|
|
2189
2465
|
}
|
|
2190
2466
|
|
|
2191
2467
|
//#endregion
|
|
2192
|
-
//#region src/helpers/
|
|
2468
|
+
//#region src/helpers/addons/starlight-setup.ts
|
|
2193
2469
|
async function setupStarlight(config) {
|
|
2194
2470
|
const { packageManager, projectDir } = config;
|
|
2195
2471
|
const s = spinner();
|
|
@@ -2221,7 +2497,7 @@ async function setupStarlight(config) {
|
|
|
2221
2497
|
}
|
|
2222
2498
|
|
|
2223
2499
|
//#endregion
|
|
2224
|
-
//#region src/helpers/
|
|
2500
|
+
//#region src/helpers/addons/tauri-setup.ts
|
|
2225
2501
|
async function setupTauri(config) {
|
|
2226
2502
|
const { packageManager, frontend, projectDir } = config;
|
|
2227
2503
|
const s = spinner();
|
|
@@ -2258,8 +2534,8 @@ async function setupTauri(config) {
|
|
|
2258
2534
|
`--window-title=${path.basename(projectDir)}`,
|
|
2259
2535
|
`--frontend-dist=${frontendDist}`,
|
|
2260
2536
|
`--dev-url=${devUrl}`,
|
|
2261
|
-
`--before-dev-command
|
|
2262
|
-
`--before-build-command
|
|
2537
|
+
`--before-dev-command="${packageManager} run dev"`,
|
|
2538
|
+
`--before-build-command="${packageManager} run build"`
|
|
2263
2539
|
];
|
|
2264
2540
|
const tauriArgsString = tauriArgs.join(" ");
|
|
2265
2541
|
const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
|
|
@@ -2277,7 +2553,7 @@ async function setupTauri(config) {
|
|
|
2277
2553
|
}
|
|
2278
2554
|
|
|
2279
2555
|
//#endregion
|
|
2280
|
-
//#region src/helpers/
|
|
2556
|
+
//#region src/helpers/addons/ultracite-setup.ts
|
|
2281
2557
|
const EDITORS = {
|
|
2282
2558
|
vscode: {
|
|
2283
2559
|
label: "VSCode / Cursor / Windsurf",
|
|
@@ -2346,7 +2622,7 @@ async function setupUltracite(config, hasHusky) {
|
|
|
2346
2622
|
];
|
|
2347
2623
|
if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
|
|
2348
2624
|
if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
|
|
2349
|
-
if (hasHusky) ultraciteArgs.push("--
|
|
2625
|
+
if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
|
|
2350
2626
|
const ultraciteArgsString = ultraciteArgs.join(" ");
|
|
2351
2627
|
const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
|
|
2352
2628
|
const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
@@ -2384,7 +2660,7 @@ function ensureArrayProperty(obj, name) {
|
|
|
2384
2660
|
}
|
|
2385
2661
|
|
|
2386
2662
|
//#endregion
|
|
2387
|
-
//#region src/helpers/
|
|
2663
|
+
//#region src/helpers/addons/vite-pwa-setup.ts
|
|
2388
2664
|
async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
2389
2665
|
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
|
2390
2666
|
if (!sourceFile) throw new Error("vite config not found");
|
|
@@ -2418,7 +2694,7 @@ async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
|
2418
2694
|
}
|
|
2419
2695
|
|
|
2420
2696
|
//#endregion
|
|
2421
|
-
//#region src/helpers/
|
|
2697
|
+
//#region src/helpers/addons/addons-setup.ts
|
|
2422
2698
|
async function setupAddons(config, isAddCommand = false) {
|
|
2423
2699
|
const { addons, frontend, projectDir, packageManager } = config;
|
|
2424
2700
|
const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
|
|
@@ -2552,7 +2828,7 @@ async function setupOxlint(projectDir, packageManager) {
|
|
|
2552
2828
|
}
|
|
2553
2829
|
|
|
2554
2830
|
//#endregion
|
|
2555
|
-
//#region src/helpers/
|
|
2831
|
+
//#region src/helpers/core/detect-project-config.ts
|
|
2556
2832
|
async function detectProjectConfig(projectDir) {
|
|
2557
2833
|
try {
|
|
2558
2834
|
const btsConfig = await readBtsConfig(projectDir);
|
|
@@ -2570,7 +2846,8 @@ async function detectProjectConfig(projectDir) {
|
|
|
2570
2846
|
packageManager: btsConfig.packageManager,
|
|
2571
2847
|
dbSetup: btsConfig.dbSetup,
|
|
2572
2848
|
api: btsConfig.api,
|
|
2573
|
-
webDeploy: btsConfig.webDeploy
|
|
2849
|
+
webDeploy: btsConfig.webDeploy,
|
|
2850
|
+
serverDeploy: btsConfig.serverDeploy
|
|
2574
2851
|
};
|
|
2575
2852
|
return null;
|
|
2576
2853
|
} catch (_error) {
|
|
@@ -2586,7 +2863,7 @@ async function isBetterTStackProject(projectDir) {
|
|
|
2586
2863
|
}
|
|
2587
2864
|
|
|
2588
2865
|
//#endregion
|
|
2589
|
-
//#region src/helpers/
|
|
2866
|
+
//#region src/helpers/core/install-dependencies.ts
|
|
2590
2867
|
async function installDependencies({ projectDir, packageManager }) {
|
|
2591
2868
|
const s = spinner();
|
|
2592
2869
|
try {
|
|
@@ -2603,7 +2880,7 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
2603
2880
|
}
|
|
2604
2881
|
|
|
2605
2882
|
//#endregion
|
|
2606
|
-
//#region src/helpers/
|
|
2883
|
+
//#region src/helpers/core/add-addons.ts
|
|
2607
2884
|
async function addAddonsToProject(input) {
|
|
2608
2885
|
try {
|
|
2609
2886
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -2628,7 +2905,8 @@ async function addAddonsToProject(input) {
|
|
|
2628
2905
|
install: input.install || false,
|
|
2629
2906
|
dbSetup: detectedConfig.dbSetup || "none",
|
|
2630
2907
|
api: detectedConfig.api || "none",
|
|
2631
|
-
webDeploy: detectedConfig.webDeploy || "none"
|
|
2908
|
+
webDeploy: detectedConfig.webDeploy || "none",
|
|
2909
|
+
serverDeploy: detectedConfig.serverDeploy || "none"
|
|
2632
2910
|
};
|
|
2633
2911
|
for (const addon of input.addons) {
|
|
2634
2912
|
const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
|
|
@@ -2651,12 +2929,92 @@ async function addAddonsToProject(input) {
|
|
|
2651
2929
|
}
|
|
2652
2930
|
|
|
2653
2931
|
//#endregion
|
|
2654
|
-
//#region src/helpers/
|
|
2655
|
-
async function
|
|
2932
|
+
//#region src/helpers/deployment/server-deploy-setup.ts
|
|
2933
|
+
async function setupServerDeploy(config) {
|
|
2934
|
+
const { serverDeploy, webDeploy, projectDir } = config;
|
|
2935
|
+
const { packageManager } = config;
|
|
2936
|
+
if (serverDeploy === "none") return;
|
|
2937
|
+
if (serverDeploy === "alchemy" && webDeploy === "alchemy") return;
|
|
2938
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
2939
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2940
|
+
if (serverDeploy === "wrangler") {
|
|
2941
|
+
await setupWorkersServerDeploy(serverDir, packageManager);
|
|
2942
|
+
await generateCloudflareWorkerTypes({
|
|
2943
|
+
serverDir,
|
|
2944
|
+
packageManager
|
|
2945
|
+
});
|
|
2946
|
+
} else if (serverDeploy === "alchemy") await setupAlchemyServerDeploy(serverDir, packageManager);
|
|
2947
|
+
}
|
|
2948
|
+
async function setupWorkersServerDeploy(serverDir, _packageManager) {
|
|
2949
|
+
const packageJsonPath = path.join(serverDir, "package.json");
|
|
2950
|
+
if (!await fs.pathExists(packageJsonPath)) return;
|
|
2951
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
2952
|
+
packageJson.scripts = {
|
|
2953
|
+
...packageJson.scripts,
|
|
2954
|
+
dev: "wrangler dev --port=3000",
|
|
2955
|
+
start: "wrangler dev",
|
|
2956
|
+
deploy: "wrangler deploy",
|
|
2957
|
+
build: "wrangler deploy --dry-run",
|
|
2958
|
+
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
|
|
2959
|
+
};
|
|
2960
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
2961
|
+
await addPackageDependency({
|
|
2962
|
+
devDependencies: [
|
|
2963
|
+
"wrangler",
|
|
2964
|
+
"@types/node",
|
|
2965
|
+
"@cloudflare/workers-types"
|
|
2966
|
+
],
|
|
2967
|
+
projectDir: serverDir
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
async function generateCloudflareWorkerTypes({ serverDir, packageManager }) {
|
|
2971
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2972
|
+
const s = spinner();
|
|
2973
|
+
try {
|
|
2974
|
+
s.start("Generating Cloudflare Workers types...");
|
|
2975
|
+
const runCmd = packageManager === "npm" ? "npm" : packageManager;
|
|
2976
|
+
await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
|
|
2977
|
+
s.stop("Cloudflare Workers types generated successfully!");
|
|
2978
|
+
} catch {
|
|
2979
|
+
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
|
2980
|
+
const managerCmd = `${packageManager} run`;
|
|
2981
|
+
log.warn(`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
async function setupAlchemyServerDeploy(serverDir, _packageManager) {
|
|
2985
|
+
if (!await fs.pathExists(serverDir)) return;
|
|
2986
|
+
await addPackageDependency({
|
|
2987
|
+
devDependencies: [
|
|
2988
|
+
"alchemy",
|
|
2989
|
+
"wrangler",
|
|
2990
|
+
"@types/node",
|
|
2991
|
+
"@cloudflare/workers-types",
|
|
2992
|
+
"dotenv"
|
|
2993
|
+
],
|
|
2994
|
+
projectDir: serverDir
|
|
2995
|
+
});
|
|
2996
|
+
const packageJsonPath = path.join(serverDir, "package.json");
|
|
2997
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
2998
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
2999
|
+
packageJson.scripts = {
|
|
3000
|
+
...packageJson.scripts,
|
|
3001
|
+
dev: "wrangler dev --port=3000",
|
|
3002
|
+
build: "wrangler deploy --dry-run",
|
|
3003
|
+
deploy: "alchemy deploy",
|
|
3004
|
+
destroy: "alchemy destroy",
|
|
3005
|
+
"alchemy:dev": "alchemy dev"
|
|
3006
|
+
};
|
|
3007
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
//#endregion
|
|
3012
|
+
//#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
|
|
3013
|
+
async function setupNextAlchemyDeploy(projectDir, _packageManager) {
|
|
2656
3014
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2657
3015
|
if (!await fs.pathExists(webAppDir)) return;
|
|
2658
3016
|
await addPackageDependency({
|
|
2659
|
-
devDependencies: ["
|
|
3017
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
2660
3018
|
projectDir: webAppDir
|
|
2661
3019
|
});
|
|
2662
3020
|
const pkgPath = path.join(webAppDir, "package.json");
|
|
@@ -2664,63 +3022,538 @@ async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
|
2664
3022
|
const pkg = await fs.readJson(pkgPath);
|
|
2665
3023
|
pkg.scripts = {
|
|
2666
3024
|
...pkg.scripts,
|
|
2667
|
-
deploy:
|
|
2668
|
-
|
|
3025
|
+
deploy: "alchemy deploy",
|
|
3026
|
+
destroy: "alchemy destroy",
|
|
3027
|
+
"alchemy:dev": "alchemy dev"
|
|
2669
3028
|
};
|
|
2670
3029
|
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2671
3030
|
}
|
|
2672
|
-
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
2673
|
-
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
2674
|
-
const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
|
|
2675
|
-
if (!sourceFile) return;
|
|
2676
|
-
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
2677
|
-
const expression = expr.getExpression();
|
|
2678
|
-
return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
|
|
2679
|
-
});
|
|
2680
|
-
if (!defineCall) return;
|
|
2681
|
-
const configObj = defineCall.getArguments()[0];
|
|
2682
|
-
if (!configObj) return;
|
|
2683
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2684
|
-
const compatProp = configObj.getProperty("compatibilityDate");
|
|
2685
|
-
if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
|
|
2686
|
-
else configObj.addPropertyAssignment({
|
|
2687
|
-
name: "compatibilityDate",
|
|
2688
|
-
initializer: `'${today}'`
|
|
2689
|
-
});
|
|
2690
|
-
const nitroInitializer = `{
|
|
2691
|
-
preset: "cloudflare_module",
|
|
2692
|
-
cloudflare: {
|
|
2693
|
-
deployConfig: true,
|
|
2694
|
-
nodeCompat: true
|
|
2695
|
-
}
|
|
2696
|
-
}`;
|
|
2697
|
-
const nitroProp = configObj.getProperty("nitro");
|
|
2698
|
-
if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
|
|
2699
|
-
else configObj.addPropertyAssignment({
|
|
2700
|
-
name: "nitro",
|
|
2701
|
-
initializer: nitroInitializer
|
|
2702
|
-
});
|
|
2703
|
-
const modulesProp = configObj.getProperty("modules");
|
|
2704
|
-
if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
2705
|
-
const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
|
|
2706
|
-
if (arrayExpr) {
|
|
2707
|
-
const alreadyHas = arrayExpr.getElements().some((el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev");
|
|
2708
|
-
if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'");
|
|
2709
|
-
}
|
|
2710
|
-
} else configObj.addPropertyAssignment({
|
|
2711
|
-
name: "modules",
|
|
2712
|
-
initializer: "['nitro-cloudflare-dev']"
|
|
2713
|
-
});
|
|
2714
|
-
await tsProject.save();
|
|
2715
3031
|
}
|
|
2716
3032
|
|
|
2717
3033
|
//#endregion
|
|
2718
|
-
//#region src/helpers/
|
|
2719
|
-
async function
|
|
3034
|
+
//#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
|
|
3035
|
+
async function setupNuxtAlchemyDeploy(projectDir, _packageManager) {
|
|
2720
3036
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2721
3037
|
if (!await fs.pathExists(webAppDir)) return;
|
|
2722
3038
|
await addPackageDependency({
|
|
2723
|
-
devDependencies: [
|
|
3039
|
+
devDependencies: [
|
|
3040
|
+
"alchemy",
|
|
3041
|
+
"nitro-cloudflare-dev",
|
|
3042
|
+
"dotenv"
|
|
3043
|
+
],
|
|
3044
|
+
projectDir: webAppDir
|
|
3045
|
+
});
|
|
3046
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3047
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3048
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3049
|
+
pkg.scripts = {
|
|
3050
|
+
...pkg.scripts,
|
|
3051
|
+
deploy: "alchemy deploy",
|
|
3052
|
+
destroy: "alchemy destroy",
|
|
3053
|
+
"alchemy:dev": "alchemy dev"
|
|
3054
|
+
};
|
|
3055
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3056
|
+
}
|
|
3057
|
+
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
3058
|
+
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
3059
|
+
try {
|
|
3060
|
+
const project = new Project({ manipulationSettings: {
|
|
3061
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3062
|
+
quoteKind: QuoteKind.Double
|
|
3063
|
+
} });
|
|
3064
|
+
project.addSourceFileAtPath(nuxtConfigPath);
|
|
3065
|
+
const sourceFile = project.getSourceFileOrThrow(nuxtConfigPath);
|
|
3066
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3067
|
+
if (!exportAssignment) return;
|
|
3068
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3069
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
|
|
3070
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3071
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3072
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3073
|
+
if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
|
|
3074
|
+
name: "nitro",
|
|
3075
|
+
initializer: `{
|
|
3076
|
+
preset: "cloudflare_module",
|
|
3077
|
+
cloudflare: {
|
|
3078
|
+
deployConfig: true,
|
|
3079
|
+
nodeCompat: true
|
|
3080
|
+
}
|
|
3081
|
+
}`
|
|
3082
|
+
});
|
|
3083
|
+
const modulesProperty = configObject.getProperty("modules");
|
|
3084
|
+
if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
|
|
3085
|
+
const initializer = modulesProperty.getInitializer();
|
|
3086
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3087
|
+
const hasModule = initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'");
|
|
3088
|
+
if (!hasModule) initializer.addElement("\"nitro-cloudflare-dev\"");
|
|
3089
|
+
}
|
|
3090
|
+
} else if (!modulesProperty) configObject.addPropertyAssignment({
|
|
3091
|
+
name: "modules",
|
|
3092
|
+
initializer: "[\"nitro-cloudflare-dev\"]"
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
await project.save();
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
console.warn("Failed to update nuxt.config.ts:", error);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
//#endregion
|
|
3102
|
+
//#region src/helpers/deployment/alchemy/alchemy-react-router-setup.ts
|
|
3103
|
+
async function setupReactRouterAlchemyDeploy(projectDir, _packageManager) {
|
|
3104
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3105
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3106
|
+
await addPackageDependency({
|
|
3107
|
+
devDependencies: [
|
|
3108
|
+
"alchemy",
|
|
3109
|
+
"@cloudflare/vite-plugin",
|
|
3110
|
+
"dotenv"
|
|
3111
|
+
],
|
|
3112
|
+
projectDir: webAppDir
|
|
3113
|
+
});
|
|
3114
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3115
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3116
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3117
|
+
pkg.scripts = {
|
|
3118
|
+
...pkg.scripts,
|
|
3119
|
+
deploy: "alchemy deploy",
|
|
3120
|
+
destroy: "alchemy destroy",
|
|
3121
|
+
"alchemy:dev": "alchemy dev"
|
|
3122
|
+
};
|
|
3123
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3124
|
+
}
|
|
3125
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3126
|
+
if (await fs.pathExists(viteConfigPath)) try {
|
|
3127
|
+
const project = new Project({ manipulationSettings: {
|
|
3128
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3129
|
+
quoteKind: QuoteKind.Double
|
|
3130
|
+
} });
|
|
3131
|
+
project.addSourceFileAtPath(viteConfigPath);
|
|
3132
|
+
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
|
3133
|
+
const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/react-router");
|
|
3134
|
+
if (!alchemyImport) sourceFile.addImportDeclaration({
|
|
3135
|
+
moduleSpecifier: "alchemy/cloudflare/react-router",
|
|
3136
|
+
defaultImport: "alchemy"
|
|
3137
|
+
});
|
|
3138
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3139
|
+
if (!exportAssignment) return;
|
|
3140
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3141
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
|
|
3142
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3143
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3144
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3145
|
+
const pluginsProperty = configObject.getProperty("plugins");
|
|
3146
|
+
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
|
3147
|
+
const initializer = pluginsProperty.getInitializer();
|
|
3148
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3149
|
+
const hasCloudflarePlugin = initializer.getElements().some((el) => el.getText().includes("cloudflare("));
|
|
3150
|
+
if (!hasCloudflarePlugin) initializer.addElement("alchemy()");
|
|
3151
|
+
}
|
|
3152
|
+
} else if (!pluginsProperty) configObject.addPropertyAssignment({
|
|
3153
|
+
name: "plugins",
|
|
3154
|
+
initializer: "[alchemy()]"
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
await project.save();
|
|
3158
|
+
} catch (error) {
|
|
3159
|
+
console.warn("Failed to update vite.config.ts:", error);
|
|
3160
|
+
}
|
|
3161
|
+
const reactRouterConfigPath = path.join(webAppDir, "react-router.config.ts");
|
|
3162
|
+
if (await fs.pathExists(reactRouterConfigPath)) try {
|
|
3163
|
+
const project = new Project({ manipulationSettings: {
|
|
3164
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3165
|
+
quoteKind: QuoteKind.Double
|
|
3166
|
+
} });
|
|
3167
|
+
project.addSourceFileAtPath(reactRouterConfigPath);
|
|
3168
|
+
const sourceFile = project.getSourceFileOrThrow(reactRouterConfigPath);
|
|
3169
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3170
|
+
if (!exportAssignment) return;
|
|
3171
|
+
const configExpression = exportAssignment.getExpression();
|
|
3172
|
+
let configObject;
|
|
3173
|
+
if (Node.isObjectLiteralExpression(configExpression)) configObject = configExpression;
|
|
3174
|
+
else if (Node.isSatisfiesExpression(configExpression)) {
|
|
3175
|
+
const expression = configExpression.getExpression();
|
|
3176
|
+
if (Node.isObjectLiteralExpression(expression)) configObject = expression;
|
|
3177
|
+
}
|
|
3178
|
+
if (!configObject || !Node.isObjectLiteralExpression(configObject)) return;
|
|
3179
|
+
const futureProperty = configObject.getProperty("future");
|
|
3180
|
+
if (!futureProperty) configObject.addPropertyAssignment({
|
|
3181
|
+
name: "future",
|
|
3182
|
+
initializer: `{
|
|
3183
|
+
unstable_viteEnvironmentApi: true,
|
|
3184
|
+
}`
|
|
3185
|
+
});
|
|
3186
|
+
else if (Node.isPropertyAssignment(futureProperty)) {
|
|
3187
|
+
const futureInitializer = futureProperty.getInitializer();
|
|
3188
|
+
if (Node.isObjectLiteralExpression(futureInitializer)) {
|
|
3189
|
+
const viteEnvApiProp = futureInitializer.getProperty("unstable_viteEnvironmentApi");
|
|
3190
|
+
if (!viteEnvApiProp) futureInitializer.addPropertyAssignment({
|
|
3191
|
+
name: "unstable_viteEnvironmentApi",
|
|
3192
|
+
initializer: "true"
|
|
3193
|
+
});
|
|
3194
|
+
else if (Node.isPropertyAssignment(viteEnvApiProp)) {
|
|
3195
|
+
const value = viteEnvApiProp.getInitializer()?.getText();
|
|
3196
|
+
if (value === "false") viteEnvApiProp.setInitializer("true");
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
await project.save();
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
console.warn("Failed to update react-router.config.ts:", error);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
//#endregion
|
|
3207
|
+
//#region src/helpers/deployment/alchemy/alchemy-solid-setup.ts
|
|
3208
|
+
async function setupSolidAlchemyDeploy(projectDir, _packageManager) {
|
|
3209
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3210
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3211
|
+
await addPackageDependency({
|
|
3212
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3213
|
+
projectDir: webAppDir
|
|
3214
|
+
});
|
|
3215
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3216
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3217
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3218
|
+
pkg.scripts = {
|
|
3219
|
+
...pkg.scripts,
|
|
3220
|
+
deploy: "alchemy deploy",
|
|
3221
|
+
destroy: "alchemy destroy",
|
|
3222
|
+
"alchemy:dev": "alchemy dev"
|
|
3223
|
+
};
|
|
3224
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
//#endregion
|
|
3229
|
+
//#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
|
|
3230
|
+
async function setupSvelteAlchemyDeploy(projectDir, _packageManager) {
|
|
3231
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3232
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3233
|
+
await addPackageDependency({
|
|
3234
|
+
devDependencies: [
|
|
3235
|
+
"alchemy",
|
|
3236
|
+
"@sveltejs/adapter-cloudflare",
|
|
3237
|
+
"dotenv"
|
|
3238
|
+
],
|
|
3239
|
+
projectDir: webAppDir
|
|
3240
|
+
});
|
|
3241
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3242
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3243
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3244
|
+
pkg.scripts = {
|
|
3245
|
+
...pkg.scripts,
|
|
3246
|
+
deploy: "alchemy deploy",
|
|
3247
|
+
destroy: "alchemy destroy",
|
|
3248
|
+
"alchemy:dev": "alchemy dev"
|
|
3249
|
+
};
|
|
3250
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3251
|
+
}
|
|
3252
|
+
const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
|
|
3253
|
+
if (!await fs.pathExists(svelteConfigPath)) return;
|
|
3254
|
+
try {
|
|
3255
|
+
const project = new Project({ manipulationSettings: {
|
|
3256
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3257
|
+
quoteKind: QuoteKind.Single
|
|
3258
|
+
} });
|
|
3259
|
+
project.addSourceFileAtPath(svelteConfigPath);
|
|
3260
|
+
const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
|
|
3261
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
3262
|
+
const adapterImport = importDeclarations.find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
|
|
3263
|
+
if (adapterImport) {
|
|
3264
|
+
adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
|
|
3265
|
+
adapterImport.removeDefaultImport();
|
|
3266
|
+
adapterImport.setDefaultImport("alchemy");
|
|
3267
|
+
} else sourceFile.insertImportDeclaration(0, {
|
|
3268
|
+
moduleSpecifier: "alchemy/cloudflare/sveltekit",
|
|
3269
|
+
defaultImport: "alchemy"
|
|
3270
|
+
});
|
|
3271
|
+
const configVariable = sourceFile.getVariableDeclaration("config");
|
|
3272
|
+
if (configVariable) {
|
|
3273
|
+
const initializer = configVariable.getInitializer();
|
|
3274
|
+
if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
|
|
3275
|
+
}
|
|
3276
|
+
await project.save();
|
|
3277
|
+
} catch (error) {
|
|
3278
|
+
console.warn("Failed to update svelte.config.js:", error);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
function updateAdapterInConfig(configObject) {
|
|
3282
|
+
if (!Node.isObjectLiteralExpression(configObject)) return;
|
|
3283
|
+
const kitProperty = configObject.getProperty("kit");
|
|
3284
|
+
if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
|
|
3285
|
+
const kitInitializer = kitProperty.getInitializer();
|
|
3286
|
+
if (Node.isObjectLiteralExpression(kitInitializer)) {
|
|
3287
|
+
const adapterProperty = kitInitializer.getProperty("adapter");
|
|
3288
|
+
if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
|
|
3289
|
+
const initializer = adapterProperty.getInitializer();
|
|
3290
|
+
if (Node.isCallExpression(initializer)) {
|
|
3291
|
+
const expression = initializer.getExpression();
|
|
3292
|
+
if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
//#endregion
|
|
3300
|
+
//#region src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts
|
|
3301
|
+
async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager) {
|
|
3302
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3303
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3304
|
+
await addPackageDependency({
|
|
3305
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3306
|
+
projectDir: webAppDir
|
|
3307
|
+
});
|
|
3308
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3309
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3310
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3311
|
+
pkg.scripts = {
|
|
3312
|
+
...pkg.scripts,
|
|
3313
|
+
deploy: "alchemy deploy",
|
|
3314
|
+
destroy: "alchemy destroy",
|
|
3315
|
+
"alchemy:dev": "alchemy dev"
|
|
3316
|
+
};
|
|
3317
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
//#endregion
|
|
3322
|
+
//#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
|
|
3323
|
+
async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager) {
|
|
3324
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3325
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3326
|
+
await addPackageDependency({
|
|
3327
|
+
devDependencies: [
|
|
3328
|
+
"alchemy",
|
|
3329
|
+
"nitropack",
|
|
3330
|
+
"dotenv"
|
|
3331
|
+
],
|
|
3332
|
+
projectDir: webAppDir
|
|
3333
|
+
});
|
|
3334
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3335
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3336
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3337
|
+
pkg.scripts = {
|
|
3338
|
+
...pkg.scripts,
|
|
3339
|
+
deploy: "alchemy deploy",
|
|
3340
|
+
destroy: "alchemy destroy",
|
|
3341
|
+
"alchemy:dev": "alchemy dev"
|
|
3342
|
+
};
|
|
3343
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3344
|
+
}
|
|
3345
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
3346
|
+
if (await fs.pathExists(viteConfigPath)) try {
|
|
3347
|
+
const project = new Project({ manipulationSettings: {
|
|
3348
|
+
indentationText: IndentationText.TwoSpaces,
|
|
3349
|
+
quoteKind: QuoteKind.Double
|
|
3350
|
+
} });
|
|
3351
|
+
project.addSourceFileAtPath(viteConfigPath);
|
|
3352
|
+
const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
|
|
3353
|
+
const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
|
|
3354
|
+
if (!alchemyImport) sourceFile.addImportDeclaration({
|
|
3355
|
+
moduleSpecifier: "alchemy/cloudflare/tanstack-start",
|
|
3356
|
+
defaultImport: "alchemy"
|
|
3357
|
+
});
|
|
3358
|
+
else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
|
|
3359
|
+
const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
|
|
3360
|
+
if (!exportAssignment) return;
|
|
3361
|
+
const defineConfigCall = exportAssignment.getExpression();
|
|
3362
|
+
if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
|
|
3363
|
+
let configObject = defineConfigCall.getArguments()[0];
|
|
3364
|
+
if (!configObject) configObject = defineConfigCall.addArgument("{}");
|
|
3365
|
+
if (Node.isObjectLiteralExpression(configObject)) {
|
|
3366
|
+
if (!configObject.getProperty("build")) configObject.addPropertyAssignment({
|
|
3367
|
+
name: "build",
|
|
3368
|
+
initializer: `{
|
|
3369
|
+
target: "esnext",
|
|
3370
|
+
rollupOptions: {
|
|
3371
|
+
external: ["node:async_hooks", "cloudflare:workers"],
|
|
3372
|
+
},
|
|
3373
|
+
}`
|
|
3374
|
+
});
|
|
3375
|
+
const pluginsProperty = configObject.getProperty("plugins");
|
|
3376
|
+
if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
|
|
3377
|
+
const initializer = pluginsProperty.getInitializer();
|
|
3378
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
3379
|
+
const hasShim = initializer.getElements().some((el) => el.getText().includes("alchemy"));
|
|
3380
|
+
if (!hasShim) initializer.addElement("alchemy()");
|
|
3381
|
+
const tanstackElements = initializer.getElements().filter((el) => el.getText().includes("tanstackStart"));
|
|
3382
|
+
tanstackElements.forEach((element) => {
|
|
3383
|
+
if (Node.isCallExpression(element)) {
|
|
3384
|
+
const args = element.getArguments();
|
|
3385
|
+
if (args.length === 0) element.addArgument(`{
|
|
3386
|
+
target: "cloudflare-module",
|
|
3387
|
+
customViteReactPlugin: true,
|
|
3388
|
+
}`);
|
|
3389
|
+
else if (args.length === 1 && Node.isObjectLiteralExpression(args[0])) {
|
|
3390
|
+
const configObj = args[0];
|
|
3391
|
+
if (!configObj.getProperty("target")) configObj.addPropertyAssignment({
|
|
3392
|
+
name: "target",
|
|
3393
|
+
initializer: "\"cloudflare-module\""
|
|
3394
|
+
});
|
|
3395
|
+
if (!configObj.getProperty("customViteReactPlugin")) configObj.addPropertyAssignment({
|
|
3396
|
+
name: "customViteReactPlugin",
|
|
3397
|
+
initializer: "true"
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
} else configObject.addPropertyAssignment({
|
|
3404
|
+
name: "plugins",
|
|
3405
|
+
initializer: "[alchemy()]"
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
await project.save();
|
|
3409
|
+
} catch (error) {
|
|
3410
|
+
console.warn("Failed to update vite.config.ts:", error);
|
|
3411
|
+
}
|
|
3412
|
+
const nitroConfigPath = path.join(webAppDir, "nitro.config.ts");
|
|
3413
|
+
const nitroConfigContent = `import { defineNitroConfig } from "nitropack/config";
|
|
3414
|
+
|
|
3415
|
+
export default defineNitroConfig({
|
|
3416
|
+
preset: "cloudflare-module",
|
|
3417
|
+
cloudflare: {
|
|
3418
|
+
nodeCompat: true,
|
|
3419
|
+
},
|
|
3420
|
+
});
|
|
3421
|
+
`;
|
|
3422
|
+
await fs.writeFile(nitroConfigPath, nitroConfigContent, "utf-8");
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
//#endregion
|
|
3426
|
+
//#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
|
|
3427
|
+
async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
|
|
3428
|
+
await addPackageDependency({
|
|
3429
|
+
devDependencies: ["alchemy", "dotenv"],
|
|
3430
|
+
projectDir
|
|
3431
|
+
});
|
|
3432
|
+
const rootPkgPath = path.join(projectDir, "package.json");
|
|
3433
|
+
if (await fs.pathExists(rootPkgPath)) {
|
|
3434
|
+
const pkg = await fs.readJson(rootPkgPath);
|
|
3435
|
+
pkg.scripts = {
|
|
3436
|
+
...pkg.scripts,
|
|
3437
|
+
deploy: "alchemy deploy",
|
|
3438
|
+
destroy: "alchemy destroy",
|
|
3439
|
+
"alchemy:dev": "alchemy dev"
|
|
3440
|
+
};
|
|
3441
|
+
await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
|
|
3442
|
+
}
|
|
3443
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3444
|
+
if (await fs.pathExists(serverDir)) await setupAlchemyServerDeploy(serverDir, packageManager);
|
|
3445
|
+
const frontend = config.frontend;
|
|
3446
|
+
const isNext = frontend.includes("next");
|
|
3447
|
+
const isNuxt = frontend.includes("nuxt");
|
|
3448
|
+
const isSvelte = frontend.includes("svelte");
|
|
3449
|
+
const isTanstackRouter = frontend.includes("tanstack-router");
|
|
3450
|
+
const isTanstackStart = frontend.includes("tanstack-start");
|
|
3451
|
+
const isReactRouter = frontend.includes("react-router");
|
|
3452
|
+
const isSolid = frontend.includes("solid");
|
|
3453
|
+
if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
|
|
3454
|
+
else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
|
3455
|
+
else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
|
3456
|
+
else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
|
3457
|
+
else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
|
3458
|
+
else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
|
3459
|
+
else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
//#endregion
|
|
3463
|
+
//#region src/helpers/deployment/workers/workers-next-setup.ts
|
|
3464
|
+
async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
3465
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3466
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3467
|
+
await addPackageDependency({
|
|
3468
|
+
dependencies: ["@opennextjs/cloudflare"],
|
|
3469
|
+
devDependencies: ["wrangler"],
|
|
3470
|
+
projectDir: webAppDir
|
|
3471
|
+
});
|
|
3472
|
+
const packageJsonPath = path.join(webAppDir, "package.json");
|
|
3473
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
3474
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
3475
|
+
pkg.scripts = {
|
|
3476
|
+
...pkg.scripts,
|
|
3477
|
+
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
3478
|
+
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
3479
|
+
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
|
3480
|
+
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
3481
|
+
};
|
|
3482
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
//#endregion
|
|
3487
|
+
//#region src/helpers/deployment/workers/workers-nuxt-setup.ts
|
|
3488
|
+
async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
3489
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3490
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3491
|
+
await addPackageDependency({
|
|
3492
|
+
devDependencies: ["nitro-cloudflare-dev", "wrangler"],
|
|
3493
|
+
projectDir: webAppDir
|
|
3494
|
+
});
|
|
3495
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
3496
|
+
if (await fs.pathExists(pkgPath)) {
|
|
3497
|
+
const pkg = await fs.readJson(pkgPath);
|
|
3498
|
+
pkg.scripts = {
|
|
3499
|
+
...pkg.scripts,
|
|
3500
|
+
deploy: `${packageManager} run build && wrangler deploy`,
|
|
3501
|
+
"cf-typegen": "wrangler types"
|
|
3502
|
+
};
|
|
3503
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
3504
|
+
}
|
|
3505
|
+
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
3506
|
+
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
3507
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
|
|
3508
|
+
if (!sourceFile) return;
|
|
3509
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
3510
|
+
const expression = expr.getExpression();
|
|
3511
|
+
return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
|
|
3512
|
+
});
|
|
3513
|
+
if (!defineCall) return;
|
|
3514
|
+
const configObj = defineCall.getArguments()[0];
|
|
3515
|
+
if (!configObj) return;
|
|
3516
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3517
|
+
const compatProp = configObj.getProperty("compatibilityDate");
|
|
3518
|
+
if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
|
|
3519
|
+
else configObj.addPropertyAssignment({
|
|
3520
|
+
name: "compatibilityDate",
|
|
3521
|
+
initializer: `'${today}'`
|
|
3522
|
+
});
|
|
3523
|
+
const nitroInitializer = `{
|
|
3524
|
+
preset: "cloudflare_module",
|
|
3525
|
+
cloudflare: {
|
|
3526
|
+
deployConfig: true,
|
|
3527
|
+
nodeCompat: true
|
|
3528
|
+
}
|
|
3529
|
+
}`;
|
|
3530
|
+
const nitroProp = configObj.getProperty("nitro");
|
|
3531
|
+
if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
|
|
3532
|
+
else configObj.addPropertyAssignment({
|
|
3533
|
+
name: "nitro",
|
|
3534
|
+
initializer: nitroInitializer
|
|
3535
|
+
});
|
|
3536
|
+
const modulesProp = configObj.getProperty("modules");
|
|
3537
|
+
if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
3538
|
+
const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
|
|
3539
|
+
if (arrayExpr) {
|
|
3540
|
+
const alreadyHas = arrayExpr.getElements().some((el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev");
|
|
3541
|
+
if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'");
|
|
3542
|
+
}
|
|
3543
|
+
} else configObj.addPropertyAssignment({
|
|
3544
|
+
name: "modules",
|
|
3545
|
+
initializer: "['nitro-cloudflare-dev']"
|
|
3546
|
+
});
|
|
3547
|
+
await tsProject.save();
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
//#endregion
|
|
3551
|
+
//#region src/helpers/deployment/workers/workers-svelte-setup.ts
|
|
3552
|
+
async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
3553
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
3554
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3555
|
+
await addPackageDependency({
|
|
3556
|
+
devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"],
|
|
2724
3557
|
projectDir: webAppDir
|
|
2725
3558
|
});
|
|
2726
3559
|
const pkgPath = path.join(webAppDir, "package.json");
|
|
@@ -2752,7 +3585,7 @@ async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
|
2752
3585
|
}
|
|
2753
3586
|
|
|
2754
3587
|
//#endregion
|
|
2755
|
-
//#region src/helpers/
|
|
3588
|
+
//#region src/helpers/deployment/workers/workers-tanstack-start-setup.ts
|
|
2756
3589
|
async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
2757
3590
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2758
3591
|
if (!await fs.pathExists(webAppDir)) return;
|
|
@@ -2790,7 +3623,7 @@ async function setupTanstackStartWorkersDeploy(projectDir, packageManager) {
|
|
|
2790
3623
|
}
|
|
2791
3624
|
|
|
2792
3625
|
//#endregion
|
|
2793
|
-
//#region src/helpers/
|
|
3626
|
+
//#region src/helpers/deployment/workers/workers-vite-setup.ts
|
|
2794
3627
|
async function setupWorkersVitePlugin(projectDir) {
|
|
2795
3628
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2796
3629
|
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
@@ -2821,12 +3654,16 @@ async function setupWorkersVitePlugin(projectDir) {
|
|
|
2821
3654
|
}
|
|
2822
3655
|
|
|
2823
3656
|
//#endregion
|
|
2824
|
-
//#region src/helpers/
|
|
3657
|
+
//#region src/helpers/deployment/web-deploy-setup.ts
|
|
2825
3658
|
async function setupWebDeploy(config) {
|
|
2826
|
-
const { webDeploy, frontend, projectDir } = config;
|
|
3659
|
+
const { webDeploy, serverDeploy, frontend, projectDir } = config;
|
|
2827
3660
|
const { packageManager } = config;
|
|
2828
3661
|
if (webDeploy === "none") return;
|
|
2829
|
-
if (webDeploy !== "
|
|
3662
|
+
if (webDeploy !== "wrangler" && webDeploy !== "alchemy") return;
|
|
3663
|
+
if (webDeploy === "alchemy" && serverDeploy === "alchemy") {
|
|
3664
|
+
await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
2830
3667
|
const isNext = frontend.includes("next");
|
|
2831
3668
|
const isNuxt = frontend.includes("nuxt");
|
|
2832
3669
|
const isSvelte = frontend.includes("svelte");
|
|
@@ -2834,11 +3671,21 @@ async function setupWebDeploy(config) {
|
|
|
2834
3671
|
const isTanstackStart = frontend.includes("tanstack-start");
|
|
2835
3672
|
const isReactRouter = frontend.includes("react-router");
|
|
2836
3673
|
const isSolid = frontend.includes("solid");
|
|
2837
|
-
if (
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
3674
|
+
if (webDeploy === "wrangler") {
|
|
3675
|
+
if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
|
|
3676
|
+
else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
|
|
3677
|
+
else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
|
|
3678
|
+
else if (isTanstackStart) await setupTanstackStartWorkersDeploy(projectDir, packageManager);
|
|
3679
|
+
else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
|
|
3680
|
+
} else if (webDeploy === "alchemy") {
|
|
3681
|
+
if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
|
|
3682
|
+
else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
|
|
3683
|
+
else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
|
|
3684
|
+
else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
|
|
3685
|
+
else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
|
|
3686
|
+
else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
|
|
3687
|
+
else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
|
|
3688
|
+
}
|
|
2842
3689
|
}
|
|
2843
3690
|
async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
2844
3691
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
@@ -2855,30 +3702,9 @@ async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
|
2855
3702
|
}
|
|
2856
3703
|
await setupWorkersVitePlugin(projectDir);
|
|
2857
3704
|
}
|
|
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
3705
|
|
|
2880
3706
|
//#endregion
|
|
2881
|
-
//#region src/helpers/
|
|
3707
|
+
//#region src/helpers/core/add-deployment.ts
|
|
2882
3708
|
async function addDeploymentToProject(input) {
|
|
2883
3709
|
try {
|
|
2884
3710
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -2886,7 +3712,8 @@ async function addDeploymentToProject(input) {
|
|
|
2886
3712
|
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
3713
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
2888
3714
|
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.`);
|
|
3715
|
+
if (input.webDeploy && detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} web deployment is already configured for this project.`);
|
|
3716
|
+
if (input.serverDeploy && detectedConfig.serverDeploy === input.serverDeploy) exitWithError(`${input.serverDeploy} server deployment is already configured for this project.`);
|
|
2890
3717
|
const config = {
|
|
2891
3718
|
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
2892
3719
|
projectDir,
|
|
@@ -2903,224 +3730,79 @@ async function addDeploymentToProject(input) {
|
|
|
2903
3730
|
packageManager: input.packageManager || detectedConfig.packageManager || "npm",
|
|
2904
3731
|
install: input.install || false,
|
|
2905
3732
|
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) {}
|
|
3733
|
+
api: detectedConfig.api || "none",
|
|
3734
|
+
webDeploy: input.webDeploy || detectedConfig.webDeploy || "none",
|
|
3735
|
+
serverDeploy: input.serverDeploy || detectedConfig.serverDeploy || "none"
|
|
3110
3736
|
};
|
|
3111
|
-
if (
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3737
|
+
if (input.webDeploy && input.webDeploy !== "none") log.info(pc.green(`Adding ${input.webDeploy} web deployment to ${config.frontend.join("/")}`));
|
|
3738
|
+
if (input.serverDeploy && input.serverDeploy !== "none") log.info(pc.green(`Adding ${input.serverDeploy} server deployment`));
|
|
3739
|
+
await setupDeploymentTemplates(projectDir, config);
|
|
3740
|
+
await setupWebDeploy(config);
|
|
3741
|
+
await setupServerDeploy(config);
|
|
3742
|
+
await updateBtsConfig(projectDir, {
|
|
3743
|
+
webDeploy: input.webDeploy || config.webDeploy,
|
|
3744
|
+
serverDeploy: input.serverDeploy || config.serverDeploy
|
|
3745
|
+
});
|
|
3746
|
+
if (config.install) await installDependencies({
|
|
3747
|
+
projectDir,
|
|
3748
|
+
packageManager: config.packageManager
|
|
3749
|
+
});
|
|
3750
|
+
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
3751
|
+
} catch (error) {
|
|
3752
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3753
|
+
exitWithError(`Error adding deployment: ${message}`);
|
|
3119
3754
|
}
|
|
3120
3755
|
}
|
|
3121
3756
|
|
|
3122
3757
|
//#endregion
|
|
3123
|
-
//#region src/
|
|
3758
|
+
//#region src/utils/format-with-biome.ts
|
|
3759
|
+
async function formatProjectWithBiome(projectDir) {
|
|
3760
|
+
const biome = new Biome();
|
|
3761
|
+
const { projectKey } = biome.openProject(projectDir);
|
|
3762
|
+
biome.applyConfiguration(projectKey, {
|
|
3763
|
+
formatter: {
|
|
3764
|
+
enabled: true,
|
|
3765
|
+
indentStyle: "tab"
|
|
3766
|
+
},
|
|
3767
|
+
javascript: { formatter: { quoteStyle: "double" } }
|
|
3768
|
+
});
|
|
3769
|
+
const files = await glob("**/*", {
|
|
3770
|
+
cwd: projectDir,
|
|
3771
|
+
dot: true,
|
|
3772
|
+
absolute: true,
|
|
3773
|
+
onlyFiles: true
|
|
3774
|
+
});
|
|
3775
|
+
for (const filePath of files) try {
|
|
3776
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
3777
|
+
const supported = new Set([
|
|
3778
|
+
".ts",
|
|
3779
|
+
".tsx",
|
|
3780
|
+
".js",
|
|
3781
|
+
".jsx",
|
|
3782
|
+
".cjs",
|
|
3783
|
+
".mjs",
|
|
3784
|
+
".cts",
|
|
3785
|
+
".mts",
|
|
3786
|
+
".json",
|
|
3787
|
+
".jsonc",
|
|
3788
|
+
".md",
|
|
3789
|
+
".mdx",
|
|
3790
|
+
".css",
|
|
3791
|
+
".scss",
|
|
3792
|
+
".html"
|
|
3793
|
+
]);
|
|
3794
|
+
if (!supported.has(ext)) continue;
|
|
3795
|
+
const original = await fs.readFile(filePath, "utf8");
|
|
3796
|
+
const result = biome.formatContent(projectKey, original, { filePath });
|
|
3797
|
+
const content = result?.content;
|
|
3798
|
+
if (typeof content !== "string") continue;
|
|
3799
|
+
if (content.length === 0 && original.length > 0) continue;
|
|
3800
|
+
if (content !== original) await fs.writeFile(filePath, content);
|
|
3801
|
+
} catch {}
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
//#endregion
|
|
3805
|
+
//#region src/helpers/addons/auth-setup.ts
|
|
3124
3806
|
async function setupAuth(config) {
|
|
3125
3807
|
const { auth, frontend, backend, projectDir } = config;
|
|
3126
3808
|
if (backend === "convex" || !auth) return;
|
|
@@ -3172,7 +3854,217 @@ function generateAuthSecret(length = 32) {
|
|
|
3172
3854
|
}
|
|
3173
3855
|
|
|
3174
3856
|
//#endregion
|
|
3175
|
-
//#region src/helpers/
|
|
3857
|
+
//#region src/helpers/addons/examples-setup.ts
|
|
3858
|
+
async function setupExamples(config) {
|
|
3859
|
+
const { examples, frontend, backend, projectDir } = config;
|
|
3860
|
+
if (backend === "convex" || !examples || examples.length === 0 || examples[0] === "none") return;
|
|
3861
|
+
if (examples.includes("ai")) {
|
|
3862
|
+
const webClientDir = path.join(projectDir, "apps/web");
|
|
3863
|
+
const nativeClientDir = path.join(projectDir, "apps/native");
|
|
3864
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
3865
|
+
const webClientDirExists = await fs.pathExists(webClientDir);
|
|
3866
|
+
const nativeClientDirExists = await fs.pathExists(nativeClientDir);
|
|
3867
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
3868
|
+
const hasNuxt = frontend.includes("nuxt");
|
|
3869
|
+
const hasSvelte = frontend.includes("svelte");
|
|
3870
|
+
const hasReactWeb = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || frontend.includes("tanstack-start");
|
|
3871
|
+
const hasReactNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3872
|
+
if (webClientDirExists) {
|
|
3873
|
+
const dependencies = ["ai"];
|
|
3874
|
+
if (hasNuxt) dependencies.push("@ai-sdk/vue");
|
|
3875
|
+
else if (hasSvelte) dependencies.push("@ai-sdk/svelte");
|
|
3876
|
+
else if (hasReactWeb) dependencies.push("@ai-sdk/react");
|
|
3877
|
+
await addPackageDependency({
|
|
3878
|
+
dependencies,
|
|
3879
|
+
projectDir: webClientDir
|
|
3880
|
+
});
|
|
3881
|
+
}
|
|
3882
|
+
if (nativeClientDirExists && hasReactNative) await addPackageDependency({
|
|
3883
|
+
dependencies: ["ai", "@ai-sdk/react"],
|
|
3884
|
+
projectDir: nativeClientDir
|
|
3885
|
+
});
|
|
3886
|
+
if (serverDirExists && backend !== "none") await addPackageDependency({
|
|
3887
|
+
dependencies: ["ai", "@ai-sdk/google"],
|
|
3888
|
+
projectDir: serverDir
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
//#endregion
|
|
3894
|
+
//#region src/helpers/core/api-setup.ts
|
|
3895
|
+
async function addBackendWorkspaceDependency(projectDir, backendPackageName, workspaceVersion) {
|
|
3896
|
+
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
3897
|
+
try {
|
|
3898
|
+
const pkgJson = await fs.readJson(pkgJsonPath);
|
|
3899
|
+
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
|
3900
|
+
pkgJson.dependencies[backendPackageName] = workspaceVersion;
|
|
3901
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
3902
|
+
} catch (_error) {}
|
|
3903
|
+
}
|
|
3904
|
+
function getFrontendType(frontend) {
|
|
3905
|
+
const reactBasedFrontends = [
|
|
3906
|
+
"tanstack-router",
|
|
3907
|
+
"react-router",
|
|
3908
|
+
"tanstack-start",
|
|
3909
|
+
"next"
|
|
3910
|
+
];
|
|
3911
|
+
const nativeFrontends = ["native-nativewind", "native-unistyles"];
|
|
3912
|
+
return {
|
|
3913
|
+
hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)),
|
|
3914
|
+
hasNuxtWeb: frontend.includes("nuxt"),
|
|
3915
|
+
hasSvelteWeb: frontend.includes("svelte"),
|
|
3916
|
+
hasSolidWeb: frontend.includes("solid"),
|
|
3917
|
+
hasNative: frontend.some((f) => nativeFrontends.includes(f))
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
function getApiDependencies(api, frontendType) {
|
|
3921
|
+
const deps = {};
|
|
3922
|
+
if (api === "orpc") deps.server = { dependencies: ["@orpc/server", "@orpc/client"] };
|
|
3923
|
+
else if (api === "trpc") deps.server = { dependencies: ["@trpc/server", "@trpc/client"] };
|
|
3924
|
+
if (frontendType.hasReactWeb) {
|
|
3925
|
+
if (api === "orpc") deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
|
3926
|
+
else if (api === "trpc") deps.web = { dependencies: [
|
|
3927
|
+
"@trpc/tanstack-react-query",
|
|
3928
|
+
"@trpc/client",
|
|
3929
|
+
"@trpc/server"
|
|
3930
|
+
] };
|
|
3931
|
+
} else if (frontendType.hasNuxtWeb && api === "orpc") deps.web = {
|
|
3932
|
+
dependencies: [
|
|
3933
|
+
"@tanstack/vue-query",
|
|
3934
|
+
"@orpc/tanstack-query",
|
|
3935
|
+
"@orpc/client"
|
|
3936
|
+
],
|
|
3937
|
+
devDependencies: ["@tanstack/vue-query-devtools"]
|
|
3938
|
+
};
|
|
3939
|
+
else if (frontendType.hasSvelteWeb && api === "orpc") deps.web = {
|
|
3940
|
+
dependencies: [
|
|
3941
|
+
"@orpc/tanstack-query",
|
|
3942
|
+
"@orpc/client",
|
|
3943
|
+
"@tanstack/svelte-query"
|
|
3944
|
+
],
|
|
3945
|
+
devDependencies: ["@tanstack/svelte-query-devtools"]
|
|
3946
|
+
};
|
|
3947
|
+
else if (frontendType.hasSolidWeb && api === "orpc") deps.web = {
|
|
3948
|
+
dependencies: [
|
|
3949
|
+
"@orpc/tanstack-query",
|
|
3950
|
+
"@orpc/client",
|
|
3951
|
+
"@tanstack/solid-query"
|
|
3952
|
+
],
|
|
3953
|
+
devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
|
|
3954
|
+
};
|
|
3955
|
+
if (api === "trpc") deps.native = { dependencies: [
|
|
3956
|
+
"@trpc/tanstack-react-query",
|
|
3957
|
+
"@trpc/client",
|
|
3958
|
+
"@trpc/server"
|
|
3959
|
+
] };
|
|
3960
|
+
else if (api === "orpc") deps.native = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] };
|
|
3961
|
+
return deps;
|
|
3962
|
+
}
|
|
3963
|
+
function getQueryDependencies(frontend) {
|
|
3964
|
+
const reactBasedFrontends = [
|
|
3965
|
+
"react-router",
|
|
3966
|
+
"tanstack-router",
|
|
3967
|
+
"tanstack-start",
|
|
3968
|
+
"next",
|
|
3969
|
+
"native-nativewind",
|
|
3970
|
+
"native-unistyles"
|
|
3971
|
+
];
|
|
3972
|
+
const deps = {};
|
|
3973
|
+
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
|
|
3974
|
+
if (needsReactQuery) {
|
|
3975
|
+
const hasReactWeb = frontend.some((f) => f !== "native-nativewind" && f !== "native-unistyles" && reactBasedFrontends.includes(f));
|
|
3976
|
+
const hasNative = frontend.includes("native-nativewind") || frontend.includes("native-unistyles");
|
|
3977
|
+
if (hasReactWeb) deps.web = {
|
|
3978
|
+
dependencies: ["@tanstack/react-query"],
|
|
3979
|
+
devDependencies: ["@tanstack/react-query-devtools"]
|
|
3980
|
+
};
|
|
3981
|
+
if (hasNative) deps.native = { dependencies: ["@tanstack/react-query"] };
|
|
3982
|
+
}
|
|
3983
|
+
if (frontend.includes("solid")) deps.web = {
|
|
3984
|
+
dependencies: ["@tanstack/solid-query"],
|
|
3985
|
+
devDependencies: ["@tanstack/solid-query-devtools", "@tanstack/solid-router-devtools"]
|
|
3986
|
+
};
|
|
3987
|
+
return deps;
|
|
3988
|
+
}
|
|
3989
|
+
function getConvexDependencies(frontend) {
|
|
3990
|
+
const deps = {
|
|
3991
|
+
web: { dependencies: ["convex"] },
|
|
3992
|
+
native: { dependencies: ["convex"] }
|
|
3993
|
+
};
|
|
3994
|
+
if (frontend.includes("tanstack-start")) deps.web.dependencies.push("@convex-dev/react-query");
|
|
3995
|
+
if (frontend.includes("svelte")) deps.web.dependencies.push("convex-svelte");
|
|
3996
|
+
if (frontend.includes("nuxt")) deps.web.dependencies.push("convex-nuxt", "convex-vue");
|
|
3997
|
+
return deps;
|
|
3998
|
+
}
|
|
3999
|
+
async function setupApi(config) {
|
|
4000
|
+
const { api, projectName, frontend, backend, packageManager, projectDir } = config;
|
|
4001
|
+
const isConvex = backend === "convex";
|
|
4002
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
4003
|
+
const nativeDir = path.join(projectDir, "apps/native");
|
|
4004
|
+
const serverDir = path.join(projectDir, "apps/server");
|
|
4005
|
+
const webDirExists = await fs.pathExists(webDir);
|
|
4006
|
+
const nativeDirExists = await fs.pathExists(nativeDir);
|
|
4007
|
+
const serverDirExists = await fs.pathExists(serverDir);
|
|
4008
|
+
const frontendType = getFrontendType(frontend);
|
|
4009
|
+
if (!isConvex && api !== "none") {
|
|
4010
|
+
const apiDeps = getApiDependencies(api, frontendType);
|
|
4011
|
+
if (serverDirExists && apiDeps.server) {
|
|
4012
|
+
await addPackageDependency({
|
|
4013
|
+
dependencies: apiDeps.server.dependencies,
|
|
4014
|
+
projectDir: serverDir
|
|
4015
|
+
});
|
|
4016
|
+
if (api === "trpc") {
|
|
4017
|
+
if (backend === "hono") await addPackageDependency({
|
|
4018
|
+
dependencies: ["@hono/trpc-server"],
|
|
4019
|
+
projectDir: serverDir
|
|
4020
|
+
});
|
|
4021
|
+
else if (backend === "elysia") await addPackageDependency({
|
|
4022
|
+
dependencies: ["@elysiajs/trpc"],
|
|
4023
|
+
projectDir: serverDir
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
if (webDirExists && apiDeps.web) await addPackageDependency({
|
|
4028
|
+
dependencies: apiDeps.web.dependencies,
|
|
4029
|
+
devDependencies: apiDeps.web.devDependencies,
|
|
4030
|
+
projectDir: webDir
|
|
4031
|
+
});
|
|
4032
|
+
if (nativeDirExists && apiDeps.native) await addPackageDependency({
|
|
4033
|
+
dependencies: apiDeps.native.dependencies,
|
|
4034
|
+
projectDir: nativeDir
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
4037
|
+
if (!isConvex) {
|
|
4038
|
+
const queryDeps = getQueryDependencies(frontend);
|
|
4039
|
+
if (webDirExists && queryDeps.web) await addPackageDependency({
|
|
4040
|
+
dependencies: queryDeps.web.dependencies,
|
|
4041
|
+
devDependencies: queryDeps.web.devDependencies,
|
|
4042
|
+
projectDir: webDir
|
|
4043
|
+
});
|
|
4044
|
+
if (nativeDirExists && queryDeps.native) await addPackageDependency({
|
|
4045
|
+
dependencies: queryDeps.native.dependencies,
|
|
4046
|
+
projectDir: nativeDir
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
if (isConvex) {
|
|
4050
|
+
const convexDeps = getConvexDependencies(frontend);
|
|
4051
|
+
if (webDirExists) await addPackageDependency({
|
|
4052
|
+
dependencies: convexDeps.web.dependencies,
|
|
4053
|
+
projectDir: webDir
|
|
4054
|
+
});
|
|
4055
|
+
if (nativeDirExists) await addPackageDependency({
|
|
4056
|
+
dependencies: convexDeps.native.dependencies,
|
|
4057
|
+
projectDir: nativeDir
|
|
4058
|
+
});
|
|
4059
|
+
const backendPackageName = `@${projectName}/backend`;
|
|
4060
|
+
const backendWorkspaceVersion = packageManager === "npm" ? "*" : "workspace:*";
|
|
4061
|
+
if (webDirExists) await addBackendWorkspaceDependency(webDir, backendPackageName, backendWorkspaceVersion);
|
|
4062
|
+
if (nativeDirExists) await addBackendWorkspaceDependency(nativeDir, backendPackageName, backendWorkspaceVersion);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
//#endregion
|
|
4067
|
+
//#region src/helpers/core/backend-setup.ts
|
|
3176
4068
|
async function setupBackendDependencies(config) {
|
|
3177
4069
|
const { backend, runtime, api, projectDir } = config;
|
|
3178
4070
|
if (backend === "convex") return;
|
|
@@ -3211,7 +4103,7 @@ async function setupBackendDependencies(config) {
|
|
|
3211
4103
|
}
|
|
3212
4104
|
|
|
3213
4105
|
//#endregion
|
|
3214
|
-
//#region src/helpers/
|
|
4106
|
+
//#region src/helpers/core/env-setup.ts
|
|
3215
4107
|
async function addEnvVariablesToFile(filePath, variables) {
|
|
3216
4108
|
await fs.ensureDir(path.dirname(filePath));
|
|
3217
4109
|
let envContent = "";
|
|
@@ -3259,7 +4151,7 @@ async function addEnvVariablesToFile(filePath, variables) {
|
|
|
3259
4151
|
if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
|
|
3260
4152
|
}
|
|
3261
4153
|
async function setupEnvironmentVariables(config) {
|
|
3262
|
-
const { backend, frontend, database, auth, examples, dbSetup, projectDir } = config;
|
|
4154
|
+
const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
|
|
3263
4155
|
const hasReactRouter = frontend.includes("react-router");
|
|
3264
4156
|
const hasTanStackRouter = frontend.includes("tanstack-router");
|
|
3265
4157
|
const hasTanStackStart = frontend.includes("tanstack-start");
|
|
@@ -3359,39 +4251,69 @@ async function setupEnvironmentVariables(config) {
|
|
|
3359
4251
|
}
|
|
3360
4252
|
];
|
|
3361
4253
|
await addEnvVariablesToFile(envPath, serverVars);
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
4254
|
+
const isUnifiedAlchemy = webDeploy === "alchemy" && serverDeploy === "alchemy";
|
|
4255
|
+
const isIndividualAlchemy = webDeploy === "alchemy" || serverDeploy === "alchemy";
|
|
4256
|
+
if (isUnifiedAlchemy) {
|
|
4257
|
+
const rootEnvPath = path.join(projectDir, ".env");
|
|
4258
|
+
const rootAlchemyVars = [{
|
|
4259
|
+
key: "ALCHEMY_PASSWORD",
|
|
4260
|
+
value: "please-change-this",
|
|
4261
|
+
condition: true
|
|
4262
|
+
}];
|
|
4263
|
+
await addEnvVariablesToFile(rootEnvPath, rootAlchemyVars);
|
|
4264
|
+
} else if (isIndividualAlchemy) {
|
|
4265
|
+
if (webDeploy === "alchemy") {
|
|
4266
|
+
const webDir = path.join(projectDir, "apps/web");
|
|
4267
|
+
if (await fs.pathExists(webDir)) {
|
|
4268
|
+
const webAlchemyVars = [{
|
|
4269
|
+
key: "ALCHEMY_PASSWORD",
|
|
4270
|
+
value: "please-change-this",
|
|
4271
|
+
condition: true
|
|
4272
|
+
}];
|
|
4273
|
+
await addEnvVariablesToFile(path.join(webDir, ".env"), webAlchemyVars);
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
if (serverDeploy === "alchemy") {
|
|
4277
|
+
const serverDir$1 = path.join(projectDir, "apps/server");
|
|
4278
|
+
if (await fs.pathExists(serverDir$1)) {
|
|
4279
|
+
const serverAlchemyVars = [{
|
|
4280
|
+
key: "ALCHEMY_PASSWORD",
|
|
4281
|
+
value: "please-change-this",
|
|
4282
|
+
condition: true
|
|
4283
|
+
}];
|
|
4284
|
+
await addEnvVariablesToFile(path.join(serverDir$1, ".env"), serverAlchemyVars);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
3367
4287
|
}
|
|
3368
4288
|
}
|
|
3369
4289
|
|
|
3370
4290
|
//#endregion
|
|
3371
4291
|
//#region src/helpers/database-providers/d1-setup.ts
|
|
3372
4292
|
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
|
-
|
|
4293
|
+
const { projectDir, serverDeploy } = config;
|
|
4294
|
+
if (serverDeploy === "wrangler") {
|
|
4295
|
+
const envPath = path.join(projectDir, "apps/server", ".env");
|
|
4296
|
+
const variables = [
|
|
4297
|
+
{
|
|
4298
|
+
key: "CLOUDFLARE_ACCOUNT_ID",
|
|
4299
|
+
value: "",
|
|
4300
|
+
condition: true
|
|
4301
|
+
},
|
|
4302
|
+
{
|
|
4303
|
+
key: "CLOUDFLARE_DATABASE_ID",
|
|
4304
|
+
value: "",
|
|
4305
|
+
condition: true
|
|
4306
|
+
},
|
|
4307
|
+
{
|
|
4308
|
+
key: "CLOUDFLARE_D1_TOKEN",
|
|
4309
|
+
value: "",
|
|
4310
|
+
condition: true
|
|
4311
|
+
}
|
|
4312
|
+
];
|
|
4313
|
+
try {
|
|
4314
|
+
await addEnvVariablesToFile(envPath, variables);
|
|
4315
|
+
} catch (_err) {}
|
|
4316
|
+
}
|
|
3395
4317
|
}
|
|
3396
4318
|
|
|
3397
4319
|
//#endregion
|
|
@@ -3839,8 +4761,10 @@ async function setupPrismaPostgres(config) {
|
|
|
3839
4761
|
else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
|
|
3840
4762
|
if (prismaConfig) {
|
|
3841
4763
|
await writeEnvFile$1(projectDir, prismaConfig);
|
|
3842
|
-
|
|
3843
|
-
|
|
4764
|
+
if (orm === "prisma") {
|
|
4765
|
+
await addDotenvImportToPrismaConfig(projectDir);
|
|
4766
|
+
await addPrismaAccelerateExtension(serverDir);
|
|
4767
|
+
}
|
|
3844
4768
|
log.success(pc.green("Prisma Postgres database configured successfully!"));
|
|
3845
4769
|
} else {
|
|
3846
4770
|
await writeEnvFile$1(projectDir);
|
|
@@ -4185,7 +5109,7 @@ async function setupTurso(config) {
|
|
|
4185
5109
|
}
|
|
4186
5110
|
|
|
4187
5111
|
//#endregion
|
|
4188
|
-
//#region src/helpers/
|
|
5112
|
+
//#region src/helpers/core/db-setup.ts
|
|
4189
5113
|
async function setupDatabase(config) {
|
|
4190
5114
|
const { database, orm, dbSetup, backend, projectDir } = config;
|
|
4191
5115
|
if (backend === "convex" || database === "none") {
|
|
@@ -4250,44 +5174,7 @@ async function setupDatabase(config) {
|
|
|
4250
5174
|
}
|
|
4251
5175
|
|
|
4252
5176
|
//#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
|
|
5177
|
+
//#region src/helpers/core/runtime-setup.ts
|
|
4291
5178
|
async function setupRuntime(config) {
|
|
4292
5179
|
const { runtime, backend, projectDir } = config;
|
|
4293
5180
|
if (backend === "convex" || backend === "next" || runtime === "none") return;
|
|
@@ -4295,23 +5182,6 @@ async function setupRuntime(config) {
|
|
|
4295
5182
|
if (!await fs.pathExists(serverDir)) return;
|
|
4296
5183
|
if (runtime === "bun") await setupBunRuntime(serverDir, backend);
|
|
4297
5184
|
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
5185
|
}
|
|
4316
5186
|
async function setupBunRuntime(serverDir, _backend) {
|
|
4317
5187
|
const packageJsonPath = path.join(serverDir, "package.json");
|
|
@@ -4351,27 +5221,20 @@ async function setupNodeRuntime(serverDir, backend) {
|
|
|
4351
5221
|
projectDir: serverDir
|
|
4352
5222
|
});
|
|
4353
5223
|
}
|
|
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
|
|
5224
|
+
|
|
5225
|
+
//#endregion
|
|
5226
|
+
//#region src/helpers/core/convex-codegen.ts
|
|
5227
|
+
async function runConvexCodegen(projectDir, packageManager) {
|
|
5228
|
+
const backendDir = path.join(projectDir, "packages/backend");
|
|
5229
|
+
const cmd = getPackageExecutionCommand(packageManager, "convex codegen");
|
|
5230
|
+
await execa(cmd, {
|
|
5231
|
+
cwd: backendDir,
|
|
5232
|
+
shell: true
|
|
4370
5233
|
});
|
|
4371
5234
|
}
|
|
4372
5235
|
|
|
4373
5236
|
//#endregion
|
|
4374
|
-
//#region src/helpers/
|
|
5237
|
+
//#region src/helpers/core/create-readme.ts
|
|
4375
5238
|
async function createReadme(projectDir, options) {
|
|
4376
5239
|
const readmePath = path.join(projectDir, "README.md");
|
|
4377
5240
|
const content = generateReadmeContent(options);
|
|
@@ -4648,7 +5511,7 @@ function generateScriptsList(packageManagerRunCmd, database, orm, _auth, hasNati
|
|
|
4648
5511
|
}
|
|
4649
5512
|
|
|
4650
5513
|
//#endregion
|
|
4651
|
-
//#region src/helpers/
|
|
5514
|
+
//#region src/helpers/core/git.ts
|
|
4652
5515
|
async function initializeGit(projectDir, useGit) {
|
|
4653
5516
|
if (!useGit) return;
|
|
4654
5517
|
const gitVersionResult = await $({
|
|
@@ -4724,20 +5587,21 @@ async function getDockerStatus(database) {
|
|
|
4724
5587
|
}
|
|
4725
5588
|
|
|
4726
5589
|
//#endregion
|
|
4727
|
-
//#region src/helpers/
|
|
5590
|
+
//#region src/helpers/core/post-installation.ts
|
|
4728
5591
|
async function displayPostInstallInstructions(config) {
|
|
4729
|
-
const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy } = config;
|
|
5592
|
+
const { database, relativePath, packageManager, depsInstalled, orm, addons, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy } = config;
|
|
4730
5593
|
const isConvex = backend === "convex";
|
|
4731
5594
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
4732
5595
|
const cdCmd = `cd ${relativePath}`;
|
|
4733
5596
|
const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
|
|
4734
|
-
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
|
|
5597
|
+
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
|
|
4735
5598
|
const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
|
|
4736
5599
|
const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
|
|
4737
5600
|
const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
|
|
4738
5601
|
const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
|
|
4739
5602
|
const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
|
|
4740
|
-
const
|
|
5603
|
+
const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
5604
|
+
const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
4741
5605
|
const hasWeb = frontend?.some((f) => [
|
|
4742
5606
|
"tanstack-router",
|
|
4743
5607
|
"react-router",
|
|
@@ -4766,8 +5630,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
4766
5630
|
if (runtime === "workers") {
|
|
4767
5631
|
if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
|
|
4768
5632
|
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
4769
|
-
output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd}
|
|
4770
|
-
}
|
|
5633
|
+
if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
|
|
5634
|
+
}
|
|
4771
5635
|
}
|
|
4772
5636
|
output += `${pc.bold("Your project will be available at:")}\n`;
|
|
4773
5637
|
if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
|
|
@@ -4780,7 +5644,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
4780
5644
|
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
|
4781
5645
|
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
|
|
4782
5646
|
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
|
|
4783
|
-
if (
|
|
5647
|
+
if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
|
|
5648
|
+
if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
|
|
4784
5649
|
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
|
4785
5650
|
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
|
4786
5651
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
|
@@ -4801,7 +5666,7 @@ function getNativeInstructions(isConvex) {
|
|
|
4801
5666
|
function getLintingInstructions(runCmd) {
|
|
4802
5667
|
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
|
|
4803
5668
|
}
|
|
4804
|
-
async function getDatabaseInstructions(database, orm, runCmd,
|
|
5669
|
+
async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
|
|
4805
5670
|
const instructions = [];
|
|
4806
5671
|
if (dbSetup === "docker") {
|
|
4807
5672
|
const dockerStatus = await getDockerStatus(database);
|
|
@@ -4810,7 +5675,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4810
5675
|
instructions.push("");
|
|
4811
5676
|
}
|
|
4812
5677
|
}
|
|
4813
|
-
if (
|
|
5678
|
+
if (serverDeploy === "wrangler" && dbSetup === "d1") {
|
|
4814
5679
|
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
|
|
4815
5680
|
instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
|
|
4816
5681
|
instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
|
|
@@ -4818,8 +5683,8 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4818
5683
|
instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white(`cd apps/server && ${packageManager} db:generate`)}`);
|
|
4819
5684
|
instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
|
|
4820
5685
|
instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
|
|
4821
|
-
instructions.push("");
|
|
4822
5686
|
}
|
|
5687
|
+
if (dbSetup === "d1" && serverDeploy === "alchemy") instructions.push(`${pc.cyan("•")} Generate migrations: ${pc.white(`${runCmd} db:generate`)}`);
|
|
4823
5688
|
if (orm === "prisma") {
|
|
4824
5689
|
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
5690
|
if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
|
|
@@ -4828,7 +5693,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
4828
5693
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
|
4829
5694
|
} else if (orm === "drizzle") {
|
|
4830
5695
|
if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
|
|
4831
|
-
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
5696
|
+
if (dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
4832
5697
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
|
4833
5698
|
if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
|
|
4834
5699
|
} else if (orm === "mongoose") {
|
|
@@ -4851,12 +5716,22 @@ function getNoOrmWarning() {
|
|
|
4851
5716
|
function getBunWebNativeWarning() {
|
|
4852
5717
|
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
|
|
4853
5718
|
}
|
|
4854
|
-
function
|
|
4855
|
-
|
|
5719
|
+
function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5720
|
+
const instructions = [];
|
|
5721
|
+
if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
|
|
5722
|
+
if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
|
|
5723
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
5724
|
+
}
|
|
5725
|
+
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5726
|
+
const instructions = [];
|
|
5727
|
+
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
|
|
5728
|
+
else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
|
|
5729
|
+
else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
|
|
5730
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
4856
5731
|
}
|
|
4857
5732
|
|
|
4858
5733
|
//#endregion
|
|
4859
|
-
//#region src/helpers/
|
|
5734
|
+
//#region src/helpers/core/project-config.ts
|
|
4860
5735
|
async function updatePackageConfigurations(projectDir, options) {
|
|
4861
5736
|
await updateRootPackageJson(projectDir, options);
|
|
4862
5737
|
if (options.backend !== "convex") await updateServerPackageJson(projectDir, options);
|
|
@@ -5037,7 +5912,7 @@ async function updateConvexPackageJson(projectDir, options) {
|
|
|
5037
5912
|
}
|
|
5038
5913
|
|
|
5039
5914
|
//#endregion
|
|
5040
|
-
//#region src/helpers/
|
|
5915
|
+
//#region src/helpers/core/create-project.ts
|
|
5041
5916
|
async function createProject(options) {
|
|
5042
5917
|
const projectDir = options.projectDir;
|
|
5043
5918
|
const isConvex = options.backend === "convex";
|
|
@@ -5065,18 +5940,18 @@ async function createProject(options) {
|
|
|
5065
5940
|
if (!isConvex && options.auth) await setupAuth(options);
|
|
5066
5941
|
await handleExtras(projectDir, options);
|
|
5067
5942
|
await setupWebDeploy(options);
|
|
5943
|
+
await setupServerDeploy(options);
|
|
5068
5944
|
await setupEnvironmentVariables(options);
|
|
5069
5945
|
await updatePackageConfigurations(projectDir, options);
|
|
5070
5946
|
await createReadme(projectDir, options);
|
|
5071
5947
|
await writeBtsConfig(options);
|
|
5948
|
+
await formatProjectWithBiome(projectDir);
|
|
5949
|
+
if (isConvex) await runConvexCodegen(projectDir, options.packageManager);
|
|
5072
5950
|
log.success("Project template successfully scaffolded!");
|
|
5073
|
-
if (options.install) {
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
});
|
|
5078
|
-
await generateCloudflareWorkerTypes(options);
|
|
5079
|
-
}
|
|
5951
|
+
if (options.install) await installDependencies({
|
|
5952
|
+
projectDir,
|
|
5953
|
+
packageManager: options.packageManager
|
|
5954
|
+
});
|
|
5080
5955
|
await initializeGit(projectDir, options.git);
|
|
5081
5956
|
await displayPostInstallInstructions({
|
|
5082
5957
|
...options,
|
|
@@ -5095,7 +5970,7 @@ async function createProject(options) {
|
|
|
5095
5970
|
}
|
|
5096
5971
|
|
|
5097
5972
|
//#endregion
|
|
5098
|
-
//#region src/helpers/
|
|
5973
|
+
//#region src/helpers/core/command-handlers.ts
|
|
5099
5974
|
async function createProjectHandler(input) {
|
|
5100
5975
|
const startTime = Date.now();
|
|
5101
5976
|
const timeScaffolded = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5105,10 +5980,11 @@ async function createProjectHandler(input) {
|
|
|
5105
5980
|
let currentPathInput;
|
|
5106
5981
|
if (input.yes && input.projectName) currentPathInput = input.projectName;
|
|
5107
5982
|
else if (input.yes) {
|
|
5108
|
-
|
|
5983
|
+
const defaultConfig = getDefaultConfig();
|
|
5984
|
+
let defaultName = defaultConfig.relativePath;
|
|
5109
5985
|
let counter = 1;
|
|
5110
|
-
while (fs.
|
|
5111
|
-
defaultName = `${
|
|
5986
|
+
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
5987
|
+
defaultName = `${defaultConfig.projectName}-${counter}`;
|
|
5112
5988
|
counter++;
|
|
5113
5989
|
}
|
|
5114
5990
|
currentPathInput = defaultName;
|
|
@@ -5146,7 +6022,8 @@ async function createProjectHandler(input) {
|
|
|
5146
6022
|
install: false,
|
|
5147
6023
|
dbSetup: "none",
|
|
5148
6024
|
api: "none",
|
|
5149
|
-
webDeploy: "none"
|
|
6025
|
+
webDeploy: "none",
|
|
6026
|
+
serverDeploy: "none"
|
|
5150
6027
|
},
|
|
5151
6028
|
reproducibleCommand: "",
|
|
5152
6029
|
timeScaffolded,
|
|
@@ -5162,28 +6039,33 @@ async function createProjectHandler(input) {
|
|
|
5162
6039
|
projectDirectory: input.projectName
|
|
5163
6040
|
};
|
|
5164
6041
|
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
6042
|
let config;
|
|
5173
6043
|
if (input.yes) {
|
|
6044
|
+
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
5174
6045
|
config = {
|
|
5175
|
-
...
|
|
6046
|
+
...getDefaultConfig(),
|
|
5176
6047
|
...flagConfig,
|
|
5177
6048
|
projectName: finalBaseName,
|
|
5178
6049
|
projectDir: finalResolvedPath,
|
|
5179
6050
|
relativePath: finalPathInput
|
|
5180
6051
|
};
|
|
6052
|
+
coerceBackendPresets(config);
|
|
6053
|
+
validateConfigCompatibility(config, providedFlags, cliInput);
|
|
5181
6054
|
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
6055
|
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
6056
|
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
5184
6057
|
log.message(displayConfig(config));
|
|
5185
6058
|
log.message("");
|
|
5186
|
-
} else
|
|
6059
|
+
} else {
|
|
6060
|
+
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
6061
|
+
const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
|
|
6062
|
+
if (Object.keys(otherFlags).length > 0) {
|
|
6063
|
+
log.info(pc.yellow("Using these pre-selected options:"));
|
|
6064
|
+
log.message(displayConfig(otherFlags));
|
|
6065
|
+
log.message("");
|
|
6066
|
+
}
|
|
6067
|
+
config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
|
|
6068
|
+
}
|
|
5187
6069
|
await createProject(config);
|
|
5188
6070
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
5189
6071
|
log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
@@ -5203,11 +6085,11 @@ async function createProjectHandler(input) {
|
|
|
5203
6085
|
}
|
|
5204
6086
|
async function handleDirectoryConflictProgrammatically(currentPathInput, strategy) {
|
|
5205
6087
|
const currentPath = path.resolve(process.cwd(), currentPathInput);
|
|
5206
|
-
if (!fs.
|
|
6088
|
+
if (!await fs.pathExists(currentPath)) return {
|
|
5207
6089
|
finalPathInput: currentPathInput,
|
|
5208
6090
|
shouldClearDirectory: false
|
|
5209
6091
|
};
|
|
5210
|
-
const dirContents = fs.
|
|
6092
|
+
const dirContents = await fs.readdir(currentPath);
|
|
5211
6093
|
const isNotEmpty = dirContents.length > 0;
|
|
5212
6094
|
if (!isNotEmpty) return {
|
|
5213
6095
|
finalPathInput: currentPathInput,
|
|
@@ -5226,7 +6108,7 @@ async function handleDirectoryConflictProgrammatically(currentPathInput, strateg
|
|
|
5226
6108
|
let counter = 1;
|
|
5227
6109
|
const baseName = currentPathInput;
|
|
5228
6110
|
let finalPathInput = `${baseName}-${counter}`;
|
|
5229
|
-
while (fs.
|
|
6111
|
+
while (await fs.pathExists(path.resolve(process.cwd(), finalPathInput)) && (await fs.readdir(path.resolve(process.cwd(), finalPathInput))).length > 0) {
|
|
5230
6112
|
counter++;
|
|
5231
6113
|
finalPathInput = `${baseName}-${counter}`;
|
|
5232
6114
|
}
|
|
@@ -5252,6 +6134,10 @@ async function addAddonsHandler(input) {
|
|
|
5252
6134
|
const deploymentPrompt = await getDeploymentToAdd(detectedConfig.frontend || [], detectedConfig.webDeploy);
|
|
5253
6135
|
if (deploymentPrompt !== "none") input.webDeploy = deploymentPrompt;
|
|
5254
6136
|
}
|
|
6137
|
+
if (!input.serverDeploy) {
|
|
6138
|
+
const serverDeploymentPrompt = await getServerDeploymentToAdd(detectedConfig.runtime, detectedConfig.serverDeploy);
|
|
6139
|
+
if (serverDeploymentPrompt !== "none") input.serverDeploy = serverDeploymentPrompt;
|
|
6140
|
+
}
|
|
5255
6141
|
const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
|
|
5256
6142
|
let somethingAdded = false;
|
|
5257
6143
|
if (input.addons && input.addons.length > 0) {
|
|
@@ -5272,6 +6158,15 @@ async function addAddonsHandler(input) {
|
|
|
5272
6158
|
});
|
|
5273
6159
|
somethingAdded = true;
|
|
5274
6160
|
}
|
|
6161
|
+
if (input.serverDeploy && input.serverDeploy !== "none") {
|
|
6162
|
+
await addDeploymentToProject({
|
|
6163
|
+
...input,
|
|
6164
|
+
install: false,
|
|
6165
|
+
suppressInstallMessage: true,
|
|
6166
|
+
serverDeploy: input.serverDeploy
|
|
6167
|
+
});
|
|
6168
|
+
somethingAdded = true;
|
|
6169
|
+
}
|
|
5275
6170
|
if (!somethingAdded) {
|
|
5276
6171
|
outro(pc.yellow("No addons or deployment configurations to add."));
|
|
5277
6172
|
return;
|
|
@@ -5375,6 +6270,7 @@ const router = t.router({
|
|
|
5375
6270
|
runtime: RuntimeSchema.optional(),
|
|
5376
6271
|
api: APISchema.optional(),
|
|
5377
6272
|
webDeploy: WebDeploySchema.optional(),
|
|
6273
|
+
serverDeploy: ServerDeploySchema.optional(),
|
|
5378
6274
|
directoryConflict: DirectoryConflictSchema.optional(),
|
|
5379
6275
|
renderTitle: z.boolean().optional(),
|
|
5380
6276
|
disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics")
|
|
@@ -5390,6 +6286,7 @@ const router = t.router({
|
|
|
5390
6286
|
add: t.procedure.meta({ description: "Add addons or deployment configurations to an existing Better-T Stack project" }).input(z.tuple([z.object({
|
|
5391
6287
|
addons: z.array(AddonsSchema).optional().default([]),
|
|
5392
6288
|
webDeploy: WebDeploySchema.optional(),
|
|
6289
|
+
serverDeploy: ServerDeploySchema.optional(),
|
|
5393
6290
|
projectDir: z.string().optional(),
|
|
5394
6291
|
install: z.boolean().optional().default(false).describe("Install dependencies after adding addons or deployment"),
|
|
5395
6292
|
packageManager: PackageManagerSchema.optional()
|