create-better-t-stack 2.22.10 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +483 -39
- package/package.json +2 -1
- package/templates/deploy/web/nuxt/wrangler.jsonc.hbs +51 -0
- package/templates/deploy/web/react/next/open-next.config.ts +6 -0
- package/templates/deploy/web/react/next/wrangler.jsonc.hbs +22 -0
- package/templates/deploy/web/react/react-router/wrangler.jsonc.hbs +8 -0
- package/templates/deploy/web/react/tanstack-router/wrangler.jsonc.hbs +8 -0
- package/templates/deploy/web/solid/wrangler.jsonc.hbs +8 -0
- package/templates/deploy/web/svelte/wrangler.jsonc.hbs +51 -0
- package/templates/frontend/react/react-router/vite.config.ts.hbs +1 -22
- package/templates/frontend/react/tanstack-router/vite.config.ts.hbs +3 -24
- package/templates/frontend/react/tanstack-start/package.json.hbs +6 -6
- package/templates/frontend/react/web-base/_gitignore +5 -0
- package/templates/frontend/solid/_gitignore +3 -0
- package/templates/frontend/solid/package.json.hbs +2 -2
- package/templates/frontend/solid/src/main.tsx.hbs +4 -1
- package/templates/frontend/solid/vite.config.ts.hbs +18 -0
- package/templates/frontend/svelte/package.json.hbs +1 -1
- package/templates/frontend/solid/vite.config.js.hbs +0 -39
- /package/templates/frontend/svelte/{svelte.config.js → svelte.config.js.hbs} +0 -0
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { PostHog } from "posthog-node";
|
|
|
11
11
|
import gradient from "gradient-string";
|
|
12
12
|
import * as JSONC from "jsonc-parser";
|
|
13
13
|
import { $, execa } from "execa";
|
|
14
|
+
import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
|
|
14
15
|
import { globby } from "globby";
|
|
15
16
|
import handlebars from "handlebars";
|
|
16
17
|
import os from "node:os";
|
|
@@ -44,7 +45,8 @@ const DEFAULT_CONFIG = {
|
|
|
44
45
|
dbSetup: "none",
|
|
45
46
|
backend: "hono",
|
|
46
47
|
runtime: "bun",
|
|
47
|
-
api: "trpc"
|
|
48
|
+
api: "trpc",
|
|
49
|
+
webDeploy: "none"
|
|
48
50
|
};
|
|
49
51
|
const dependencyVersionMap = {
|
|
50
52
|
"better-auth": "^1.2.10",
|
|
@@ -59,8 +61,8 @@ const dependencyVersionMap = {
|
|
|
59
61
|
"@prisma/client": "^6.9.0",
|
|
60
62
|
prisma: "^6.9.0",
|
|
61
63
|
mongoose: "^8.14.0",
|
|
62
|
-
"vite-plugin-pwa": "^0.
|
|
63
|
-
"@vite-pwa/assets-generator": "^0.
|
|
64
|
+
"vite-plugin-pwa": "^1.0.1",
|
|
65
|
+
"@vite-pwa/assets-generator": "^1.0.0",
|
|
64
66
|
"@tauri-apps/cli": "^2.4.0",
|
|
65
67
|
"@biomejs/biome": "^2.0.0",
|
|
66
68
|
husky: "^9.1.7",
|
|
@@ -102,13 +104,18 @@ const dependencyVersionMap = {
|
|
|
102
104
|
"@tanstack/react-query": "^5.80.5",
|
|
103
105
|
"@tanstack/solid-query": "^5.75.0",
|
|
104
106
|
"@tanstack/solid-query-devtools": "^5.75.0",
|
|
105
|
-
wrangler: "^4.
|
|
107
|
+
wrangler: "^4.23.0",
|
|
108
|
+
"@cloudflare/vite-plugin": "^1.9.0",
|
|
109
|
+
"@opennextjs/cloudflare": "^1.3.0",
|
|
110
|
+
"nitro-cloudflare-dev": "^0.2.2",
|
|
111
|
+
"@sveltejs/adapter-cloudflare": "^7.0.4"
|
|
106
112
|
};
|
|
107
113
|
const ADDON_COMPATIBILITY = {
|
|
108
114
|
pwa: [
|
|
109
115
|
"tanstack-router",
|
|
110
116
|
"react-router",
|
|
111
|
-
"solid"
|
|
117
|
+
"solid",
|
|
118
|
+
"next"
|
|
112
119
|
],
|
|
113
120
|
tauri: [
|
|
114
121
|
"tanstack-router",
|
|
@@ -211,6 +218,7 @@ const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(
|
|
|
211
218
|
];
|
|
212
219
|
return !invalidChars.some((char) => name.includes(char));
|
|
213
220
|
}, "Project name contains invalid characters").refine((name) => name.toLowerCase() !== "node_modules", "Project name is reserved").describe("Project name or path");
|
|
221
|
+
const WebDeploySchema = z.enum(["workers", "none"]).describe("Web deployment");
|
|
214
222
|
|
|
215
223
|
//#endregion
|
|
216
224
|
//#region src/utils/addon-compatibility.ts
|
|
@@ -318,7 +326,7 @@ async function getAddonsToAdd(frontend, existingAddons = []) {
|
|
|
318
326
|
const response = await multiselect({
|
|
319
327
|
message: "Select addons",
|
|
320
328
|
options,
|
|
321
|
-
required:
|
|
329
|
+
required: false
|
|
322
330
|
});
|
|
323
331
|
if (isCancel(response)) {
|
|
324
332
|
cancel(pc.red("Operation cancelled"));
|
|
@@ -809,7 +817,7 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
809
817
|
}];
|
|
810
818
|
if (backend === "hono") runtimeOptions.push({
|
|
811
819
|
value: "workers",
|
|
812
|
-
label: "Cloudflare Workers
|
|
820
|
+
label: "Cloudflare Workers",
|
|
813
821
|
hint: "Edge runtime on Cloudflare's global network"
|
|
814
822
|
});
|
|
815
823
|
const response = await select({
|
|
@@ -824,6 +832,79 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
824
832
|
return response;
|
|
825
833
|
}
|
|
826
834
|
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/prompts/web-deploy.ts
|
|
837
|
+
const WORKERS_COMPATIBLE_FRONTENDS = [
|
|
838
|
+
"tanstack-router",
|
|
839
|
+
"react-router",
|
|
840
|
+
"solid",
|
|
841
|
+
"next",
|
|
842
|
+
"nuxt",
|
|
843
|
+
"svelte"
|
|
844
|
+
];
|
|
845
|
+
function getDeploymentDisplay(deployment) {
|
|
846
|
+
if (deployment === "workers") return {
|
|
847
|
+
label: "Cloudflare Workers",
|
|
848
|
+
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
849
|
+
};
|
|
850
|
+
return {
|
|
851
|
+
label: deployment,
|
|
852
|
+
hint: `Add ${deployment} deployment`
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []) {
|
|
856
|
+
if (deployment !== void 0) return deployment;
|
|
857
|
+
const hasCompatibleFrontend = frontend.some((f) => WORKERS_COMPATIBLE_FRONTENDS.includes(f));
|
|
858
|
+
if (!hasCompatibleFrontend) return "none";
|
|
859
|
+
const options = [{
|
|
860
|
+
value: "workers",
|
|
861
|
+
label: "Cloudflare Workers",
|
|
862
|
+
hint: "Deploy to Cloudflare Workers using Wrangler"
|
|
863
|
+
}, {
|
|
864
|
+
value: "none",
|
|
865
|
+
label: "None",
|
|
866
|
+
hint: "Manual setup"
|
|
867
|
+
}];
|
|
868
|
+
const response = await select({
|
|
869
|
+
message: "Select web deployment",
|
|
870
|
+
options,
|
|
871
|
+
initialValue: DEFAULT_CONFIG.webDeploy
|
|
872
|
+
});
|
|
873
|
+
if (isCancel(response)) {
|
|
874
|
+
cancel(pc.red("Operation cancelled"));
|
|
875
|
+
process.exit(0);
|
|
876
|
+
}
|
|
877
|
+
return response;
|
|
878
|
+
}
|
|
879
|
+
async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
880
|
+
const options = [];
|
|
881
|
+
if (frontend.some((f) => WORKERS_COMPATIBLE_FRONTENDS.includes(f)) && existingDeployment !== "workers") {
|
|
882
|
+
const { label, hint } = getDeploymentDisplay("workers");
|
|
883
|
+
options.push({
|
|
884
|
+
value: "workers",
|
|
885
|
+
label,
|
|
886
|
+
hint
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
if (existingDeployment && existingDeployment !== "none") return "none";
|
|
890
|
+
if (options.length > 0) options.push({
|
|
891
|
+
value: "none",
|
|
892
|
+
label: "None",
|
|
893
|
+
hint: "Skip deployment setup"
|
|
894
|
+
});
|
|
895
|
+
if (options.length === 0) return "none";
|
|
896
|
+
const response = await select({
|
|
897
|
+
message: "Select web deployment",
|
|
898
|
+
options,
|
|
899
|
+
initialValue: DEFAULT_CONFIG.webDeploy
|
|
900
|
+
});
|
|
901
|
+
if (isCancel(response)) {
|
|
902
|
+
cancel(pc.red("Operation cancelled"));
|
|
903
|
+
process.exit(0);
|
|
904
|
+
}
|
|
905
|
+
return response;
|
|
906
|
+
}
|
|
907
|
+
|
|
827
908
|
//#endregion
|
|
828
909
|
//#region src/prompts/config-prompts.ts
|
|
829
910
|
async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
@@ -838,6 +919,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
838
919
|
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
|
839
920
|
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
|
|
840
921
|
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
|
|
922
|
+
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend),
|
|
841
923
|
git: () => getGitChoice(flags.git),
|
|
842
924
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
|
843
925
|
install: () => getinstallChoice(flags.install)
|
|
@@ -853,6 +935,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
853
935
|
result.auth = false;
|
|
854
936
|
result.dbSetup = "none";
|
|
855
937
|
result.examples = ["todo"];
|
|
938
|
+
result.webDeploy = "none";
|
|
856
939
|
}
|
|
857
940
|
if (result.backend === "none") {
|
|
858
941
|
result.runtime = "none";
|
|
@@ -862,6 +945,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
862
945
|
result.auth = false;
|
|
863
946
|
result.dbSetup = "none";
|
|
864
947
|
result.examples = [];
|
|
948
|
+
result.webDeploy = "none";
|
|
865
949
|
}
|
|
866
950
|
return {
|
|
867
951
|
projectName,
|
|
@@ -879,7 +963,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
879
963
|
packageManager: result.packageManager,
|
|
880
964
|
install: result.install,
|
|
881
965
|
dbSetup: result.dbSetup,
|
|
882
|
-
api: result.api
|
|
966
|
+
api: result.api,
|
|
967
|
+
webDeploy: result.webDeploy
|
|
883
968
|
};
|
|
884
969
|
}
|
|
885
970
|
|
|
@@ -1013,6 +1098,7 @@ function displayConfig(config) {
|
|
|
1013
1098
|
configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
|
|
1014
1099
|
}
|
|
1015
1100
|
if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
|
|
1101
|
+
if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
|
|
1016
1102
|
if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
|
|
1017
1103
|
return configDisplay.join("\n");
|
|
1018
1104
|
}
|
|
@@ -1034,6 +1120,7 @@ function generateReproducibleCommand(config) {
|
|
|
1034
1120
|
if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
|
|
1035
1121
|
else flags.push("--examples none");
|
|
1036
1122
|
flags.push(`--db-setup ${config.dbSetup}`);
|
|
1123
|
+
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
1037
1124
|
flags.push(config.git ? "--git" : "--no-git");
|
|
1038
1125
|
flags.push(`--package-manager ${config.packageManager}`);
|
|
1039
1126
|
flags.push(config.install ? "--install" : "--no-install");
|
|
@@ -1210,6 +1297,7 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1210
1297
|
if (options.runtime) config.runtime = options.runtime;
|
|
1211
1298
|
if (options.dbSetup) config.dbSetup = options.dbSetup;
|
|
1212
1299
|
if (options.packageManager) config.packageManager = options.packageManager;
|
|
1300
|
+
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1213
1301
|
if (projectName) {
|
|
1214
1302
|
const result = ProjectNameSchema.safeParse(path.basename(projectName));
|
|
1215
1303
|
if (!result.success) {
|
|
@@ -1395,6 +1483,13 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1395
1483
|
consola$1.fatal("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
1396
1484
|
process.exit(1);
|
|
1397
1485
|
}
|
|
1486
|
+
if (config.webDeploy === "workers" && config.frontend && config.frontend.length > 0) {
|
|
1487
|
+
const incompatibleFrontends = config.frontend.filter((f) => f === "tanstack-start");
|
|
1488
|
+
if (incompatibleFrontends.length > 0) {
|
|
1489
|
+
consola$1.fatal(`The following frontends are not compatible with '--web-deploy workers': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or remove '--web-deploy workers'.`);
|
|
1490
|
+
process.exit(1);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1398
1493
|
return config;
|
|
1399
1494
|
}
|
|
1400
1495
|
function getProvidedFlags(options) {
|
|
@@ -1418,7 +1513,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1418
1513
|
auth: projectConfig.auth,
|
|
1419
1514
|
packageManager: projectConfig.packageManager,
|
|
1420
1515
|
dbSetup: projectConfig.dbSetup,
|
|
1421
|
-
api: projectConfig.api
|
|
1516
|
+
api: projectConfig.api,
|
|
1517
|
+
webDeploy: projectConfig.webDeploy
|
|
1422
1518
|
};
|
|
1423
1519
|
const baseContent = {
|
|
1424
1520
|
$schema: "https://better-t-stack.dev/schema.json",
|
|
@@ -1434,7 +1530,8 @@ async function writeBtsConfig(projectConfig) {
|
|
|
1434
1530
|
auth: btsConfig.auth,
|
|
1435
1531
|
packageManager: btsConfig.packageManager,
|
|
1436
1532
|
dbSetup: btsConfig.dbSetup,
|
|
1437
|
-
api: btsConfig.api
|
|
1533
|
+
api: btsConfig.api,
|
|
1534
|
+
webDeploy: btsConfig.webDeploy
|
|
1438
1535
|
};
|
|
1439
1536
|
let configContent = JSON.stringify(baseContent);
|
|
1440
1537
|
const formatResult = JSONC.format(configContent, void 0, {
|
|
@@ -1614,6 +1711,57 @@ async function setupTauri(config) {
|
|
|
1614
1711
|
}
|
|
1615
1712
|
}
|
|
1616
1713
|
|
|
1714
|
+
//#endregion
|
|
1715
|
+
//#region src/utils/ts-morph.ts
|
|
1716
|
+
const tsProject = new Project({
|
|
1717
|
+
useInMemoryFileSystem: false,
|
|
1718
|
+
skipAddingFilesFromTsConfig: true,
|
|
1719
|
+
manipulationSettings: {
|
|
1720
|
+
quoteKind: QuoteKind.Single,
|
|
1721
|
+
indentationText: IndentationText.TwoSpaces
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
function ensureArrayProperty(obj, name) {
|
|
1725
|
+
return obj.getProperty(name)?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression) ?? obj.addPropertyAssignment({
|
|
1726
|
+
name,
|
|
1727
|
+
initializer: "[]"
|
|
1728
|
+
}).getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
//#endregion
|
|
1732
|
+
//#region src/helpers/setup/vite-pwa-setup.ts
|
|
1733
|
+
async function addPwaToViteConfig(viteConfigPath, projectName) {
|
|
1734
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
|
1735
|
+
if (!sourceFile) throw new Error("vite config not found");
|
|
1736
|
+
const hasImport = sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "vite-plugin-pwa");
|
|
1737
|
+
if (!hasImport) sourceFile.insertImportDeclaration(0, {
|
|
1738
|
+
namedImports: ["VitePWA"],
|
|
1739
|
+
moduleSpecifier: "vite-plugin-pwa"
|
|
1740
|
+
});
|
|
1741
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
1742
|
+
const expression = expr.getExpression();
|
|
1743
|
+
return Node.isIdentifier(expression) && expression.getText() === "defineConfig";
|
|
1744
|
+
});
|
|
1745
|
+
if (!defineCall) throw new Error("Could not find defineConfig call in vite config");
|
|
1746
|
+
const callExpr = defineCall;
|
|
1747
|
+
const configObject = callExpr.getArguments()[0];
|
|
1748
|
+
if (!configObject) throw new Error("defineConfig argument is not an object literal");
|
|
1749
|
+
const pluginsArray = ensureArrayProperty(configObject, "plugins");
|
|
1750
|
+
const alreadyPresent = pluginsArray.getElements().some((el) => el.getText().startsWith("VitePWA("));
|
|
1751
|
+
if (!alreadyPresent) pluginsArray.addElement(`VitePWA({
|
|
1752
|
+
registerType: "autoUpdate",
|
|
1753
|
+
manifest: {
|
|
1754
|
+
name: "${projectName}",
|
|
1755
|
+
short_name: "${projectName}",
|
|
1756
|
+
description: "${projectName} - PWA Application",
|
|
1757
|
+
theme_color: "#0c0c0c",
|
|
1758
|
+
},
|
|
1759
|
+
pwaAssets: { disabled: false, config: true },
|
|
1760
|
+
devOptions: { enabled: true },
|
|
1761
|
+
})`);
|
|
1762
|
+
await tsProject.save();
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1617
1765
|
//#endregion
|
|
1618
1766
|
//#region src/helpers/setup/addons-setup.ts
|
|
1619
1767
|
async function setupAddons(config, isAddCommand = false) {
|
|
@@ -1706,6 +1854,8 @@ async function setupPwa(projectDir, frontends) {
|
|
|
1706
1854
|
};
|
|
1707
1855
|
await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
|
|
1708
1856
|
}
|
|
1857
|
+
const viteConfigTs = path.join(clientPackageDir, "vite.config.ts");
|
|
1858
|
+
if (await fs.pathExists(viteConfigTs)) await addPwaToViteConfig(viteConfigTs, path.basename(projectDir));
|
|
1709
1859
|
}
|
|
1710
1860
|
|
|
1711
1861
|
//#endregion
|
|
@@ -1726,7 +1876,8 @@ async function detectProjectConfig(projectDir) {
|
|
|
1726
1876
|
auth: btsConfig.auth,
|
|
1727
1877
|
packageManager: btsConfig.packageManager,
|
|
1728
1878
|
dbSetup: btsConfig.dbSetup,
|
|
1729
|
-
api: btsConfig.api
|
|
1879
|
+
api: btsConfig.api,
|
|
1880
|
+
webDeploy: btsConfig.webDeploy
|
|
1730
1881
|
};
|
|
1731
1882
|
return null;
|
|
1732
1883
|
} catch (_error) {
|
|
@@ -2124,10 +2275,30 @@ async function handleExtras(projectDir, context) {
|
|
|
2124
2275
|
if (await fs.pathExists(runtimeWorkersDir)) await processAndCopyFiles("**/*", runtimeWorkersDir, projectDir, context, false);
|
|
2125
2276
|
}
|
|
2126
2277
|
}
|
|
2278
|
+
async function setupDeploymentTemplates(projectDir, context) {
|
|
2279
|
+
if (context.webDeploy === "none") return;
|
|
2280
|
+
if (context.webDeploy === "workers") {
|
|
2281
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2282
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
2283
|
+
const frontends = context.frontend;
|
|
2284
|
+
const templateMap = {
|
|
2285
|
+
"tanstack-router": "react/tanstack-router",
|
|
2286
|
+
"react-router": "react/react-router",
|
|
2287
|
+
solid: "solid",
|
|
2288
|
+
next: "react/next",
|
|
2289
|
+
nuxt: "nuxt",
|
|
2290
|
+
svelte: "svelte"
|
|
2291
|
+
};
|
|
2292
|
+
for (const f of frontends) if (templateMap[f]) {
|
|
2293
|
+
const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/web/${templateMap[f]}`);
|
|
2294
|
+
if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2127
2298
|
|
|
2128
2299
|
//#endregion
|
|
2129
2300
|
//#region src/helpers/project-generation/add-addons.ts
|
|
2130
|
-
function exitWithError(message) {
|
|
2301
|
+
function exitWithError$1(message) {
|
|
2131
2302
|
cancel(pc.red(message));
|
|
2132
2303
|
process.exit(1);
|
|
2133
2304
|
}
|
|
@@ -2135,9 +2306,9 @@ async function addAddonsToProject(input) {
|
|
|
2135
2306
|
try {
|
|
2136
2307
|
const projectDir = input.projectDir || process.cwd();
|
|
2137
2308
|
const isBetterTStack = await isBetterTStackProject(projectDir);
|
|
2138
|
-
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.");
|
|
2309
|
+
if (!isBetterTStack) exitWithError$1("This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.");
|
|
2139
2310
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
2140
|
-
if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
2311
|
+
if (!detectedConfig) exitWithError$1("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
2141
2312
|
const config = {
|
|
2142
2313
|
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
2143
2314
|
projectDir,
|
|
@@ -2154,11 +2325,12 @@ async function addAddonsToProject(input) {
|
|
|
2154
2325
|
packageManager: input.packageManager || detectedConfig.packageManager || "npm",
|
|
2155
2326
|
install: input.install || false,
|
|
2156
2327
|
dbSetup: detectedConfig.dbSetup || "none",
|
|
2157
|
-
api: detectedConfig.api || "none"
|
|
2328
|
+
api: detectedConfig.api || "none",
|
|
2329
|
+
webDeploy: detectedConfig.webDeploy || "none"
|
|
2158
2330
|
};
|
|
2159
2331
|
for (const addon of input.addons) {
|
|
2160
2332
|
const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
|
|
2161
|
-
if (!isCompatible) exitWithError(reason || `${addon} addon is not compatible with current frontend configuration`);
|
|
2333
|
+
if (!isCompatible) exitWithError$1(reason || `${addon} addon is not compatible with current frontend configuration`);
|
|
2162
2334
|
}
|
|
2163
2335
|
log.info(pc.green(`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`));
|
|
2164
2336
|
await setupAddonsTemplate(projectDir, config);
|
|
@@ -2170,10 +2342,256 @@ async function addAddonsToProject(input) {
|
|
|
2170
2342
|
projectDir,
|
|
2171
2343
|
packageManager: config.packageManager
|
|
2172
2344
|
});
|
|
2173
|
-
else log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
2345
|
+
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
2174
2346
|
} catch (error) {
|
|
2175
2347
|
const message = error instanceof Error ? error.message : String(error);
|
|
2176
|
-
exitWithError(`Error adding addons: ${message}`);
|
|
2348
|
+
exitWithError$1(`Error adding addons: ${message}`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
//#endregion
|
|
2353
|
+
//#region src/helpers/setup/workers-nuxt-setup.ts
|
|
2354
|
+
async function setupNuxtWorkersDeploy(projectDir, packageManager) {
|
|
2355
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2356
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
2357
|
+
await addPackageDependency({
|
|
2358
|
+
devDependencies: ["nitro-cloudflare-dev", "wrangler"],
|
|
2359
|
+
projectDir: webAppDir
|
|
2360
|
+
});
|
|
2361
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
2362
|
+
if (await fs.pathExists(pkgPath)) {
|
|
2363
|
+
const pkg = await fs.readJson(pkgPath);
|
|
2364
|
+
pkg.scripts = {
|
|
2365
|
+
...pkg.scripts,
|
|
2366
|
+
deploy: `${packageManager} run build && wrangler deploy`,
|
|
2367
|
+
"cf-typegen": "wrangler types"
|
|
2368
|
+
};
|
|
2369
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2370
|
+
}
|
|
2371
|
+
const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
|
|
2372
|
+
if (!await fs.pathExists(nuxtConfigPath)) return;
|
|
2373
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(nuxtConfigPath);
|
|
2374
|
+
if (!sourceFile) return;
|
|
2375
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
2376
|
+
const expression = expr.getExpression();
|
|
2377
|
+
return Node.isIdentifier(expression) && expression.getText() === "defineNuxtConfig";
|
|
2378
|
+
});
|
|
2379
|
+
if (!defineCall) return;
|
|
2380
|
+
const configObj = defineCall.getArguments()[0];
|
|
2381
|
+
if (!configObj) return;
|
|
2382
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2383
|
+
const compatProp = configObj.getProperty("compatibilityDate");
|
|
2384
|
+
if (compatProp && compatProp.getKind() === SyntaxKind.PropertyAssignment) compatProp.setInitializer(`'${today}'`);
|
|
2385
|
+
else configObj.addPropertyAssignment({
|
|
2386
|
+
name: "compatibilityDate",
|
|
2387
|
+
initializer: `'${today}'`
|
|
2388
|
+
});
|
|
2389
|
+
const nitroInitializer = `{
|
|
2390
|
+
preset: "cloudflare_module",
|
|
2391
|
+
cloudflare: {
|
|
2392
|
+
deployConfig: true,
|
|
2393
|
+
nodeCompat: true
|
|
2394
|
+
}
|
|
2395
|
+
}`;
|
|
2396
|
+
const nitroProp = configObj.getProperty("nitro");
|
|
2397
|
+
if (nitroProp && nitroProp.getKind() === SyntaxKind.PropertyAssignment) nitroProp.setInitializer(nitroInitializer);
|
|
2398
|
+
else configObj.addPropertyAssignment({
|
|
2399
|
+
name: "nitro",
|
|
2400
|
+
initializer: nitroInitializer
|
|
2401
|
+
});
|
|
2402
|
+
const modulesProp = configObj.getProperty("modules");
|
|
2403
|
+
if (modulesProp && modulesProp.getKind() === SyntaxKind.PropertyAssignment) {
|
|
2404
|
+
const arrayExpr = modulesProp.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
|
|
2405
|
+
if (arrayExpr) {
|
|
2406
|
+
const alreadyHas = arrayExpr.getElements().some((el) => el.getText().replace(/['"`]/g, "") === "nitro-cloudflare-dev");
|
|
2407
|
+
if (!alreadyHas) arrayExpr.addElement("'nitro-cloudflare-dev'");
|
|
2408
|
+
}
|
|
2409
|
+
} else configObj.addPropertyAssignment({
|
|
2410
|
+
name: "modules",
|
|
2411
|
+
initializer: "['nitro-cloudflare-dev']"
|
|
2412
|
+
});
|
|
2413
|
+
await tsProject.save();
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
//#endregion
|
|
2417
|
+
//#region src/helpers/setup/workers-svelte-setup.ts
|
|
2418
|
+
async function setupSvelteWorkersDeploy(projectDir, packageManager) {
|
|
2419
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2420
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
2421
|
+
await addPackageDependency({
|
|
2422
|
+
devDependencies: ["@sveltejs/adapter-cloudflare", "wrangler"],
|
|
2423
|
+
projectDir: webAppDir
|
|
2424
|
+
});
|
|
2425
|
+
const pkgPath = path.join(webAppDir, "package.json");
|
|
2426
|
+
if (await fs.pathExists(pkgPath)) {
|
|
2427
|
+
const pkg = await fs.readJson(pkgPath);
|
|
2428
|
+
pkg.scripts = {
|
|
2429
|
+
...pkg.scripts,
|
|
2430
|
+
deploy: `${packageManager} run build && wrangler deploy`,
|
|
2431
|
+
"cf-typegen": "wrangler types ./src/worker-configuration.d.ts"
|
|
2432
|
+
};
|
|
2433
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2434
|
+
}
|
|
2435
|
+
const possibleConfigFiles = [path.join(webAppDir, "svelte.config.js"), path.join(webAppDir, "svelte.config.ts")];
|
|
2436
|
+
const existingConfigPath = (await Promise.all(possibleConfigFiles.map(async (p) => await fs.pathExists(p) ? p : ""))).find((p) => p);
|
|
2437
|
+
if (existingConfigPath) {
|
|
2438
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(existingConfigPath);
|
|
2439
|
+
if (!sourceFile) return;
|
|
2440
|
+
const adapterImport = sourceFile.getImportDeclarations().find((imp) => ["@sveltejs/adapter-auto", "@sveltejs/adapter-node"].includes(imp.getModuleSpecifierValue()));
|
|
2441
|
+
if (adapterImport) adapterImport.setModuleSpecifier("@sveltejs/adapter-cloudflare");
|
|
2442
|
+
else {
|
|
2443
|
+
const alreadyHasCloudflare = sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "@sveltejs/adapter-cloudflare");
|
|
2444
|
+
if (!alreadyHasCloudflare) sourceFile.insertImportDeclaration(0, {
|
|
2445
|
+
defaultImport: "adapter",
|
|
2446
|
+
moduleSpecifier: "@sveltejs/adapter-cloudflare"
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
await tsProject.save();
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
//#endregion
|
|
2454
|
+
//#region src/helpers/setup/workers-vite-setup.ts
|
|
2455
|
+
async function setupWorkersVitePlugin(projectDir) {
|
|
2456
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2457
|
+
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
|
2458
|
+
if (!await fs.pathExists(viteConfigPath)) throw new Error("vite.config.ts not found in web app directory");
|
|
2459
|
+
await addPackageDependency({
|
|
2460
|
+
devDependencies: ["@cloudflare/vite-plugin", "wrangler"],
|
|
2461
|
+
projectDir: webAppDir
|
|
2462
|
+
});
|
|
2463
|
+
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
|
2464
|
+
if (!sourceFile) throw new Error("vite.config.ts not found in web app directory");
|
|
2465
|
+
const hasCloudflareImport = sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "@cloudflare/vite-plugin");
|
|
2466
|
+
if (!hasCloudflareImport) sourceFile.insertImportDeclaration(0, {
|
|
2467
|
+
namedImports: ["cloudflare"],
|
|
2468
|
+
moduleSpecifier: "@cloudflare/vite-plugin"
|
|
2469
|
+
});
|
|
2470
|
+
const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
|
|
2471
|
+
const expression = expr.getExpression();
|
|
2472
|
+
return Node.isIdentifier(expression) && expression.getText() === "defineConfig";
|
|
2473
|
+
});
|
|
2474
|
+
if (!defineCall) throw new Error("Could not find defineConfig call in vite config");
|
|
2475
|
+
const callExpr = defineCall;
|
|
2476
|
+
const configObject = callExpr.getArguments()[0];
|
|
2477
|
+
if (!configObject) throw new Error("defineConfig argument is not an object literal");
|
|
2478
|
+
const pluginsArray = ensureArrayProperty(configObject, "plugins");
|
|
2479
|
+
const hasCloudflarePlugin = pluginsArray.getElements().some((el) => el.getText().includes("cloudflare("));
|
|
2480
|
+
if (!hasCloudflarePlugin) pluginsArray.addElement("cloudflare()");
|
|
2481
|
+
await tsProject.save();
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
//#endregion
|
|
2485
|
+
//#region src/helpers/setup/web-deploy-setup.ts
|
|
2486
|
+
async function setupWebDeploy(config) {
|
|
2487
|
+
const { webDeploy, frontend, projectDir } = config;
|
|
2488
|
+
const { packageManager } = config;
|
|
2489
|
+
if (webDeploy === "none") return;
|
|
2490
|
+
if (webDeploy !== "workers") return;
|
|
2491
|
+
const isNext = frontend.includes("next");
|
|
2492
|
+
const isNuxt = frontend.includes("nuxt");
|
|
2493
|
+
const isSvelte = frontend.includes("svelte");
|
|
2494
|
+
const isTanstackRouter = frontend.includes("tanstack-router");
|
|
2495
|
+
const isReactRouter = frontend.includes("react-router");
|
|
2496
|
+
const isSolid = frontend.includes("solid");
|
|
2497
|
+
if (isNext) await setupNextWorkersDeploy(projectDir, packageManager);
|
|
2498
|
+
else if (isNuxt) await setupNuxtWorkersDeploy(projectDir, packageManager);
|
|
2499
|
+
else if (isSvelte) await setupSvelteWorkersDeploy(projectDir, packageManager);
|
|
2500
|
+
else if (isTanstackRouter || isReactRouter || isSolid) await setupWorkersWebDeploy(projectDir, packageManager);
|
|
2501
|
+
}
|
|
2502
|
+
async function setupWorkersWebDeploy(projectDir, pkgManager) {
|
|
2503
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2504
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
2505
|
+
const packageJsonPath = path.join(webAppDir, "package.json");
|
|
2506
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
2507
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
2508
|
+
packageJson.scripts = {
|
|
2509
|
+
...packageJson.scripts,
|
|
2510
|
+
"wrangler:dev": "wrangler dev --port=3001",
|
|
2511
|
+
deploy: `${pkgManager} run build && wrangler deploy`
|
|
2512
|
+
};
|
|
2513
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
2514
|
+
}
|
|
2515
|
+
await setupWorkersVitePlugin(projectDir);
|
|
2516
|
+
}
|
|
2517
|
+
async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
2518
|
+
const webAppDir = path.join(projectDir, "apps/web");
|
|
2519
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
2520
|
+
await addPackageDependency({
|
|
2521
|
+
dependencies: ["@opennextjs/cloudflare"],
|
|
2522
|
+
devDependencies: ["wrangler"],
|
|
2523
|
+
projectDir: webAppDir
|
|
2524
|
+
});
|
|
2525
|
+
const packageJsonPath = path.join(webAppDir, "package.json");
|
|
2526
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
2527
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
2528
|
+
pkg.scripts = {
|
|
2529
|
+
...pkg.scripts,
|
|
2530
|
+
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
2531
|
+
deploy: "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
2532
|
+
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
|
2533
|
+
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
2534
|
+
};
|
|
2535
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
//#endregion
|
|
2540
|
+
//#region src/helpers/project-generation/add-deployment.ts
|
|
2541
|
+
function exitWithError(message) {
|
|
2542
|
+
cancel(pc.red(message));
|
|
2543
|
+
process.exit(1);
|
|
2544
|
+
}
|
|
2545
|
+
async function addDeploymentToProject(input) {
|
|
2546
|
+
try {
|
|
2547
|
+
const projectDir = input.projectDir || process.cwd();
|
|
2548
|
+
const isBetterTStack = await isBetterTStackProject(projectDir);
|
|
2549
|
+
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.");
|
|
2550
|
+
const detectedConfig = await detectProjectConfig(projectDir);
|
|
2551
|
+
if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
2552
|
+
if (detectedConfig.webDeploy === input.webDeploy) exitWithError(`${input.webDeploy} deployment is already configured for this project.`);
|
|
2553
|
+
if (input.webDeploy === "workers") {
|
|
2554
|
+
const compatibleFrontends = [
|
|
2555
|
+
"tanstack-router",
|
|
2556
|
+
"react-router",
|
|
2557
|
+
"solid",
|
|
2558
|
+
"next",
|
|
2559
|
+
"svelte"
|
|
2560
|
+
];
|
|
2561
|
+
const hasCompatible = detectedConfig.frontend?.some((f) => compatibleFrontends.includes(f));
|
|
2562
|
+
if (!hasCompatible) exitWithError("Cloudflare Workers deployment requires a compatible web frontend (tanstack-router, react-router, solid, next, or svelte).");
|
|
2563
|
+
}
|
|
2564
|
+
const config = {
|
|
2565
|
+
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
2566
|
+
projectDir,
|
|
2567
|
+
relativePath: ".",
|
|
2568
|
+
database: detectedConfig.database || "none",
|
|
2569
|
+
orm: detectedConfig.orm || "none",
|
|
2570
|
+
backend: detectedConfig.backend || "none",
|
|
2571
|
+
runtime: detectedConfig.runtime || "none",
|
|
2572
|
+
frontend: detectedConfig.frontend || [],
|
|
2573
|
+
addons: detectedConfig.addons || [],
|
|
2574
|
+
examples: detectedConfig.examples || [],
|
|
2575
|
+
auth: detectedConfig.auth || false,
|
|
2576
|
+
git: false,
|
|
2577
|
+
packageManager: input.packageManager || detectedConfig.packageManager || "npm",
|
|
2578
|
+
install: input.install || false,
|
|
2579
|
+
dbSetup: detectedConfig.dbSetup || "none",
|
|
2580
|
+
api: detectedConfig.api || "none",
|
|
2581
|
+
webDeploy: input.webDeploy
|
|
2582
|
+
};
|
|
2583
|
+
log.info(pc.green(`Adding ${input.webDeploy} deployment to ${config.frontend.join("/")}`));
|
|
2584
|
+
await setupDeploymentTemplates(projectDir, config);
|
|
2585
|
+
await setupWebDeploy(config);
|
|
2586
|
+
await updateBtsConfig(projectDir, { webDeploy: input.webDeploy });
|
|
2587
|
+
if (config.install) await installDependencies({
|
|
2588
|
+
projectDir,
|
|
2589
|
+
packageManager: config.packageManager
|
|
2590
|
+
});
|
|
2591
|
+
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
2592
|
+
} catch (error) {
|
|
2593
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2594
|
+
exitWithError(`Error adding deployment: ${message}`);
|
|
2177
2595
|
}
|
|
2178
2596
|
}
|
|
2179
2597
|
|
|
@@ -3861,7 +4279,7 @@ async function initializeGit(projectDir, useGit) {
|
|
|
3861
4279
|
})`git init`;
|
|
3862
4280
|
if (result.exitCode !== 0) throw new Error(`Git initialization failed: ${result.stderr}`);
|
|
3863
4281
|
await $({ cwd: projectDir })`git add -A`;
|
|
3864
|
-
await $({ cwd: projectDir })`git commit -m ${"
|
|
4282
|
+
await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
|
|
3865
4283
|
}
|
|
3866
4284
|
|
|
3867
4285
|
//#endregion
|
|
@@ -3967,7 +4385,7 @@ function getTauriInstructions(runCmd) {
|
|
|
3967
4385
|
return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan("•")} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies.\nSee: https://v2.tauri.app/start/prerequisites/`;
|
|
3968
4386
|
}
|
|
3969
4387
|
function getPwaInstructions() {
|
|
3970
|
-
return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow("NOTE:")} There is a known compatibility issue between VitePWA
|
|
4388
|
+
return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow("NOTE:")} There is a known compatibility issue between VitePWA \nand React Router v7.See: https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
|
|
3971
4389
|
}
|
|
3972
4390
|
function getStarlightInstructions(runCmd) {
|
|
3973
4391
|
return `\n${pc.bold("Documentation with Starlight:")}\n${pc.cyan("•")} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan("•")} Build docs site: ${`cd apps/docs && ${runCmd} build`}`;
|
|
@@ -4151,6 +4569,7 @@ async function createProject(options) {
|
|
|
4151
4569
|
}
|
|
4152
4570
|
if (options.examples.length > 0 && options.examples[0] !== "none") await setupExamplesTemplate(projectDir, options);
|
|
4153
4571
|
await setupAddonsTemplate(projectDir, options);
|
|
4572
|
+
await setupDeploymentTemplates(projectDir, options);
|
|
4154
4573
|
await setupApi(options);
|
|
4155
4574
|
if (!isConvex) {
|
|
4156
4575
|
await setupBackendDependencies(options);
|
|
@@ -4161,6 +4580,7 @@ async function createProject(options) {
|
|
|
4161
4580
|
if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
|
|
4162
4581
|
if (!isConvex && options.auth) await setupAuth(options);
|
|
4163
4582
|
await handleExtras(projectDir, options);
|
|
4583
|
+
await setupWebDeploy(options);
|
|
4164
4584
|
await setupEnvironmentVariables(options);
|
|
4165
4585
|
await updatePackageConfigurations(projectDir, options);
|
|
4166
4586
|
await createReadme(projectDir, options);
|
|
@@ -4252,28 +4672,50 @@ async function createProjectHandler(input) {
|
|
|
4252
4672
|
}
|
|
4253
4673
|
async function addAddonsHandler(input) {
|
|
4254
4674
|
try {
|
|
4675
|
+
const projectDir = input.projectDir || process.cwd();
|
|
4676
|
+
const detectedConfig = await detectProjectConfig(projectDir);
|
|
4677
|
+
if (!detectedConfig) {
|
|
4678
|
+
cancel(pc.red("Could not detect project configuration. Please ensure this is a valid Better-T Stack project."));
|
|
4679
|
+
process.exit(1);
|
|
4680
|
+
}
|
|
4255
4681
|
if (!input.addons || input.addons.length === 0) {
|
|
4256
|
-
const projectDir = input.projectDir || process.cwd();
|
|
4257
|
-
const detectedConfig = await detectProjectConfig(projectDir);
|
|
4258
|
-
if (!detectedConfig) {
|
|
4259
|
-
cancel(pc.red("Could not detect project configuration. Please ensure this is a valid Better-T Stack project."));
|
|
4260
|
-
process.exit(1);
|
|
4261
|
-
}
|
|
4262
4682
|
const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
|
|
4263
|
-
if (addonsPrompt.length
|
|
4264
|
-
outro(pc.yellow("No addons to add or all compatible addons are already present."));
|
|
4265
|
-
return;
|
|
4266
|
-
}
|
|
4267
|
-
input.addons = addonsPrompt;
|
|
4683
|
+
if (addonsPrompt.length > 0) input.addons = addonsPrompt;
|
|
4268
4684
|
}
|
|
4269
|
-
if (!input.
|
|
4270
|
-
|
|
4685
|
+
if (!input.webDeploy) {
|
|
4686
|
+
const deploymentPrompt = await getDeploymentToAdd(detectedConfig.frontend || [], detectedConfig.webDeploy);
|
|
4687
|
+
if (deploymentPrompt !== "none") input.webDeploy = deploymentPrompt;
|
|
4688
|
+
}
|
|
4689
|
+
const packageManager = input.packageManager || detectedConfig.packageManager || "npm";
|
|
4690
|
+
let somethingAdded = false;
|
|
4691
|
+
if (input.addons && input.addons.length > 0) {
|
|
4692
|
+
await addAddonsToProject({
|
|
4693
|
+
...input,
|
|
4694
|
+
install: false,
|
|
4695
|
+
suppressInstallMessage: true,
|
|
4696
|
+
addons: input.addons
|
|
4697
|
+
});
|
|
4698
|
+
somethingAdded = true;
|
|
4699
|
+
}
|
|
4700
|
+
if (input.webDeploy && input.webDeploy !== "none") {
|
|
4701
|
+
await addDeploymentToProject({
|
|
4702
|
+
...input,
|
|
4703
|
+
install: false,
|
|
4704
|
+
suppressInstallMessage: true,
|
|
4705
|
+
webDeploy: input.webDeploy
|
|
4706
|
+
});
|
|
4707
|
+
somethingAdded = true;
|
|
4708
|
+
}
|
|
4709
|
+
if (!somethingAdded) {
|
|
4710
|
+
outro(pc.yellow("No addons or deployment configurations to add."));
|
|
4271
4711
|
return;
|
|
4272
4712
|
}
|
|
4273
|
-
await
|
|
4274
|
-
|
|
4275
|
-
|
|
4713
|
+
if (input.install) await installDependencies({
|
|
4714
|
+
projectDir,
|
|
4715
|
+
packageManager
|
|
4276
4716
|
});
|
|
4717
|
+
else log.info(pc.yellow(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`));
|
|
4718
|
+
outro(pc.green("Add command completed successfully!"));
|
|
4277
4719
|
} catch (error) {
|
|
4278
4720
|
console.error(error);
|
|
4279
4721
|
process.exit(1);
|
|
@@ -4363,7 +4805,8 @@ const router = t.router({
|
|
|
4363
4805
|
dbSetup: DatabaseSetupSchema.optional(),
|
|
4364
4806
|
backend: BackendSchema.optional(),
|
|
4365
4807
|
runtime: RuntimeSchema.optional(),
|
|
4366
|
-
api: APISchema.optional()
|
|
4808
|
+
api: APISchema.optional(),
|
|
4809
|
+
webDeploy: WebDeploySchema.optional()
|
|
4367
4810
|
}).optional().default({})])).mutation(async ({ input }) => {
|
|
4368
4811
|
const [projectName, options] = input;
|
|
4369
4812
|
const combinedInput = {
|
|
@@ -4372,10 +4815,11 @@ const router = t.router({
|
|
|
4372
4815
|
};
|
|
4373
4816
|
await createProjectHandler(combinedInput);
|
|
4374
4817
|
}),
|
|
4375
|
-
add: t.procedure.meta({ description: "Add addons to an existing Better-T Stack project" }).input(zod.tuple([zod.object({
|
|
4818
|
+
add: t.procedure.meta({ description: "Add addons or deployment configurations to an existing Better-T Stack project" }).input(zod.tuple([zod.object({
|
|
4376
4819
|
addons: zod.array(AddonsSchema).optional().default([]),
|
|
4820
|
+
webDeploy: WebDeploySchema.optional(),
|
|
4377
4821
|
projectDir: zod.string().optional(),
|
|
4378
|
-
install: zod.boolean().optional().default(false).describe("Install dependencies after adding addons"),
|
|
4822
|
+
install: zod.boolean().optional().default(false).describe("Install dependencies after adding addons or deployment"),
|
|
4379
4823
|
packageManager: PackageManagerSchema.optional()
|
|
4380
4824
|
}).optional().default({})])).mutation(async ({ input }) => {
|
|
4381
4825
|
const [options] = input;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"picocolors": "^1.1.1",
|
|
65
65
|
"posthog-node": "^5.1.1",
|
|
66
66
|
"trpc-cli": "^0.9.2",
|
|
67
|
+
"ts-morph": "^26.0.0",
|
|
67
68
|
"zod": "^3.25.67"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For more details on how to configure Wrangler, refer to:
|
|
3
|
+
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
|
4
|
+
*/
|
|
5
|
+
{
|
|
6
|
+
"$schema": "../../node_modules/wrangler/config-schema.json",
|
|
7
|
+
"name": "{{projectName}}",
|
|
8
|
+
"main": "./.output/server/index.mjs",
|
|
9
|
+
"compatibility_date": "2025-07-01",
|
|
10
|
+
"assets": {
|
|
11
|
+
"binding": "ASSETS",
|
|
12
|
+
"directory": "./.output/public/"
|
|
13
|
+
},
|
|
14
|
+
"observability": {
|
|
15
|
+
"enabled": true
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Smart Placement
|
|
19
|
+
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
|
20
|
+
*/
|
|
21
|
+
// "placement": { "mode": "smart" },
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bindings
|
|
25
|
+
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
|
26
|
+
* databases, object storage, AI inference, real-time communication and more.
|
|
27
|
+
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Environment Variables
|
|
32
|
+
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
|
33
|
+
*/
|
|
34
|
+
// "vars": { "MY_VARIABLE": "production_value" },
|
|
35
|
+
/**
|
|
36
|
+
* Note: Use secrets to store sensitive data.
|
|
37
|
+
* https://developers.cloudflare.com/workers/configuration/secrets/
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Static Assets
|
|
42
|
+
* https://developers.cloudflare.com/workers/static-assets/binding/
|
|
43
|
+
*/
|
|
44
|
+
// "assets": { "directory": "./public/", "binding": "ASSETS" },
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Service Bindings (communicate between multiple Workers)
|
|
48
|
+
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
|
49
|
+
*/
|
|
50
|
+
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
|
51
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineCloudflareConfig } from "@opennextjs/cloudflare/config";
|
|
2
|
+
// import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
|
|
3
|
+
|
|
4
|
+
export default defineCloudflareConfig({
|
|
5
|
+
// incrementalCache: r2IncrementalCache,
|
|
6
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../node_modules/wrangler/config-schema.json",
|
|
3
|
+
"main": ".open-next/worker.js",
|
|
4
|
+
"name": "{{projectName}}",
|
|
5
|
+
"compatibility_date": "2025-07-05",
|
|
6
|
+
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
|
|
7
|
+
"assets": {
|
|
8
|
+
"directory": ".open-next/assets",
|
|
9
|
+
"binding": "ASSETS"
|
|
10
|
+
},
|
|
11
|
+
// "r2_buckets": [
|
|
12
|
+
// // Use R2 incremental cache
|
|
13
|
+
// // See https://opennext.js.org/cloudflare/caching
|
|
14
|
+
// {
|
|
15
|
+
// "binding": "NEXT_INC_CACHE_R2_BUCKET",
|
|
16
|
+
// // Create the bucket before deploying
|
|
17
|
+
// // You can change the bucket name if you want
|
|
18
|
+
// // See https://developers.cloudflare.com/workers/wrangler/commands/#r2-bucket-create
|
|
19
|
+
// "bucket_name": "cache"
|
|
20
|
+
// }
|
|
21
|
+
// ]
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For more details on how to configure Wrangler, refer to:
|
|
3
|
+
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
|
4
|
+
*/
|
|
5
|
+
{
|
|
6
|
+
"$schema": "../../node_modules/wrangler/config-schema.json",
|
|
7
|
+
"name": "{{projectName}}",
|
|
8
|
+
"main": ".svelte-kit/cloudflare/_worker.js",
|
|
9
|
+
"compatibility_date": "2025-07-05",
|
|
10
|
+
"assets": {
|
|
11
|
+
"binding": "ASSETS",
|
|
12
|
+
"directory": ".svelte-kit/cloudflare"
|
|
13
|
+
},
|
|
14
|
+
"observability": {
|
|
15
|
+
"enabled": true
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Smart Placement
|
|
19
|
+
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
|
20
|
+
*/
|
|
21
|
+
// "placement": { "mode": "smart" },
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bindings
|
|
25
|
+
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
|
26
|
+
* databases, object storage, AI inference, real-time communication and more.
|
|
27
|
+
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Environment Variables
|
|
32
|
+
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
|
33
|
+
*/
|
|
34
|
+
// "vars": { "MY_VARIABLE": "production_value" },
|
|
35
|
+
/**
|
|
36
|
+
* Note: Use secrets to store sensitive data.
|
|
37
|
+
* https://developers.cloudflare.com/workers/configuration/secrets/
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Static Assets
|
|
42
|
+
* https://developers.cloudflare.com/workers/static-assets/binding/
|
|
43
|
+
*/
|
|
44
|
+
// "assets": { "directory": "./public/", "binding": "ASSETS" },
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Service Bindings (communicate between multiple Workers)
|
|
48
|
+
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
|
49
|
+
*/
|
|
50
|
+
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
|
51
|
+
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
{{#if (includes addons "pwa")}}
|
|
2
|
-
import { VitePWA } from "vite-plugin-pwa";
|
|
3
|
-
{{/if}}
|
|
4
1
|
import { reactRouter } from "@react-router/dev/vite";
|
|
5
2
|
import tailwindcss from "@tailwindcss/vite";
|
|
6
3
|
import { defineConfig } from "vite";
|
|
@@ -11,23 +8,5 @@ export default defineConfig({
|
|
|
11
8
|
tailwindcss(),
|
|
12
9
|
reactRouter(),
|
|
13
10
|
tsconfigPaths(),
|
|
14
|
-
{{#if (includes addons "pwa")}}
|
|
15
|
-
VitePWA({
|
|
16
|
-
registerType: "autoUpdate",
|
|
17
|
-
manifest: {
|
|
18
|
-
name: "{{projectName}}",
|
|
19
|
-
short_name: "{{projectName}}",
|
|
20
|
-
description: "{{projectName}} - PWA Application",
|
|
21
|
-
theme_color: "#0c0c0c",
|
|
22
|
-
},
|
|
23
|
-
pwaAssets: {
|
|
24
|
-
disabled: false,
|
|
25
|
-
config: true,
|
|
26
|
-
},
|
|
27
|
-
devOptions: {
|
|
28
|
-
enabled: true,
|
|
29
|
-
},
|
|
30
|
-
}),
|
|
31
|
-
{{/if}}
|
|
32
11
|
],
|
|
33
|
-
});
|
|
12
|
+
});
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
{{#if (includes addons "pwa")}}
|
|
2
|
-
import { VitePWA } from "vite-plugin-pwa";
|
|
3
|
-
{{/if}}
|
|
4
1
|
import tailwindcss from "@tailwindcss/vite";
|
|
5
|
-
import {
|
|
2
|
+
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
|
6
3
|
import react from "@vitejs/plugin-react";
|
|
7
4
|
import path from "node:path";
|
|
8
5
|
import { defineConfig } from "vite";
|
|
@@ -10,30 +7,12 @@ import { defineConfig } from "vite";
|
|
|
10
7
|
export default defineConfig({
|
|
11
8
|
plugins: [
|
|
12
9
|
tailwindcss(),
|
|
13
|
-
|
|
10
|
+
tanstackRouter({}),
|
|
14
11
|
react(),
|
|
15
|
-
{{#if (includes addons "pwa")}}
|
|
16
|
-
VitePWA({
|
|
17
|
-
registerType: "autoUpdate",
|
|
18
|
-
manifest: {
|
|
19
|
-
name: "{{projectName}}",
|
|
20
|
-
short_name: "{{projectName}}",
|
|
21
|
-
description: "{{projectName}} - PWA Application",
|
|
22
|
-
theme_color: "#0c0c0c",
|
|
23
|
-
},
|
|
24
|
-
pwaAssets: {
|
|
25
|
-
disabled: false,
|
|
26
|
-
config: true,
|
|
27
|
-
},
|
|
28
|
-
devOptions: {
|
|
29
|
-
enabled: true,
|
|
30
|
-
},
|
|
31
|
-
}),
|
|
32
|
-
{{/if}}
|
|
33
12
|
],
|
|
34
13
|
resolve: {
|
|
35
14
|
alias: {
|
|
36
15
|
"@": path.resolve(__dirname, "./src"),
|
|
37
16
|
},
|
|
38
17
|
},
|
|
39
|
-
});
|
|
18
|
+
});
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
"@tanstack/react-start": "^1.121.0-alpha.27",
|
|
18
18
|
"@tanstack/router-plugin": "^1.121.0",
|
|
19
19
|
"class-variance-authority": "^0.7.1",
|
|
20
|
-
|
|
21
|
-
"lucide-react": "^0.
|
|
20
|
+
"clsx": "^2.1.1",
|
|
21
|
+
"lucide-react": "^0.525.0",
|
|
22
22
|
"next-themes": "^0.4.6",
|
|
23
23
|
"react": "19.0.0",
|
|
24
24
|
"react-dom": "19.0.0",
|
|
25
25
|
"sonner": "^2.0.3",
|
|
26
26
|
"tailwindcss": "^4.1.3",
|
|
27
|
-
"tailwind-merge": "^
|
|
27
|
+
"tailwind-merge": "^3.3.1",
|
|
28
28
|
"tw-animate-css": "^1.2.5",
|
|
29
29
|
"vite-tsconfig-paths": "^5.1.4",
|
|
30
30
|
"zod": "^3.25.16"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@vitejs/plugin-react": "^4.5.2",
|
|
39
39
|
"jsdom": "^26.0.0",
|
|
40
40
|
"typescript": "^5.7.2",
|
|
41
|
-
"vite": "^
|
|
42
|
-
"web-vitals": "^
|
|
41
|
+
"vite": "^7.0.2",
|
|
42
|
+
"web-vitals": "^5.0.3"
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite --port 3001",
|
|
7
|
-
"build": "vite build
|
|
7
|
+
"build": "vite build",
|
|
8
8
|
"serve": "vite preview",
|
|
9
9
|
"test": "vitest run"
|
|
10
10
|
},
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"typescript": "^5.7.2",
|
|
24
|
-
"vite": "^
|
|
24
|
+
"vite": "^7.0.2",
|
|
25
25
|
"vite-plugin-solid": "^2.11.2"
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -4,7 +4,7 @@ import { routeTree } from "./routeTree.gen";
|
|
|
4
4
|
import "./styles.css";
|
|
5
5
|
{{#if (eq api "orpc")}}
|
|
6
6
|
import { QueryClientProvider } from "@tanstack/solid-query";
|
|
7
|
-
import { queryClient } from "./utils/orpc";
|
|
7
|
+
import { orpc, queryClient } from "./utils/orpc";
|
|
8
8
|
{{/if}}
|
|
9
9
|
|
|
10
10
|
const router = createRouter({
|
|
@@ -12,6 +12,9 @@ const router = createRouter({
|
|
|
12
12
|
defaultPreload: "intent",
|
|
13
13
|
scrollRestoration: true,
|
|
14
14
|
defaultPreloadStaleTime: 0,
|
|
15
|
+
{{#if (eq api "orpc")}}
|
|
16
|
+
context: { orpc, queryClient },
|
|
17
|
+
{{/if}}
|
|
15
18
|
});
|
|
16
19
|
|
|
17
20
|
declare module "@tanstack/solid-router" {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
|
3
|
+
import solidPlugin from "vite-plugin-solid";
|
|
4
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [
|
|
9
|
+
tanstackRouter({ target: "solid", autoCodeSplitting: true }),
|
|
10
|
+
solidPlugin(),
|
|
11
|
+
tailwindcss(),
|
|
12
|
+
],
|
|
13
|
+
resolve: {
|
|
14
|
+
alias: {
|
|
15
|
+
"@": path.resolve(__dirname, "./src"),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vite";
|
|
2
|
-
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
|
3
|
-
import solidPlugin from "vite-plugin-solid";
|
|
4
|
-
import tailwindcss from "@tailwindcss/vite";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
{{#if (includes addons "pwa")}}
|
|
7
|
-
import { VitePWA } from "vite-plugin-pwa";
|
|
8
|
-
{{/if}}
|
|
9
|
-
|
|
10
|
-
export default defineConfig({
|
|
11
|
-
plugins: [
|
|
12
|
-
TanStackRouterVite({ target: "solid", autoCodeSplitting: true }),
|
|
13
|
-
solidPlugin(),
|
|
14
|
-
tailwindcss(),
|
|
15
|
-
{{#if (includes addons "pwa")}}
|
|
16
|
-
VitePWA({
|
|
17
|
-
registerType: "autoUpdate",
|
|
18
|
-
manifest: {
|
|
19
|
-
name: "{{projectName}}",
|
|
20
|
-
short_name: "{{projectName}}",
|
|
21
|
-
description: "{{projectName}} - PWA Application",
|
|
22
|
-
theme_color: "#0c0c0c",
|
|
23
|
-
},
|
|
24
|
-
pwaAssets: {
|
|
25
|
-
disabled: false,
|
|
26
|
-
config: true,
|
|
27
|
-
},
|
|
28
|
-
devOptions: {
|
|
29
|
-
enabled: true,
|
|
30
|
-
},
|
|
31
|
-
}),
|
|
32
|
-
{{/if}}
|
|
33
|
-
],
|
|
34
|
-
resolve: {
|
|
35
|
-
alias: {
|
|
36
|
-
"@": path.resolve(__dirname, "./src"),
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
});
|
|
File without changes
|