create-better-t-stack 2.33.8-canary.98a850ad → 2.33.9-canary.20c8e952
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/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{src-D0MPVT22.js → src-CpB-qAME.js} +249 -113
- package/package.json +1 -1
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -5,7 +5,6 @@ import { createCli, trpcServer } from "trpc-cli";
|
|
|
5
5
|
import z from "zod";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import consola, { consola as consola$1 } from "consola";
|
|
8
|
-
import * as fs$1 from "fs-extra";
|
|
9
8
|
import fs from "fs-extra";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
11
10
|
import gradient from "gradient-string";
|
|
@@ -30,9 +29,8 @@ const getUserPkgManager = () => {
|
|
|
30
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
30
|
const distPath = path.dirname(__filename);
|
|
32
31
|
const PKG_ROOT = path.join(distPath, "../");
|
|
33
|
-
const
|
|
32
|
+
const DEFAULT_CONFIG_BASE = {
|
|
34
33
|
projectName: "my-better-t-app",
|
|
35
|
-
projectDir: path.resolve(process.cwd(), "my-better-t-app"),
|
|
36
34
|
relativePath: "my-better-t-app",
|
|
37
35
|
frontend: ["tanstack-router"],
|
|
38
36
|
database: "sqlite",
|
|
@@ -41,7 +39,6 @@ const DEFAULT_CONFIG = {
|
|
|
41
39
|
addons: ["turborepo"],
|
|
42
40
|
examples: [],
|
|
43
41
|
git: true,
|
|
44
|
-
packageManager: getUserPkgManager(),
|
|
45
42
|
install: true,
|
|
46
43
|
dbSetup: "none",
|
|
47
44
|
backend: "hono",
|
|
@@ -50,6 +47,17 @@ const DEFAULT_CONFIG = {
|
|
|
50
47
|
webDeploy: "none",
|
|
51
48
|
serverDeploy: "none"
|
|
52
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();
|
|
53
61
|
const dependencyVersionMap = {
|
|
54
62
|
"better-auth": "^1.3.4",
|
|
55
63
|
"@better-auth/expo": "^1.3.4",
|
|
@@ -137,7 +145,8 @@ const ADDON_COMPATIBILITY = {
|
|
|
137
145
|
"react-router",
|
|
138
146
|
"nuxt",
|
|
139
147
|
"svelte",
|
|
140
|
-
"solid"
|
|
148
|
+
"solid",
|
|
149
|
+
"next"
|
|
141
150
|
],
|
|
142
151
|
biome: [],
|
|
143
152
|
husky: [],
|
|
@@ -565,6 +574,20 @@ function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
|
|
|
565
574
|
function validateServerDeployRequiresBackend(serverDeploy, backend) {
|
|
566
575
|
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
|
|
567
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
|
+
}
|
|
568
591
|
|
|
569
592
|
//#endregion
|
|
570
593
|
//#region src/prompts/api.ts
|
|
@@ -1298,9 +1321,9 @@ async function getProjectName(initialName) {
|
|
|
1298
1321
|
|
|
1299
1322
|
//#endregion
|
|
1300
1323
|
//#region src/utils/get-latest-cli-version.ts
|
|
1301
|
-
const getLatestCLIVersion =
|
|
1324
|
+
const getLatestCLIVersion = () => {
|
|
1302
1325
|
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
|
1303
|
-
const packageJsonContent =
|
|
1326
|
+
const packageJsonContent = fs.readJSONSync(packageJsonPath);
|
|
1304
1327
|
return packageJsonContent.version ?? "1.0.0";
|
|
1305
1328
|
};
|
|
1306
1329
|
|
|
@@ -1562,7 +1585,7 @@ const renderTitle = () => {
|
|
|
1562
1585
|
};
|
|
1563
1586
|
|
|
1564
1587
|
//#endregion
|
|
1565
|
-
//#region src/
|
|
1588
|
+
//#region src/utils/config-processing.ts
|
|
1566
1589
|
function processArrayOption(options) {
|
|
1567
1590
|
if (!options || options.length === 0) return [];
|
|
1568
1591
|
if (options.includes("none")) return [];
|
|
@@ -1573,22 +1596,10 @@ function deriveProjectName(projectName, projectDirectory) {
|
|
|
1573
1596
|
if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
|
|
1574
1597
|
return "";
|
|
1575
1598
|
}
|
|
1576
|
-
function
|
|
1577
|
-
const result = ProjectNameSchema.safeParse(name);
|
|
1578
|
-
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1579
|
-
}
|
|
1580
|
-
function processAndValidateFlags(options, providedFlags, projectName) {
|
|
1599
|
+
function processFlags(options, projectName) {
|
|
1581
1600
|
const config = {};
|
|
1582
|
-
if (options.api)
|
|
1583
|
-
config.api = options.api;
|
|
1584
|
-
if (options.api === "none") {
|
|
1585
|
-
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.");
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1601
|
+
if (options.api) config.api = options.api;
|
|
1588
1602
|
if (options.backend) config.backend = options.backend;
|
|
1589
|
-
if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
|
|
1590
|
-
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.`);
|
|
1591
|
-
}
|
|
1592
1603
|
if (options.database) config.database = options.database;
|
|
1593
1604
|
if (options.orm) config.orm = options.orm;
|
|
1594
1605
|
if (options.auth !== void 0) config.auth = options.auth;
|
|
@@ -1600,70 +1611,180 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1600
1611
|
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1601
1612
|
if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
|
|
1602
1613
|
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
|
1603
|
-
if (derivedName)
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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
|
+
}
|
|
1627
1688
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
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.");
|
|
1694
|
+
}
|
|
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) {
|
|
1632
1699
|
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
|
1633
1700
|
if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1634
1701
|
}
|
|
1635
1702
|
coerceBackendPresets(config);
|
|
1636
1703
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
if (
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") exitWithError("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1646
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") exitWithError("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1647
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") exitWithError("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
|
|
1648
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") exitWithError("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1649
|
-
if (config.dbSetup === "d1") {
|
|
1650
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
|
|
1651
|
-
if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1652
|
-
}
|
|
1653
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
|
|
1654
|
-
if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
|
|
1655
|
-
}
|
|
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);
|
|
1656
1712
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1713
|
+
}
|
|
1714
|
+
function validateApiConstraints(config, options) {
|
|
1715
|
+
if (config.api === "none") {
|
|
1716
|
+
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
function validateFullConfig(config, providedFlags, options) {
|
|
1720
|
+
validateDatabaseOrmAuth(config, providedFlags);
|
|
1721
|
+
validateDatabaseSetup(config, providedFlags);
|
|
1722
|
+
validateBackendConstraints(config, providedFlags, options);
|
|
1723
|
+
validateFrontendConstraints(config, providedFlags);
|
|
1724
|
+
validateApiConstraints(config, options);
|
|
1662
1725
|
validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
|
1726
|
+
validateWorkersCompatibility(providedFlags, options, config);
|
|
1727
|
+
if (config.addons && config.addons.length > 0) {
|
|
1728
|
+
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
|
1729
|
+
config.addons = [...new Set(config.addons)];
|
|
1730
|
+
}
|
|
1731
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
1732
|
+
}
|
|
1733
|
+
function validateConfigForProgrammaticUse(config) {
|
|
1734
|
+
try {
|
|
1735
|
+
validateDatabaseOrmAuth(config);
|
|
1736
|
+
if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
|
|
1737
|
+
validateApiFrontendCompatibility(config.api, config.frontend);
|
|
1738
|
+
if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend);
|
|
1739
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
if (error instanceof Error) throw error;
|
|
1742
|
+
throw new Error(String(error));
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
//#endregion
|
|
1747
|
+
//#region src/utils/project-name-validation.ts
|
|
1748
|
+
function validateProjectName(name) {
|
|
1749
|
+
const result = ProjectNameSchema.safeParse(name);
|
|
1750
|
+
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1751
|
+
}
|
|
1752
|
+
function validateProjectNameThrow(name) {
|
|
1753
|
+
const result = ProjectNameSchema.safeParse(name);
|
|
1754
|
+
if (!result.success) throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
|
|
1755
|
+
}
|
|
1756
|
+
function extractAndValidateProjectName(projectName, projectDirectory, throwOnError = false) {
|
|
1757
|
+
const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
|
|
1758
|
+
if (!derivedName) return "";
|
|
1759
|
+
const nameToValidate = projectName ? path.basename(projectName) : derivedName;
|
|
1760
|
+
if (throwOnError) validateProjectNameThrow(nameToValidate);
|
|
1761
|
+
else validateProjectName(nameToValidate);
|
|
1762
|
+
return projectName || derivedName;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/validation.ts
|
|
1767
|
+
function processAndValidateFlags(options, providedFlags, projectName) {
|
|
1768
|
+
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);
|
|
1663
1777
|
return config;
|
|
1664
1778
|
}
|
|
1665
|
-
function
|
|
1666
|
-
|
|
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);
|
|
1667
1788
|
}
|
|
1668
1789
|
|
|
1669
1790
|
//#endregion
|
|
@@ -2216,20 +2337,16 @@ async function setupDeploymentTemplates(projectDir, context) {
|
|
|
2216
2337
|
}
|
|
2217
2338
|
} else {
|
|
2218
2339
|
if (context.webDeploy === "alchemy") {
|
|
2340
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2219
2341
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
2220
|
-
if (await fs.pathExists(webAppDir))
|
|
2221
|
-
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2222
|
-
if (await fs.pathExists(alchemyTemplateSrc)) await processAndCopyFiles("**/*", alchemyTemplateSrc, webAppDir, context);
|
|
2223
|
-
}
|
|
2342
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
|
|
2224
2343
|
}
|
|
2225
2344
|
if (context.serverDeploy === "alchemy") {
|
|
2345
|
+
const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
|
|
2226
2346
|
const serverAppDir = path.join(projectDir, "apps/server");
|
|
2227
|
-
if (await fs.pathExists(serverAppDir)) {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2231
|
-
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2232
|
-
}
|
|
2347
|
+
if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
|
|
2348
|
+
await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2349
|
+
await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
|
|
2233
2350
|
}
|
|
2234
2351
|
}
|
|
2235
2352
|
}
|
|
@@ -2501,7 +2618,7 @@ async function setupUltracite(config, hasHusky) {
|
|
|
2501
2618
|
];
|
|
2502
2619
|
if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
|
|
2503
2620
|
if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
|
|
2504
|
-
if (hasHusky) ultraciteArgs.push("--
|
|
2621
|
+
if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
|
|
2505
2622
|
const ultraciteArgsString = ultraciteArgs.join(" ");
|
|
2506
2623
|
const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
|
|
2507
2624
|
const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
@@ -3342,15 +3459,15 @@ async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
|
|
|
3342
3459
|
//#region src/helpers/deployment/workers/workers-next-setup.ts
|
|
3343
3460
|
async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
3344
3461
|
const webAppDir = path.join(projectDir, "apps/web");
|
|
3345
|
-
if (!await fs
|
|
3462
|
+
if (!await fs.pathExists(webAppDir)) return;
|
|
3346
3463
|
await addPackageDependency({
|
|
3347
3464
|
dependencies: ["@opennextjs/cloudflare"],
|
|
3348
3465
|
devDependencies: ["wrangler"],
|
|
3349
3466
|
projectDir: webAppDir
|
|
3350
3467
|
});
|
|
3351
3468
|
const packageJsonPath = path.join(webAppDir, "package.json");
|
|
3352
|
-
if (await fs
|
|
3353
|
-
const pkg = await fs
|
|
3469
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
3470
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
3354
3471
|
pkg.scripts = {
|
|
3355
3472
|
...pkg.scripts,
|
|
3356
3473
|
preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
@@ -3358,7 +3475,7 @@ async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
|
3358
3475
|
upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
|
3359
3476
|
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
3360
3477
|
};
|
|
3361
|
-
await fs
|
|
3478
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
3362
3479
|
}
|
|
3363
3480
|
}
|
|
3364
3481
|
|
|
@@ -5471,13 +5588,14 @@ async function displayPostInstallInstructions(config) {
|
|
|
5471
5588
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
5472
5589
|
const cdCmd = `cd ${relativePath}`;
|
|
5473
5590
|
const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
|
|
5474
|
-
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
|
|
5591
|
+
const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
|
|
5475
5592
|
const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
|
|
5476
5593
|
const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
|
|
5477
5594
|
const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
|
|
5478
5595
|
const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
|
|
5479
5596
|
const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
|
|
5480
|
-
const
|
|
5597
|
+
const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
5598
|
+
const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
|
|
5481
5599
|
const hasWeb = frontend?.some((f) => [
|
|
5482
5600
|
"tanstack-router",
|
|
5483
5601
|
"react-router",
|
|
@@ -5506,8 +5624,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
5506
5624
|
if (runtime === "workers") {
|
|
5507
5625
|
if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
|
|
5508
5626
|
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
|
|
5509
|
-
if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n
|
|
5510
|
-
}
|
|
5627
|
+
if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
|
|
5628
|
+
}
|
|
5511
5629
|
}
|
|
5512
5630
|
output += `${pc.bold("Your project will be available at:")}\n`;
|
|
5513
5631
|
if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
|
|
@@ -5520,7 +5638,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
5520
5638
|
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
|
5521
5639
|
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
|
|
5522
5640
|
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
|
|
5523
|
-
if (
|
|
5641
|
+
if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
|
|
5642
|
+
if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
|
|
5524
5643
|
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
|
|
5525
5644
|
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
|
|
5526
5645
|
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
|
|
@@ -5541,7 +5660,7 @@ function getNativeInstructions(isConvex) {
|
|
|
5541
5660
|
function getLintingInstructions(runCmd) {
|
|
5542
5661
|
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
|
|
5543
5662
|
}
|
|
5544
|
-
async function getDatabaseInstructions(database, orm, runCmd,
|
|
5663
|
+
async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
|
|
5545
5664
|
const instructions = [];
|
|
5546
5665
|
if (dbSetup === "docker") {
|
|
5547
5666
|
const dockerStatus = await getDockerStatus(database);
|
|
@@ -5550,7 +5669,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
5550
5669
|
instructions.push("");
|
|
5551
5670
|
}
|
|
5552
5671
|
}
|
|
5553
|
-
if (
|
|
5672
|
+
if (serverDeploy === "wrangler" && dbSetup === "d1") {
|
|
5554
5673
|
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
|
|
5555
5674
|
instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
|
|
5556
5675
|
instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
|
|
@@ -5558,8 +5677,8 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
5558
5677
|
instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white(`cd apps/server && ${packageManager} db:generate`)}`);
|
|
5559
5678
|
instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
|
|
5560
5679
|
instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
|
|
5561
|
-
instructions.push("");
|
|
5562
5680
|
}
|
|
5681
|
+
if (dbSetup === "d1" && serverDeploy === "alchemy") instructions.push(`${pc.cyan("•")} Generate migrations: ${pc.white(`${runCmd} db:generate`)}`);
|
|
5563
5682
|
if (orm === "prisma") {
|
|
5564
5683
|
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`);
|
|
5565
5684
|
if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
|
|
@@ -5568,7 +5687,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
|
|
5568
5687
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
|
5569
5688
|
} else if (orm === "drizzle") {
|
|
5570
5689
|
if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
|
|
5571
|
-
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
5690
|
+
if (dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
|
5572
5691
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
|
5573
5692
|
if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
|
|
5574
5693
|
} else if (orm === "mongoose") {
|
|
@@ -5591,8 +5710,18 @@ function getNoOrmWarning() {
|
|
|
5591
5710
|
function getBunWebNativeWarning() {
|
|
5592
5711
|
return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
|
|
5593
5712
|
}
|
|
5594
|
-
function
|
|
5595
|
-
|
|
5713
|
+
function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5714
|
+
const instructions = [];
|
|
5715
|
+
if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
|
|
5716
|
+
if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
|
|
5717
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
5718
|
+
}
|
|
5719
|
+
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
|
|
5720
|
+
const instructions = [];
|
|
5721
|
+
if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
|
|
5722
|
+
else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
|
|
5723
|
+
else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
|
|
5724
|
+
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
5596
5725
|
}
|
|
5597
5726
|
|
|
5598
5727
|
//#endregion
|
|
@@ -5805,6 +5934,7 @@ async function createProject(options) {
|
|
|
5805
5934
|
if (!isConvex && options.auth) await setupAuth(options);
|
|
5806
5935
|
await handleExtras(projectDir, options);
|
|
5807
5936
|
await setupWebDeploy(options);
|
|
5937
|
+
await setupServerDeploy(options);
|
|
5808
5938
|
await setupEnvironmentVariables(options);
|
|
5809
5939
|
await updatePackageConfigurations(projectDir, options);
|
|
5810
5940
|
await createReadme(projectDir, options);
|
|
@@ -5844,10 +5974,11 @@ async function createProjectHandler(input) {
|
|
|
5844
5974
|
let currentPathInput;
|
|
5845
5975
|
if (input.yes && input.projectName) currentPathInput = input.projectName;
|
|
5846
5976
|
else if (input.yes) {
|
|
5847
|
-
|
|
5977
|
+
const defaultConfig = getDefaultConfig();
|
|
5978
|
+
let defaultName = defaultConfig.relativePath;
|
|
5848
5979
|
let counter = 1;
|
|
5849
5980
|
while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
|
|
5850
|
-
defaultName = `${
|
|
5981
|
+
defaultName = `${defaultConfig.projectName}-${counter}`;
|
|
5851
5982
|
counter++;
|
|
5852
5983
|
}
|
|
5853
5984
|
currentPathInput = defaultName;
|
|
@@ -5902,28 +6033,33 @@ async function createProjectHandler(input) {
|
|
|
5902
6033
|
projectDirectory: input.projectName
|
|
5903
6034
|
};
|
|
5904
6035
|
const providedFlags = getProvidedFlags(cliInput);
|
|
5905
|
-
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
5906
|
-
const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
|
|
5907
|
-
if (!input.yes && Object.keys(otherFlags).length > 0) {
|
|
5908
|
-
log.info(pc.yellow("Using these pre-selected options:"));
|
|
5909
|
-
log.message(displayConfig(otherFlags));
|
|
5910
|
-
log.message("");
|
|
5911
|
-
}
|
|
5912
6036
|
let config;
|
|
5913
6037
|
if (input.yes) {
|
|
6038
|
+
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
5914
6039
|
config = {
|
|
5915
|
-
...
|
|
6040
|
+
...getDefaultConfig(),
|
|
5916
6041
|
...flagConfig,
|
|
5917
6042
|
projectName: finalBaseName,
|
|
5918
6043
|
projectDir: finalResolvedPath,
|
|
5919
6044
|
relativePath: finalPathInput
|
|
5920
6045
|
};
|
|
6046
|
+
coerceBackendPresets(config);
|
|
6047
|
+
validateConfigCompatibility(config, providedFlags, cliInput);
|
|
5921
6048
|
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");
|
|
5922
6049
|
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");
|
|
5923
6050
|
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
|
5924
6051
|
log.message(displayConfig(config));
|
|
5925
6052
|
log.message("");
|
|
5926
|
-
} else
|
|
6053
|
+
} else {
|
|
6054
|
+
const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
|
|
6055
|
+
const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
|
|
6056
|
+
if (Object.keys(otherFlags).length > 0) {
|
|
6057
|
+
log.info(pc.yellow("Using these pre-selected options:"));
|
|
6058
|
+
log.message(displayConfig(otherFlags));
|
|
6059
|
+
log.message("");
|
|
6060
|
+
}
|
|
6061
|
+
config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
|
|
6062
|
+
}
|
|
5927
6063
|
await createProject(config);
|
|
5928
6064
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
5929
6065
|
log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.33.
|
|
3
|
+
"version": "2.33.9-canary.20c8e952",
|
|
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",
|