create-better-fullstack 2.0.3 → 2.1.1
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/{add-handler-BQN6pwGJ.mjs → add-handler-9F-AsGM-.mjs} +11 -9
- package/dist/addons-setup-C8eaCaH5.mjs +5 -0
- package/dist/{addons-setup-HSghQS7c.mjs → addons-setup-C_xrNtkL.mjs} +3 -83
- package/dist/cli.mjs +2 -2
- package/dist/compatibility-rules-D7zYNVjC.mjs +372 -0
- package/dist/config-validation-C4glouQh.mjs +1512 -0
- package/dist/doctor-BFSSbS-U.mjs +280 -0
- package/dist/{bts-config-Bg1Qea9Y.mjs → errors-D9yiiGVq.mjs} +215 -72
- package/dist/generated-checks-DUvVXWId.mjs +91 -0
- package/dist/index.d.mts +85 -29
- package/dist/index.mjs +11 -6
- package/dist/{install-dependencies-D0Z1dZEx.mjs → install-dependencies-DDGF-zDG.mjs} +102 -397
- package/dist/mcp-BXLhb7wW.mjs +7 -0
- package/dist/mcp-entry.mjs +1802 -296
- package/dist/render-title-zvyKC1ej.mjs +34 -0
- package/dist/run-BmRKR2wG.mjs +11 -0
- package/dist/{run-BhcXBjxh.mjs → run-DRzP53v3.mjs} +534 -1717
- package/package.json +5 -4
- package/dist/addons-setup-CV5uZyhH.mjs +0 -5
- package/dist/mcp-58r70ZcL.mjs +0 -5
- package/dist/run-DCxVLUMS.mjs +0 -7
- /package/dist/{update-deps-CLebIM70.mjs → update-deps-D5OG0KmJ.mjs} +0 -0
|
@@ -0,0 +1,1512 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { A as types_exports, c as isSilent, i as exitWithError, k as hasGraphPart } from "./errors-D9yiiGVq.mjs";
|
|
3
|
+
import { C as validateWebDeployFrontendTemplates, D as incompatibilityError, E as constraintError, O as missingRequirementError, S as validateUILibraryFrontendCompatibility, T as validateWorkersCompatibility, _ as validateExamplesCompatibility, b as validateServerDeployRequiresBackend, g as validateApiFrontendCompatibility, h as validateAddonsAgainstFrontends, n as ensureSingleWebAndNative, p as validateAIFrontendCompatibility, u as isWebFrontend, v as validatePaymentsCompatibility, w as validateWebDeployRequiresWebFrontend, x as validateUILibraryCSSFrameworkCompatibility, y as validateSelfBackendCompatibility } from "./compatibility-rules-D7zYNVjC.mjs";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
import consola from "consola";
|
|
7
|
+
|
|
8
|
+
//#region src/create-command-input.ts
|
|
9
|
+
const CreateCommandOptionsSchema = z.object({
|
|
10
|
+
template: types_exports.TemplateSchema.optional().describe("Use a predefined template"),
|
|
11
|
+
fromHistory: z.number().optional().describe("Replay the stack of the Nth most-recent project from history (1 = most recent)"),
|
|
12
|
+
config: z.string().optional().describe("Path to a bts.jsonc/JSON config file to use as the base stack"),
|
|
13
|
+
yes: z.boolean().optional().default(false).describe("Use default configuration"),
|
|
14
|
+
yolo: z.boolean().optional().default(false).describe("(WARNING - NOT RECOMMENDED) Bypass validations and compatibility checks"),
|
|
15
|
+
part: z.array(z.string()).optional().describe("Stack graph part binding, e.g. frontend:typescript:next or backend.orm:go:gorm"),
|
|
16
|
+
verbose: z.boolean().optional().default(false).describe("Show detailed result information"),
|
|
17
|
+
dryRun: z.boolean().optional().default(false).describe("Preview generated file tree without writing to disk"),
|
|
18
|
+
verify: z.boolean().optional().default(false).describe("Run generated project checks after scaffolding without starting dev servers"),
|
|
19
|
+
ecosystem: types_exports.EcosystemSchema.optional().describe("Language ecosystem (typescript, react-native, rust, python, go, java, or elixir)"),
|
|
20
|
+
database: types_exports.DatabaseSchema.optional(),
|
|
21
|
+
orm: types_exports.ORMSchema.optional(),
|
|
22
|
+
auth: types_exports.AuthSchema.optional(),
|
|
23
|
+
payments: types_exports.PaymentsSchema.optional(),
|
|
24
|
+
email: types_exports.EmailSchema.optional(),
|
|
25
|
+
fileUpload: types_exports.FileUploadSchema.optional(),
|
|
26
|
+
effect: types_exports.EffectSchema.optional(),
|
|
27
|
+
stateManagement: types_exports.StateManagementSchema.optional(),
|
|
28
|
+
validation: types_exports.ValidationSchema.optional(),
|
|
29
|
+
forms: types_exports.FormsSchema.optional(),
|
|
30
|
+
testing: types_exports.TestingSchema.optional(),
|
|
31
|
+
ai: types_exports.AISchema.optional(),
|
|
32
|
+
realtime: types_exports.RealtimeSchema.optional(),
|
|
33
|
+
jobQueue: types_exports.JobQueueSchema.optional(),
|
|
34
|
+
animation: types_exports.AnimationSchema.optional(),
|
|
35
|
+
logging: types_exports.LoggingSchema.optional(),
|
|
36
|
+
observability: types_exports.ObservabilitySchema.optional(),
|
|
37
|
+
featureFlags: types_exports.FeatureFlagsSchema.optional().describe("Feature flags provider"),
|
|
38
|
+
analytics: types_exports.AnalyticsSchema.optional().describe("Privacy-focused analytics"),
|
|
39
|
+
cms: types_exports.CMSSchema.optional().describe("Headless CMS solution"),
|
|
40
|
+
caching: types_exports.CachingSchema.optional().describe("Caching solution"),
|
|
41
|
+
rateLimit: types_exports.RateLimitSchema.optional().describe("Rate limiting solution"),
|
|
42
|
+
i18n: types_exports.I18nSchema.optional().describe("Internationalization (i18n) library"),
|
|
43
|
+
search: types_exports.SearchSchema.optional().describe("Search engine solution"),
|
|
44
|
+
vectorDb: types_exports.VectorDbSchema.optional().describe("Vector database for AI embeddings (pgvector, qdrant, chroma, pinecone)"),
|
|
45
|
+
fileStorage: types_exports.FileStorageSchema.optional().describe("File storage solution (S3, R2)"),
|
|
46
|
+
mobileNavigation: types_exports.MobileNavigationSchema.optional().describe("Mobile navigation (expo-router, react-navigation)"),
|
|
47
|
+
mobileUI: types_exports.MobileUISchema.optional().describe("Mobile UI (tamagui, gluestack-ui, uniwind, unistyles)"),
|
|
48
|
+
mobileStorage: types_exports.MobileStorageSchema.optional().describe("Mobile storage (mmkv)"),
|
|
49
|
+
mobileTesting: types_exports.MobileTestingSchema.optional().describe("Mobile testing (maestro, react-native-testing-library)"),
|
|
50
|
+
mobilePush: types_exports.MobilePushSchema.optional().describe("Mobile push notifications (expo-notifications)"),
|
|
51
|
+
mobileOTA: types_exports.MobileOTASchema.optional().describe("Mobile OTA updates (expo-updates)"),
|
|
52
|
+
mobileDeepLinking: types_exports.MobileDeepLinkingSchema.optional().describe("Mobile deep linking (expo-linking)"),
|
|
53
|
+
frontend: z.array(types_exports.FrontendSchema).optional(),
|
|
54
|
+
astroIntegration: types_exports.AstroIntegrationSchema.optional().describe("Astro UI framework integration (react, vue, svelte, solid)"),
|
|
55
|
+
addons: z.array(types_exports.AddonsSchema).optional(),
|
|
56
|
+
examples: z.array(types_exports.ExamplesSchema).optional(),
|
|
57
|
+
git: z.boolean().optional(),
|
|
58
|
+
packageManager: types_exports.PackageManagerSchema.optional(),
|
|
59
|
+
install: z.boolean().optional(),
|
|
60
|
+
versionChannel: types_exports.VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
|
|
61
|
+
dbSetup: types_exports.DatabaseSetupSchema.optional(),
|
|
62
|
+
backend: types_exports.BackendSchema.optional(),
|
|
63
|
+
runtime: types_exports.RuntimeSchema.optional(),
|
|
64
|
+
api: types_exports.APISchema.optional(),
|
|
65
|
+
cssFramework: types_exports.CSSFrameworkSchema.optional(),
|
|
66
|
+
uiLibrary: types_exports.UILibrarySchema.optional(),
|
|
67
|
+
shadcnBase: types_exports.ShadcnBaseSchema.optional().describe("shadcn/ui headless library (radix, base)"),
|
|
68
|
+
shadcnStyle: types_exports.ShadcnStyleSchema.optional().describe("shadcn/ui visual style (vega, nova, maia, lyra, mira)"),
|
|
69
|
+
shadcnIconLibrary: types_exports.ShadcnIconLibrarySchema.optional().describe("shadcn/ui icon library (lucide, tabler, hugeicons, phosphor, remixicon)"),
|
|
70
|
+
shadcnColorTheme: types_exports.ShadcnColorThemeSchema.optional().describe("shadcn/ui color theme (neutral, blue, violet, etc.)"),
|
|
71
|
+
shadcnBaseColor: types_exports.ShadcnBaseColorSchema.optional().describe("shadcn/ui base neutral color (neutral, stone, zinc, gray)"),
|
|
72
|
+
shadcnFont: types_exports.ShadcnFontSchema.optional().describe("shadcn/ui font (inter, geist, figtree, etc.)"),
|
|
73
|
+
shadcnRadius: types_exports.ShadcnRadiusSchema.optional().describe("shadcn/ui border radius (default, none, small, medium, large)"),
|
|
74
|
+
webDeploy: types_exports.WebDeploySchema.optional(),
|
|
75
|
+
serverDeploy: types_exports.ServerDeploySchema.optional(),
|
|
76
|
+
directoryConflict: types_exports.DirectoryConflictSchema.optional(),
|
|
77
|
+
renderTitle: z.boolean().optional(),
|
|
78
|
+
disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics"),
|
|
79
|
+
manualDb: z.boolean().optional().default(false).describe("Skip automatic/manual database setup prompt and use manual setup"),
|
|
80
|
+
rustWebFramework: types_exports.RustWebFrameworkSchema.optional().describe("Rust web framework (axum, actix-web)"),
|
|
81
|
+
rustFrontend: types_exports.RustFrontendSchema.optional().describe("Rust WASM frontend (leptos, dioxus)"),
|
|
82
|
+
rustOrm: types_exports.RustOrmSchema.optional().describe("Rust ORM/database (sea-orm, sqlx)"),
|
|
83
|
+
rustApi: types_exports.RustApiSchema.optional().describe("Rust API layer (tonic, async-graphql)"),
|
|
84
|
+
rustCli: types_exports.RustCliSchema.optional().describe("Rust CLI tools (clap, ratatui)"),
|
|
85
|
+
rustLibraries: z.array(types_exports.RustLibrariesSchema).optional().describe("Rust core libraries"),
|
|
86
|
+
rustLogging: types_exports.RustLoggingSchema.optional().describe("Rust logging (tracing, env-logger)"),
|
|
87
|
+
rustErrorHandling: types_exports.RustErrorHandlingSchema.optional().describe("Rust error handling (anyhow-thiserror, eyre)"),
|
|
88
|
+
rustCaching: types_exports.RustCachingSchema.optional().describe("Rust caching (moka, redis)"),
|
|
89
|
+
rustAuth: types_exports.RustAuthSchema.optional().describe("Rust auth (oauth2)"),
|
|
90
|
+
rustRealtime: types_exports.RustRealtimeSchema.optional().describe("Rust realtime (tokio-tungstenite)"),
|
|
91
|
+
rustMessageQueue: types_exports.RustMessageQueueSchema.optional().describe("Rust message queue (lapin)"),
|
|
92
|
+
rustObservability: types_exports.RustObservabilitySchema.optional().describe("Rust observability (opentelemetry)"),
|
|
93
|
+
rustTemplating: types_exports.RustTemplatingSchema.optional().describe("Rust templating (askama, tera)"),
|
|
94
|
+
pythonWebFramework: types_exports.PythonWebFrameworkSchema.optional().describe("Python web framework (fastapi, django)"),
|
|
95
|
+
pythonOrm: types_exports.PythonOrmSchema.optional().describe("Python ORM/database (sqlalchemy, sqlmodel)"),
|
|
96
|
+
pythonValidation: types_exports.PythonValidationSchema.optional().describe("Python validation (pydantic)"),
|
|
97
|
+
pythonAi: z.array(types_exports.PythonAiSchema).optional().describe("Python AI/ML frameworks"),
|
|
98
|
+
pythonAuth: types_exports.PythonAuthSchema.optional().describe("Python auth library (authlib, jwt)"),
|
|
99
|
+
pythonApi: types_exports.PythonApiSchema.optional().describe("Python API framework (django-rest-framework, django-ninja)"),
|
|
100
|
+
pythonTaskQueue: types_exports.PythonTaskQueueSchema.optional().describe("Python task queue (celery)"),
|
|
101
|
+
pythonGraphql: types_exports.PythonGraphqlSchema.optional().describe("Python GraphQL framework (strawberry)"),
|
|
102
|
+
pythonQuality: types_exports.PythonQualitySchema.optional().describe("Python code quality (ruff, mypy, pyright)"),
|
|
103
|
+
pythonTesting: z.array(types_exports.PythonTestingSchema).optional().describe("Python testing libraries (pytest, hypothesis)"),
|
|
104
|
+
pythonCaching: types_exports.PythonCachingSchema.optional().describe("Python caching (redis, aiocache)"),
|
|
105
|
+
pythonRealtime: types_exports.PythonRealtimeSchema.optional().describe("Python realtime (python-socketio, websockets)"),
|
|
106
|
+
pythonObservability: types_exports.PythonObservabilitySchema.optional().describe("Python observability (opentelemetry)"),
|
|
107
|
+
pythonCli: z.array(types_exports.PythonCliSchema).optional().describe("Python CLI tooling (typer, click, rich)"),
|
|
108
|
+
goWebFramework: types_exports.GoWebFrameworkSchema.optional().describe("Go web framework (gin, echo, fiber)"),
|
|
109
|
+
goOrm: types_exports.GoOrmSchema.optional().describe("Go ORM/database (gorm, sqlc)"),
|
|
110
|
+
goApi: types_exports.GoApiSchema.optional().describe("Go API layer (grpc-go)"),
|
|
111
|
+
goCli: types_exports.GoCliSchema.optional().describe("Go CLI tools (cobra, bubbletea, urfave-cli)"),
|
|
112
|
+
goLogging: types_exports.GoLoggingSchema.optional().describe("Go logging (zap, zerolog, slog)"),
|
|
113
|
+
goAuth: types_exports.GoAuthSchema.optional().describe("Go auth (casbin, jwt, goth)"),
|
|
114
|
+
goTesting: z.array(types_exports.GoTestingSchema).optional().describe("Go testing libraries (testify, gomock)"),
|
|
115
|
+
goRealtime: types_exports.GoRealtimeSchema.optional().describe("Go realtime library (gorilla-websocket, centrifuge)"),
|
|
116
|
+
goMessageQueue: types_exports.GoMessageQueueSchema.optional().describe("Go message queue (nats, watermill)"),
|
|
117
|
+
goCaching: types_exports.GoCachingSchema.optional().describe("Go caching library (redis, ristretto)"),
|
|
118
|
+
goConfig: types_exports.GoConfigSchema.optional().describe("Go config management (viper, koanf)"),
|
|
119
|
+
goObservability: types_exports.GoObservabilitySchema.optional().describe("Go observability (opentelemetry)"),
|
|
120
|
+
javaWebFramework: types_exports.JavaWebFrameworkSchema.optional().describe("Java web framework (spring-boot, quarkus, none)"),
|
|
121
|
+
javaBuildTool: types_exports.JavaBuildToolSchema.optional().describe("Java build tool (maven, gradle, none)"),
|
|
122
|
+
javaOrm: types_exports.JavaOrmSchema.optional().describe("Java ORM/database (spring-data-jpa)"),
|
|
123
|
+
javaAuth: types_exports.JavaAuthSchema.optional().describe("Java auth (spring-security)"),
|
|
124
|
+
javaApi: types_exports.JavaApiSchema.optional().describe("Java API layer (spring-graphql)"),
|
|
125
|
+
javaLogging: types_exports.JavaLoggingSchema.optional().describe("Java logging (logback, log4j2)"),
|
|
126
|
+
javaLibraries: z.array(types_exports.JavaLibrariesSchema).optional().describe("Java application libraries"),
|
|
127
|
+
javaTestingLibraries: z.array(types_exports.JavaTestingLibrariesSchema).optional().describe("Java testing libraries"),
|
|
128
|
+
dotnetWebFramework: types_exports.DotnetWebFrameworkSchema.optional().describe(".NET web framework (aspnet-minimal, aspnet-mvc, aspnet-blazor, none)"),
|
|
129
|
+
dotnetOrm: types_exports.DotnetOrmSchema.optional().describe(".NET data access (ef-core, dapper, linq2db)"),
|
|
130
|
+
dotnetAuth: types_exports.DotnetAuthSchema.optional().describe(".NET auth (aspnet-identity, duende-identityserver, auth0-aspnet, none)"),
|
|
131
|
+
dotnetApi: types_exports.DotnetApiSchema.optional().describe(".NET API style (minimal-api, graphql-hotchocolate, grpc-dotnet, none)"),
|
|
132
|
+
dotnetTesting: z.array(types_exports.DotnetTestingSchema).optional().describe(".NET testing libraries"),
|
|
133
|
+
dotnetJobQueue: types_exports.DotnetJobQueueSchema.optional().describe(".NET jobs (hangfire, quartz-net, hosted-services, none)"),
|
|
134
|
+
dotnetRealtime: types_exports.DotnetRealtimeSchema.optional().describe(".NET realtime (signalr, none)"),
|
|
135
|
+
dotnetObservability: z.array(types_exports.DotnetObservabilitySchema).optional().describe(".NET observability/logging libraries"),
|
|
136
|
+
dotnetValidation: types_exports.DotnetValidationSchema.optional().describe(".NET validation (fluentvalidation, data-annotations)"),
|
|
137
|
+
dotnetCaching: types_exports.DotnetCachingSchema.optional().describe(".NET caching (redis, memory-cache, none)"),
|
|
138
|
+
dotnetDeploy: types_exports.DotnetDeploySchema.optional().describe(".NET deploy target (docker, azure, aws, none)"),
|
|
139
|
+
elixirWebFramework: types_exports.ElixirWebFrameworkSchema.optional().describe("Elixir web framework (phoenix, phoenix-live-view, none)"),
|
|
140
|
+
elixirOrm: types_exports.ElixirOrmSchema.optional().describe("Elixir ORM/database (ecto, ecto-sql, none)"),
|
|
141
|
+
elixirAuth: types_exports.ElixirAuthSchema.optional().describe("Elixir auth (phx-gen-auth, ueberauth, guardian, none)"),
|
|
142
|
+
elixirApi: types_exports.ElixirApiSchema.optional().describe("Elixir API layer (rest, absinthe, none)"),
|
|
143
|
+
elixirRealtime: types_exports.ElixirRealtimeSchema.optional().describe("Elixir realtime (channels, presence, pubsub, live-view-streams, none)"),
|
|
144
|
+
elixirJobs: types_exports.ElixirJobsSchema.optional().describe("Elixir jobs (oban, quantum, none)"),
|
|
145
|
+
elixirValidation: types_exports.ElixirValidationSchema.optional().describe("Elixir validation (ecto-changesets, nimble-options, none)"),
|
|
146
|
+
elixirHttp: types_exports.ElixirHttpSchema.optional().describe("Elixir HTTP client (req, finch, none)"),
|
|
147
|
+
elixirJson: types_exports.ElixirJsonSchema.optional().describe("Elixir JSON library (jason, none)"),
|
|
148
|
+
elixirEmail: types_exports.ElixirEmailSchema.optional().describe("Elixir email library (swoosh, none)"),
|
|
149
|
+
elixirCaching: types_exports.ElixirCachingSchema.optional().describe("Elixir caching (cachex, nebulex, none)"),
|
|
150
|
+
elixirObservability: types_exports.ElixirObservabilitySchema.optional().describe("Elixir observability (telemetry, opentelemetry, prom_ex, none)"),
|
|
151
|
+
elixirTesting: types_exports.ElixirTestingSchema.optional().describe("Elixir testing (ex_unit, mox, bypass, wallaby, none)"),
|
|
152
|
+
elixirQuality: types_exports.ElixirQualitySchema.optional().describe("Elixir code quality (credo, dialyxir, sobelow, none)"),
|
|
153
|
+
elixirDeploy: types_exports.ElixirDeploySchema.optional().describe("Elixir deploy target (docker, fly, gigalixir, mix-release, none)"),
|
|
154
|
+
elixirLibraries: z.array(types_exports.ElixirLibrariesSchema).optional().describe("Elixir libraries (broadway, nx)"),
|
|
155
|
+
aiDocs: z.array(types_exports.AiDocsSchema).optional().describe("AI documentation files (claude-md, agents-md, cursorrules)")
|
|
156
|
+
});
|
|
157
|
+
const CreateCommandInputSchema = z.tuple([types_exports.ProjectNameSchema.optional(), CreateCommandOptionsSchema]);
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/utils/generate-reproducible-command.ts
|
|
161
|
+
function getBaseCommand(packageManager) {
|
|
162
|
+
switch (packageManager) {
|
|
163
|
+
case "bun": return "bun create better-fullstack@latest";
|
|
164
|
+
case "pnpm": return "pnpm create better-fullstack@latest";
|
|
165
|
+
case "yarn": return "yarn create better-fullstack@latest";
|
|
166
|
+
case "npm":
|
|
167
|
+
default: return "npx create-better-fullstack@latest";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function formatArrayFlag(flag, values) {
|
|
171
|
+
const normalizedValues = values.filter((value) => value !== "none");
|
|
172
|
+
if (normalizedValues.length === 0) return `--${flag} none`;
|
|
173
|
+
return `--${flag} ${normalizedValues.join(" ")}`;
|
|
174
|
+
}
|
|
175
|
+
function appendCommonFlags(flags, config) {
|
|
176
|
+
if (config.aiDocs && config.aiDocs.length > 0) flags.push(formatArrayFlag("ai-docs", config.aiDocs));
|
|
177
|
+
else flags.push("--ai-docs none");
|
|
178
|
+
flags.push(config.git ? "--git" : "--no-git");
|
|
179
|
+
flags.push(`--package-manager ${config.packageManager}`);
|
|
180
|
+
if (config.versionChannel !== "stable") flags.push(`--version-channel ${config.versionChannel}`);
|
|
181
|
+
flags.push(config.install ? "--install" : "--no-install");
|
|
182
|
+
}
|
|
183
|
+
function hasGraphPrimaryPart(config, role, ecosystem) {
|
|
184
|
+
return config.stackParts?.some((part) => part.source !== "provided" && part.role === role && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
|
|
185
|
+
}
|
|
186
|
+
function hasOwnedGraphPart(config, ownerRole, role, ecosystem, toolId) {
|
|
187
|
+
const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === ownerRole && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
|
|
188
|
+
if (!owner) return false;
|
|
189
|
+
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === role && part.ownerPartId === owner.id && (!ecosystem || part.ecosystem === ecosystem) && (!toolId || part.toolId === toolId)));
|
|
190
|
+
}
|
|
191
|
+
function hasGraphAddonPart(config, addon) {
|
|
192
|
+
const binding = (0, types_exports.getAddonStackPartBinding)(addon);
|
|
193
|
+
if (!binding) return false;
|
|
194
|
+
if (!binding.ownerRole) return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && !part.ownerPartId));
|
|
195
|
+
const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === binding.ownerRole && !part.ownerPartId && part.ecosystem === binding.ecosystem);
|
|
196
|
+
if (!owner) return false;
|
|
197
|
+
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && part.ownerPartId === owner.id));
|
|
198
|
+
}
|
|
199
|
+
function hasGraphExamplePart(config, example) {
|
|
200
|
+
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === "examples" && part.ecosystem === "universal" && part.toolId === example && !part.ownerPartId));
|
|
201
|
+
}
|
|
202
|
+
function hasGraphArrayParts(config, values, hasPart) {
|
|
203
|
+
const normalizedValues = values.filter((value) => value !== "none");
|
|
204
|
+
return normalizedValues.length > 0 && normalizedValues.every((value) => hasPart(config, value));
|
|
205
|
+
}
|
|
206
|
+
function appendChangedStringFlag(flags, flag, value, defaultValue) {
|
|
207
|
+
if (value !== defaultValue) flags.push(`--${flag} ${value}`);
|
|
208
|
+
}
|
|
209
|
+
function appendChangedGraphStringFlag(flags, config, role, ecosystem, flag, value, defaultValue) {
|
|
210
|
+
if (hasGraphPart(config, role, ecosystem)) return;
|
|
211
|
+
appendChangedStringFlag(flags, flag, value, defaultValue);
|
|
212
|
+
}
|
|
213
|
+
function appendChangedOwnedGraphStringFlag(flags, config, ownerRole, role, ecosystem, flag, value, defaultValue) {
|
|
214
|
+
if (hasOwnedGraphPart(config, ownerRole, role, ecosystem, value)) return;
|
|
215
|
+
appendChangedStringFlag(flags, flag, value, defaultValue);
|
|
216
|
+
}
|
|
217
|
+
function hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values) {
|
|
218
|
+
const normalizedValues = values.filter((value) => value !== "none");
|
|
219
|
+
if (normalizedValues.length === 0) return false;
|
|
220
|
+
return normalizedValues.every((value) => hasOwnedGraphPart(config, ownerRole, role, ecosystem, value));
|
|
221
|
+
}
|
|
222
|
+
function appendChangedOwnedGraphArrayFlag(flags, config, ownerRole, role, ecosystem, flag, values, defaultValues) {
|
|
223
|
+
if (hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values)) return;
|
|
224
|
+
appendChangedArrayFlag(flags, flag, values, defaultValues);
|
|
225
|
+
}
|
|
226
|
+
function appendChangedArrayFlag(flags, flag, values, defaultValues) {
|
|
227
|
+
if (values.length !== defaultValues.length || values.some((value, index) => value !== defaultValues[index])) flags.push(formatArrayFlag(flag, values));
|
|
228
|
+
}
|
|
229
|
+
function appendAstroIntegrationFlag(flags, config) {
|
|
230
|
+
if (config.frontend.includes("astro") && config.astroIntegration !== "none") flags.push(`--astro-integration ${config.astroIntegration}`);
|
|
231
|
+
}
|
|
232
|
+
function appendGraphExtraFlags(flags, config) {
|
|
233
|
+
if (!hasGraphArrayParts(config, config.addons, hasGraphAddonPart)) appendChangedArrayFlag(flags, "addons", config.addons, ["turborepo"]);
|
|
234
|
+
if (!hasGraphArrayParts(config, config.examples, hasGraphExamplePart)) appendChangedArrayFlag(flags, "examples", config.examples, []);
|
|
235
|
+
if (hasGraphPrimaryPart(config, "database")) appendChangedOwnedGraphStringFlag(flags, config, "database", "dbSetup", "universal", "db-setup", config.dbSetup, "none");
|
|
236
|
+
else appendChangedStringFlag(flags, "db-setup", config.dbSetup, "none");
|
|
237
|
+
if (hasGraphPrimaryPart(config, "frontend", "typescript")) {
|
|
238
|
+
appendAstroIntegrationFlag(flags, config);
|
|
239
|
+
appendChangedOwnedGraphStringFlag(flags, config, "frontend", "deploy", "typescript", "web-deploy", config.webDeploy, "none");
|
|
240
|
+
appendChangedGraphStringFlag(flags, config, "css", "typescript", "css-framework", config.cssFramework, "tailwind");
|
|
241
|
+
appendChangedGraphStringFlag(flags, config, "ui", "typescript", "ui-library", config.uiLibrary, "shadcn-ui");
|
|
242
|
+
if (config.uiLibrary === "shadcn-ui") {
|
|
243
|
+
appendChangedStringFlag(flags, "shadcn-base", config.shadcnBase ?? "radix", "radix");
|
|
244
|
+
appendChangedStringFlag(flags, "shadcn-style", config.shadcnStyle ?? "nova", "nova");
|
|
245
|
+
appendChangedStringFlag(flags, "shadcn-icon-library", config.shadcnIconLibrary ?? "lucide", "lucide");
|
|
246
|
+
appendChangedStringFlag(flags, "shadcn-color-theme", config.shadcnColorTheme ?? "neutral", "neutral");
|
|
247
|
+
appendChangedStringFlag(flags, "shadcn-base-color", config.shadcnBaseColor ?? "neutral", "neutral");
|
|
248
|
+
appendChangedStringFlag(flags, "shadcn-font", config.shadcnFont ?? "inter", "inter");
|
|
249
|
+
appendChangedStringFlag(flags, "shadcn-radius", config.shadcnRadius ?? "default", "default");
|
|
250
|
+
}
|
|
251
|
+
appendChangedGraphStringFlag(flags, config, "stateManagement", "typescript", "state-management", config.stateManagement, "none");
|
|
252
|
+
appendChangedGraphStringFlag(flags, config, "forms", "typescript", "forms", config.forms, "react-hook-form");
|
|
253
|
+
appendChangedStringFlag(flags, "validation", config.validation, "zod");
|
|
254
|
+
appendChangedStringFlag(flags, "testing", config.testing, "vitest");
|
|
255
|
+
appendChangedGraphStringFlag(flags, config, "animation", "typescript", "animation", config.animation, "none");
|
|
256
|
+
appendChangedGraphStringFlag(flags, config, "fileUpload", "typescript", "file-upload", config.fileUpload, "none");
|
|
257
|
+
appendChangedGraphStringFlag(flags, config, "i18n", "typescript", "i18n", config.i18n, "none");
|
|
258
|
+
appendChangedGraphStringFlag(flags, config, "analytics", "typescript", "analytics", config.analytics, "none");
|
|
259
|
+
}
|
|
260
|
+
if (hasGraphPrimaryPart(config, "frontend", "typescript") || hasGraphPrimaryPart(config, "backend", "typescript")) {
|
|
261
|
+
if (hasGraphPrimaryPart(config, "backend", "typescript")) {
|
|
262
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "runtime", "typescript", "runtime", config.runtime, "bun");
|
|
263
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "deploy", "typescript", "server-deploy", config.serverDeploy, "none");
|
|
264
|
+
} else appendChangedStringFlag(flags, "server-deploy", config.serverDeploy, "none");
|
|
265
|
+
appendChangedGraphStringFlag(flags, config, "payments", "typescript", "payments", config.payments, "none");
|
|
266
|
+
appendChangedGraphStringFlag(flags, config, "email", "typescript", "email", config.email, "none");
|
|
267
|
+
appendChangedStringFlag(flags, "effect", config.effect, "none");
|
|
268
|
+
appendChangedGraphStringFlag(flags, config, "ai", "typescript", "ai", config.ai, "none");
|
|
269
|
+
appendChangedGraphStringFlag(flags, config, "realtime", "typescript", "realtime", config.realtime, "none");
|
|
270
|
+
appendChangedGraphStringFlag(flags, config, "jobQueue", "typescript", "job-queue", config.jobQueue, "none");
|
|
271
|
+
appendChangedGraphStringFlag(flags, config, "logging", "typescript", "logging", config.logging, "none");
|
|
272
|
+
appendChangedGraphStringFlag(flags, config, "observability", "typescript", "observability", config.observability, "none");
|
|
273
|
+
appendChangedGraphStringFlag(flags, config, "featureFlags", "typescript", "feature-flags", config.featureFlags, "none");
|
|
274
|
+
appendChangedGraphStringFlag(flags, config, "caching", "typescript", "caching", config.caching, "none");
|
|
275
|
+
appendChangedGraphStringFlag(flags, config, "rateLimit", "typescript", "rate-limit", config.rateLimit, "none");
|
|
276
|
+
appendChangedGraphStringFlag(flags, config, "cms", "typescript", "cms", config.cms, "none");
|
|
277
|
+
appendChangedGraphStringFlag(flags, config, "search", "typescript", "search", config.search, "none");
|
|
278
|
+
appendChangedGraphStringFlag(flags, config, "vectorDb", "typescript", "vectorDb", config.vectorDb, "none");
|
|
279
|
+
appendChangedGraphStringFlag(flags, config, "fileStorage", "typescript", "file-storage", config.fileStorage, "none");
|
|
280
|
+
}
|
|
281
|
+
if (hasGraphPrimaryPart(config, "mobile")) {
|
|
282
|
+
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "navigation", "react-native", "mobile-navigation", config.mobileNavigation, "expo-router");
|
|
283
|
+
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "ui", "react-native", "mobile-ui", config.mobileUI, "none");
|
|
284
|
+
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "storage", "react-native", "mobile-storage", config.mobileStorage, "none");
|
|
285
|
+
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "testing", "react-native", "mobile-testing", config.mobileTesting, "none");
|
|
286
|
+
appendChangedStringFlag(flags, "mobile-push", config.mobilePush, "none");
|
|
287
|
+
appendChangedStringFlag(flags, "mobile-ota", config.mobileOTA, "none");
|
|
288
|
+
appendChangedStringFlag(flags, "mobile-deep-linking", config.mobileDeepLinking, "none");
|
|
289
|
+
}
|
|
290
|
+
if (hasGraphPrimaryPart(config, "frontend", "rust")) appendChangedStringFlag(flags, "rust-frontend", config.rustFrontend, "none");
|
|
291
|
+
if (hasGraphPrimaryPart(config, "backend", "rust")) {
|
|
292
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "rust", "rust-cli", config.rustCli, "none");
|
|
293
|
+
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "rust", "rust-libraries", config.rustLibraries, []);
|
|
294
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "rust", "rust-logging", config.rustLogging, "tracing");
|
|
295
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "errorHandling", "rust", "rust-error-handling", config.rustErrorHandling, "anyhow-thiserror");
|
|
296
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "caching", "rust", "rust-caching", config.rustCaching, "none");
|
|
297
|
+
}
|
|
298
|
+
if (hasGraphPrimaryPart(config, "backend", "python")) {
|
|
299
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "validation", "python", "python-validation", config.pythonValidation, "none");
|
|
300
|
+
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "ai", "python", "python-ai", config.pythonAi, []);
|
|
301
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "jobQueue", "python", "python-task-queue", config.pythonTaskQueue, "none");
|
|
302
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "api", "python", "python-graphql", config.pythonGraphql, "none");
|
|
303
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "python", "python-quality", config.pythonQuality, "none");
|
|
304
|
+
}
|
|
305
|
+
if (hasGraphPrimaryPart(config, "backend", "go")) {
|
|
306
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "go", "go-cli", config.goCli, "none");
|
|
307
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "go", "go-logging", config.goLogging, "none");
|
|
308
|
+
}
|
|
309
|
+
if (hasGraphPrimaryPart(config, "backend", "java")) {
|
|
310
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "buildTool", "java", "java-build-tool", config.javaBuildTool, "maven");
|
|
311
|
+
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "java", "java-libraries", config.javaLibraries, []);
|
|
312
|
+
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "testing", "java", "java-testing-libraries", config.javaTestingLibraries, ["junit5"]);
|
|
313
|
+
}
|
|
314
|
+
if (hasGraphPrimaryPart(config, "backend", "elixir")) {
|
|
315
|
+
appendChangedGraphStringFlag(flags, config, "realtime", "elixir", "elixir-realtime", config.elixirRealtime, "channels");
|
|
316
|
+
appendChangedGraphStringFlag(flags, config, "jobQueue", "elixir", "elixir-jobs", config.elixirJobs, "none");
|
|
317
|
+
appendChangedGraphStringFlag(flags, config, "validation", "elixir", "elixir-validation", config.elixirValidation, "ecto-changesets");
|
|
318
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "httpClient", "elixir", "elixir-http", config.elixirHttp, "req");
|
|
319
|
+
appendChangedStringFlag(flags, "elixir-json", config.elixirJson, "jason");
|
|
320
|
+
appendChangedGraphStringFlag(flags, config, "email", "elixir", "elixir-email", config.elixirEmail, "none");
|
|
321
|
+
appendChangedGraphStringFlag(flags, config, "caching", "elixir", "elixir-caching", config.elixirCaching, "none");
|
|
322
|
+
appendChangedGraphStringFlag(flags, config, "observability", "elixir", "elixir-observability", config.elixirObservability, "telemetry");
|
|
323
|
+
appendChangedGraphStringFlag(flags, config, "testing", "elixir", "elixir-testing", config.elixirTesting, "ex_unit");
|
|
324
|
+
appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "elixir", "elixir-quality", config.elixirQuality, "credo");
|
|
325
|
+
appendChangedGraphStringFlag(flags, config, "deploy", "elixir", "elixir-deploy", config.elixirDeploy, "none");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function appendSharedNonTypeScriptFlags(flags, config) {
|
|
329
|
+
flags.push(`--email ${config.email}`);
|
|
330
|
+
flags.push(`--observability ${config.observability}`);
|
|
331
|
+
flags.push(`--caching ${config.caching}`);
|
|
332
|
+
flags.push(`--search ${config.search}`);
|
|
333
|
+
flags.push(formatArrayFlag("addons", config.addons));
|
|
334
|
+
flags.push(formatArrayFlag("examples", config.examples));
|
|
335
|
+
flags.push(`--db-setup ${config.dbSetup}`);
|
|
336
|
+
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
337
|
+
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
338
|
+
}
|
|
339
|
+
function getTypeScriptFlags(config) {
|
|
340
|
+
const flags = [];
|
|
341
|
+
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
|
|
342
|
+
else flags.push("--frontend none");
|
|
343
|
+
appendAstroIntegrationFlag(flags, config);
|
|
344
|
+
flags.push(`--backend ${config.backend}`);
|
|
345
|
+
flags.push(`--runtime ${config.runtime}`);
|
|
346
|
+
flags.push(`--database ${config.database}`);
|
|
347
|
+
flags.push(`--orm ${config.orm}`);
|
|
348
|
+
flags.push(`--api ${config.api}`);
|
|
349
|
+
flags.push(`--auth ${config.auth}`);
|
|
350
|
+
flags.push(`--payments ${config.payments}`);
|
|
351
|
+
flags.push(`--email ${config.email}`);
|
|
352
|
+
flags.push(`--file-upload ${config.fileUpload}`);
|
|
353
|
+
flags.push(`--effect ${config.effect}`);
|
|
354
|
+
flags.push(`--css-framework ${config.cssFramework}`);
|
|
355
|
+
flags.push(`--ui-library ${config.uiLibrary}`);
|
|
356
|
+
if (config.uiLibrary === "shadcn-ui") {
|
|
357
|
+
flags.push(`--shadcn-base ${config.shadcnBase}`);
|
|
358
|
+
flags.push(`--shadcn-style ${config.shadcnStyle}`);
|
|
359
|
+
flags.push(`--shadcn-icon-library ${config.shadcnIconLibrary}`);
|
|
360
|
+
flags.push(`--shadcn-color-theme ${config.shadcnColorTheme}`);
|
|
361
|
+
flags.push(`--shadcn-base-color ${config.shadcnBaseColor}`);
|
|
362
|
+
flags.push(`--shadcn-font ${config.shadcnFont}`);
|
|
363
|
+
flags.push(`--shadcn-radius ${config.shadcnRadius}`);
|
|
364
|
+
}
|
|
365
|
+
flags.push(`--ai ${config.ai}`);
|
|
366
|
+
flags.push(`--state-management ${config.stateManagement}`);
|
|
367
|
+
flags.push(`--forms ${config.forms}`);
|
|
368
|
+
flags.push(`--validation ${config.validation}`);
|
|
369
|
+
flags.push(`--testing ${config.testing}`);
|
|
370
|
+
flags.push(`--animation ${config.animation}`);
|
|
371
|
+
flags.push(`--realtime ${config.realtime}`);
|
|
372
|
+
flags.push(`--job-queue ${config.jobQueue}`);
|
|
373
|
+
flags.push(`--logging ${config.logging}`);
|
|
374
|
+
flags.push(`--observability ${config.observability}`);
|
|
375
|
+
flags.push(`--feature-flags ${config.featureFlags}`);
|
|
376
|
+
flags.push(`--caching ${config.caching}`);
|
|
377
|
+
flags.push(`--rate-limit ${config.rateLimit}`);
|
|
378
|
+
flags.push(`--i18n ${config.i18n}`);
|
|
379
|
+
flags.push(`--cms ${config.cms}`);
|
|
380
|
+
flags.push(`--search ${config.search}`);
|
|
381
|
+
flags.push(`--vector-db ${config.vectorDb}`);
|
|
382
|
+
flags.push(`--file-storage ${config.fileStorage}`);
|
|
383
|
+
flags.push(`--mobile-navigation ${config.mobileNavigation}`);
|
|
384
|
+
flags.push(`--mobile-ui ${config.mobileUI}`);
|
|
385
|
+
flags.push(`--mobile-storage ${config.mobileStorage}`);
|
|
386
|
+
flags.push(`--mobile-testing ${config.mobileTesting}`);
|
|
387
|
+
flags.push(`--mobile-push ${config.mobilePush}`);
|
|
388
|
+
flags.push(`--mobile-ota ${config.mobileOTA}`);
|
|
389
|
+
flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
|
|
390
|
+
if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
|
|
391
|
+
else flags.push("--addons none");
|
|
392
|
+
if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
|
|
393
|
+
else flags.push("--examples none");
|
|
394
|
+
flags.push(`--db-setup ${config.dbSetup}`);
|
|
395
|
+
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
396
|
+
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
397
|
+
appendCommonFlags(flags, config);
|
|
398
|
+
return flags;
|
|
399
|
+
}
|
|
400
|
+
function getReactNativeFlags(config) {
|
|
401
|
+
const flags = ["--ecosystem react-native"];
|
|
402
|
+
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
|
|
403
|
+
else flags.push("--frontend native-bare");
|
|
404
|
+
flags.push(`--auth ${config.auth}`);
|
|
405
|
+
flags.push(`--mobile-navigation ${config.mobileNavigation}`);
|
|
406
|
+
flags.push(`--mobile-ui ${config.mobileUI}`);
|
|
407
|
+
flags.push(`--mobile-storage ${config.mobileStorage}`);
|
|
408
|
+
flags.push(`--mobile-testing ${config.mobileTesting}`);
|
|
409
|
+
flags.push(`--mobile-push ${config.mobilePush}`);
|
|
410
|
+
flags.push(`--mobile-ota ${config.mobileOTA}`);
|
|
411
|
+
flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
|
|
412
|
+
appendCommonFlags(flags, config);
|
|
413
|
+
return flags;
|
|
414
|
+
}
|
|
415
|
+
function getRustFlags(config) {
|
|
416
|
+
const flags = ["--ecosystem rust"];
|
|
417
|
+
flags.push(`--rust-web-framework ${config.rustWebFramework}`);
|
|
418
|
+
flags.push(`--rust-frontend ${config.rustFrontend}`);
|
|
419
|
+
flags.push(`--rust-orm ${config.rustOrm}`);
|
|
420
|
+
flags.push(`--rust-api ${config.rustApi}`);
|
|
421
|
+
flags.push(`--rust-cli ${config.rustCli}`);
|
|
422
|
+
flags.push(formatArrayFlag("rust-libraries", config.rustLibraries));
|
|
423
|
+
flags.push(`--rust-logging ${config.rustLogging}`);
|
|
424
|
+
flags.push(`--rust-error-handling ${config.rustErrorHandling}`);
|
|
425
|
+
flags.push(`--rust-caching ${config.rustCaching}`);
|
|
426
|
+
flags.push(`--rust-auth ${config.rustAuth}`);
|
|
427
|
+
flags.push(`--rust-realtime ${config.rustRealtime}`);
|
|
428
|
+
flags.push(`--rust-message-queue ${config.rustMessageQueue}`);
|
|
429
|
+
flags.push(`--rust-observability ${config.rustObservability}`);
|
|
430
|
+
flags.push(`--rust-templating ${config.rustTemplating}`);
|
|
431
|
+
appendSharedNonTypeScriptFlags(flags, config);
|
|
432
|
+
appendCommonFlags(flags, config);
|
|
433
|
+
return flags;
|
|
434
|
+
}
|
|
435
|
+
function getPythonFlags(config) {
|
|
436
|
+
const flags = ["--ecosystem python"];
|
|
437
|
+
flags.push(`--python-web-framework ${config.pythonWebFramework}`);
|
|
438
|
+
flags.push(`--python-orm ${config.pythonOrm}`);
|
|
439
|
+
flags.push(`--python-validation ${config.pythonValidation}`);
|
|
440
|
+
flags.push(formatArrayFlag("python-ai", config.pythonAi));
|
|
441
|
+
flags.push(`--python-auth ${config.pythonAuth}`);
|
|
442
|
+
flags.push(`--python-api ${config.pythonApi}`);
|
|
443
|
+
flags.push(`--python-task-queue ${config.pythonTaskQueue}`);
|
|
444
|
+
flags.push(`--python-graphql ${config.pythonGraphql}`);
|
|
445
|
+
flags.push(`--python-quality ${config.pythonQuality}`);
|
|
446
|
+
flags.push(formatArrayFlag("python-testing", config.pythonTesting));
|
|
447
|
+
flags.push(`--python-caching ${config.pythonCaching}`);
|
|
448
|
+
flags.push(`--python-realtime ${config.pythonRealtime}`);
|
|
449
|
+
flags.push(`--python-observability ${config.pythonObservability}`);
|
|
450
|
+
flags.push(formatArrayFlag("python-cli", config.pythonCli));
|
|
451
|
+
appendSharedNonTypeScriptFlags(flags, config);
|
|
452
|
+
appendCommonFlags(flags, config);
|
|
453
|
+
return flags;
|
|
454
|
+
}
|
|
455
|
+
function getGoFlags(config) {
|
|
456
|
+
const flags = ["--ecosystem go"];
|
|
457
|
+
flags.push(`--go-web-framework ${config.goWebFramework}`);
|
|
458
|
+
flags.push(`--go-orm ${config.goOrm}`);
|
|
459
|
+
flags.push(`--go-api ${config.goApi}`);
|
|
460
|
+
flags.push(`--go-cli ${config.goCli}`);
|
|
461
|
+
flags.push(`--go-logging ${config.goLogging}`);
|
|
462
|
+
flags.push(`--go-auth ${config.goAuth}`);
|
|
463
|
+
flags.push(formatArrayFlag("go-testing", config.goTesting));
|
|
464
|
+
flags.push(`--go-realtime ${config.goRealtime}`);
|
|
465
|
+
flags.push(`--go-message-queue ${config.goMessageQueue}`);
|
|
466
|
+
flags.push(`--go-caching ${config.goCaching}`);
|
|
467
|
+
flags.push(`--go-config ${config.goConfig}`);
|
|
468
|
+
flags.push(`--go-observability ${config.goObservability}`);
|
|
469
|
+
flags.push(`--auth ${config.auth}`);
|
|
470
|
+
appendSharedNonTypeScriptFlags(flags, config);
|
|
471
|
+
appendCommonFlags(flags, config);
|
|
472
|
+
return flags;
|
|
473
|
+
}
|
|
474
|
+
function getJavaFlags(config) {
|
|
475
|
+
const flags = ["--ecosystem java"];
|
|
476
|
+
flags.push(`--java-web-framework ${config.javaWebFramework}`);
|
|
477
|
+
flags.push(`--java-build-tool ${config.javaBuildTool}`);
|
|
478
|
+
flags.push(`--java-orm ${config.javaOrm}`);
|
|
479
|
+
flags.push(`--java-auth ${config.javaAuth}`);
|
|
480
|
+
flags.push(`--java-api ${config.javaApi}`);
|
|
481
|
+
flags.push(`--java-logging ${config.javaLogging}`);
|
|
482
|
+
flags.push(formatArrayFlag("java-libraries", config.javaLibraries));
|
|
483
|
+
flags.push(formatArrayFlag("java-testing-libraries", config.javaTestingLibraries));
|
|
484
|
+
appendSharedNonTypeScriptFlags(flags, config);
|
|
485
|
+
appendCommonFlags(flags, config);
|
|
486
|
+
return flags;
|
|
487
|
+
}
|
|
488
|
+
function getDotnetFlags(config) {
|
|
489
|
+
const flags = ["--ecosystem dotnet"];
|
|
490
|
+
flags.push(`--dotnet-web-framework ${config.dotnetWebFramework}`);
|
|
491
|
+
flags.push(`--dotnet-orm ${config.dotnetOrm}`);
|
|
492
|
+
flags.push(`--dotnet-auth ${config.dotnetAuth}`);
|
|
493
|
+
flags.push(`--dotnet-api ${config.dotnetApi}`);
|
|
494
|
+
flags.push(formatArrayFlag("dotnet-testing", config.dotnetTesting));
|
|
495
|
+
flags.push(`--dotnet-job-queue ${config.dotnetJobQueue}`);
|
|
496
|
+
flags.push(`--dotnet-realtime ${config.dotnetRealtime}`);
|
|
497
|
+
flags.push(formatArrayFlag("dotnet-observability", config.dotnetObservability));
|
|
498
|
+
flags.push(`--dotnet-validation ${config.dotnetValidation}`);
|
|
499
|
+
flags.push(`--dotnet-caching ${config.dotnetCaching}`);
|
|
500
|
+
flags.push(`--dotnet-deploy ${config.dotnetDeploy}`);
|
|
501
|
+
appendCommonFlags(flags, config);
|
|
502
|
+
return flags;
|
|
503
|
+
}
|
|
504
|
+
function getElixirFlags(config) {
|
|
505
|
+
const flags = ["--ecosystem elixir"];
|
|
506
|
+
flags.push(`--elixir-web-framework ${config.elixirWebFramework}`);
|
|
507
|
+
flags.push(`--elixir-orm ${config.elixirOrm}`);
|
|
508
|
+
flags.push(`--elixir-auth ${config.elixirAuth}`);
|
|
509
|
+
flags.push(`--elixir-api ${config.elixirApi}`);
|
|
510
|
+
flags.push(`--elixir-realtime ${config.elixirRealtime}`);
|
|
511
|
+
flags.push(`--elixir-jobs ${config.elixirJobs}`);
|
|
512
|
+
flags.push(`--elixir-validation ${config.elixirValidation}`);
|
|
513
|
+
flags.push(`--elixir-http ${config.elixirHttp}`);
|
|
514
|
+
flags.push(`--elixir-json ${config.elixirJson}`);
|
|
515
|
+
flags.push(`--elixir-email ${config.elixirEmail}`);
|
|
516
|
+
flags.push(`--elixir-caching ${config.elixirCaching}`);
|
|
517
|
+
flags.push(`--elixir-observability ${config.elixirObservability}`);
|
|
518
|
+
flags.push(`--elixir-testing ${config.elixirTesting}`);
|
|
519
|
+
flags.push(`--elixir-quality ${config.elixirQuality}`);
|
|
520
|
+
flags.push(`--elixir-deploy ${config.elixirDeploy}`);
|
|
521
|
+
flags.push(formatArrayFlag("elixir-libraries", config.elixirLibraries));
|
|
522
|
+
appendCommonFlags(flags, config);
|
|
523
|
+
return flags;
|
|
524
|
+
}
|
|
525
|
+
function generateReproducibleCommand(config) {
|
|
526
|
+
let flags;
|
|
527
|
+
if (config.stackParts && config.stackParts.length > 0) {
|
|
528
|
+
flags = config.stackParts.filter((part) => part.source !== "provided").map((part) => `--part ${(0, types_exports.formatStackPartSpec)(part, config.stackParts ?? [])}`);
|
|
529
|
+
appendGraphExtraFlags(flags, config);
|
|
530
|
+
appendCommonFlags(flags, config);
|
|
531
|
+
return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
|
|
532
|
+
}
|
|
533
|
+
switch (config.ecosystem) {
|
|
534
|
+
case "react-native":
|
|
535
|
+
flags = getReactNativeFlags(config);
|
|
536
|
+
break;
|
|
537
|
+
case "rust":
|
|
538
|
+
flags = getRustFlags(config);
|
|
539
|
+
break;
|
|
540
|
+
case "python":
|
|
541
|
+
flags = getPythonFlags(config);
|
|
542
|
+
break;
|
|
543
|
+
case "go":
|
|
544
|
+
flags = getGoFlags(config);
|
|
545
|
+
break;
|
|
546
|
+
case "java":
|
|
547
|
+
flags = getJavaFlags(config);
|
|
548
|
+
break;
|
|
549
|
+
case "dotnet":
|
|
550
|
+
flags = getDotnetFlags(config);
|
|
551
|
+
break;
|
|
552
|
+
case "elixir":
|
|
553
|
+
flags = getElixirFlags(config);
|
|
554
|
+
break;
|
|
555
|
+
case "typescript":
|
|
556
|
+
default:
|
|
557
|
+
flags = getTypeScriptFlags(config);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/utils/templates.ts
|
|
565
|
+
const TEMPLATE_PRESETS = {
|
|
566
|
+
mern: {
|
|
567
|
+
database: "mongodb",
|
|
568
|
+
orm: "mongoose",
|
|
569
|
+
backend: "express",
|
|
570
|
+
runtime: "node",
|
|
571
|
+
frontend: ["react-router"],
|
|
572
|
+
api: "orpc",
|
|
573
|
+
auth: "better-auth",
|
|
574
|
+
payments: "none",
|
|
575
|
+
addons: ["turborepo"],
|
|
576
|
+
examples: ["none"],
|
|
577
|
+
dbSetup: "mongodb-atlas",
|
|
578
|
+
webDeploy: "none",
|
|
579
|
+
serverDeploy: "none"
|
|
580
|
+
},
|
|
581
|
+
pern: {
|
|
582
|
+
database: "postgres",
|
|
583
|
+
orm: "drizzle",
|
|
584
|
+
backend: "express",
|
|
585
|
+
runtime: "node",
|
|
586
|
+
frontend: ["tanstack-router"],
|
|
587
|
+
api: "trpc",
|
|
588
|
+
auth: "better-auth",
|
|
589
|
+
payments: "none",
|
|
590
|
+
addons: ["turborepo"],
|
|
591
|
+
examples: ["none"],
|
|
592
|
+
dbSetup: "none",
|
|
593
|
+
webDeploy: "none",
|
|
594
|
+
serverDeploy: "none"
|
|
595
|
+
},
|
|
596
|
+
t3: {
|
|
597
|
+
database: "postgres",
|
|
598
|
+
orm: "prisma",
|
|
599
|
+
backend: "self",
|
|
600
|
+
runtime: "none",
|
|
601
|
+
frontend: ["next"],
|
|
602
|
+
api: "trpc",
|
|
603
|
+
auth: "better-auth",
|
|
604
|
+
payments: "none",
|
|
605
|
+
addons: ["biome", "turborepo"],
|
|
606
|
+
examples: ["none"],
|
|
607
|
+
dbSetup: "none",
|
|
608
|
+
webDeploy: "none",
|
|
609
|
+
serverDeploy: "none"
|
|
610
|
+
},
|
|
611
|
+
uniwind: {
|
|
612
|
+
database: "none",
|
|
613
|
+
orm: "none",
|
|
614
|
+
backend: "none",
|
|
615
|
+
runtime: "none",
|
|
616
|
+
frontend: ["native-uniwind"],
|
|
617
|
+
api: "none",
|
|
618
|
+
auth: "none",
|
|
619
|
+
payments: "none",
|
|
620
|
+
addons: ["none"],
|
|
621
|
+
examples: ["none"],
|
|
622
|
+
dbSetup: "none",
|
|
623
|
+
webDeploy: "none",
|
|
624
|
+
serverDeploy: "none"
|
|
625
|
+
},
|
|
626
|
+
none: null
|
|
627
|
+
};
|
|
628
|
+
function getTemplateConfig(template) {
|
|
629
|
+
if (template === "none" || !template) return null;
|
|
630
|
+
const config = TEMPLATE_PRESETS[template];
|
|
631
|
+
if (!config) throw new Error(`Unknown template: ${template}`);
|
|
632
|
+
return config;
|
|
633
|
+
}
|
|
634
|
+
function getTemplateDescription(template) {
|
|
635
|
+
return {
|
|
636
|
+
mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
|
|
637
|
+
pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
|
|
638
|
+
t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
|
|
639
|
+
uniwind: "Expo + Uniwind native app with no backend services",
|
|
640
|
+
none: "No template - Full customization"
|
|
641
|
+
}[template] || "";
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/utils/peer-dependency-conflicts.ts
|
|
646
|
+
/**
|
|
647
|
+
* Known peer dependency conflicts.
|
|
648
|
+
*
|
|
649
|
+
* Add new conflicts here as they are discovered.
|
|
650
|
+
* The validator will check these against the user's selected options.
|
|
651
|
+
*/
|
|
652
|
+
const PEER_DEPENDENCY_CONFLICTS = [
|
|
653
|
+
{
|
|
654
|
+
id: "redux-toolkit-react19",
|
|
655
|
+
description: "redux-toolkit with React 19 may have peer dependency warnings",
|
|
656
|
+
packages: ["@reduxjs/toolkit", "react-redux"],
|
|
657
|
+
severity: "warning",
|
|
658
|
+
resolution: "Consider using zustand or jotai for React 19 projects, or ensure react-redux v9+",
|
|
659
|
+
triggeredBy: [{
|
|
660
|
+
optionKey: "stateManagement",
|
|
661
|
+
values: ["redux-toolkit"]
|
|
662
|
+
}],
|
|
663
|
+
conflictsWithOptions: [{
|
|
664
|
+
optionKey: "frontend",
|
|
665
|
+
values: ["next", "vinext"]
|
|
666
|
+
}]
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
id: "effect-schema-zod-overlap",
|
|
670
|
+
description: "effect/Schema (built into effect) and zod both provide validation - may cause confusion",
|
|
671
|
+
packages: ["effect", "zod"],
|
|
672
|
+
severity: "warning",
|
|
673
|
+
resolution: "Use --validation effect-schema with Effect, or --effect none with Zod",
|
|
674
|
+
triggeredBy: [{
|
|
675
|
+
optionKey: "effect",
|
|
676
|
+
values: ["effect-full"]
|
|
677
|
+
}],
|
|
678
|
+
conflictsWithOptions: [{
|
|
679
|
+
optionKey: "validation",
|
|
680
|
+
values: ["zod"]
|
|
681
|
+
}]
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
id: "prisma-d1-version",
|
|
685
|
+
description: "Prisma with D1 requires Prisma 5.x+ and specific adapter setup",
|
|
686
|
+
packages: ["prisma", "@prisma/adapter-d1"],
|
|
687
|
+
severity: "warning",
|
|
688
|
+
resolution: "Consider Drizzle ORM for simpler D1 integration",
|
|
689
|
+
triggeredBy: [{
|
|
690
|
+
optionKey: "orm",
|
|
691
|
+
values: ["prisma"]
|
|
692
|
+
}],
|
|
693
|
+
conflictsWithOptions: [{
|
|
694
|
+
optionKey: "dbSetup",
|
|
695
|
+
values: ["d1"]
|
|
696
|
+
}]
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: "biome-linting-overlap",
|
|
700
|
+
description: "Biome includes linting/formatting - other linting addons are redundant",
|
|
701
|
+
packages: [
|
|
702
|
+
"@biomejs/biome",
|
|
703
|
+
"eslint",
|
|
704
|
+
"prettier"
|
|
705
|
+
],
|
|
706
|
+
severity: "warning",
|
|
707
|
+
resolution: "Choose either Biome (all-in-one) or other linting tools, not both",
|
|
708
|
+
triggeredBy: [{
|
|
709
|
+
optionKey: "addons",
|
|
710
|
+
values: ["biome"]
|
|
711
|
+
}],
|
|
712
|
+
conflictsWithOptions: [{
|
|
713
|
+
optionKey: "addons",
|
|
714
|
+
values: ["ultracite", "oxlint"]
|
|
715
|
+
}]
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
id: "framer-motion-react19",
|
|
719
|
+
description: "framer-motion versions <12 may have React 19 issues",
|
|
720
|
+
packages: ["framer-motion"],
|
|
721
|
+
severity: "warning",
|
|
722
|
+
resolution: "The CLI uses motion (framer-motion v12+) which supports React 19",
|
|
723
|
+
triggeredBy: [{
|
|
724
|
+
optionKey: "animation",
|
|
725
|
+
values: ["framer-motion"]
|
|
726
|
+
}],
|
|
727
|
+
conflictsWithOptions: [{
|
|
728
|
+
optionKey: "frontend",
|
|
729
|
+
values: ["next", "vinext"]
|
|
730
|
+
}]
|
|
731
|
+
}
|
|
732
|
+
];
|
|
733
|
+
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/utils/peer-dependency-validator.ts
|
|
736
|
+
/**
|
|
737
|
+
* Peer dependency conflict validator.
|
|
738
|
+
*
|
|
739
|
+
* Checks project configuration against known conflicts and provides
|
|
740
|
+
* warnings or errors before project creation begins.
|
|
741
|
+
*/
|
|
742
|
+
/**
|
|
743
|
+
* Checks if a config value matches any of the specified values.
|
|
744
|
+
*/
|
|
745
|
+
function matchesValue(configValue, values) {
|
|
746
|
+
if (configValue === void 0 || configValue === null) return false;
|
|
747
|
+
if (Array.isArray(configValue)) return values.some((v) => configValue.includes(v));
|
|
748
|
+
return values.includes(configValue);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Checks the configuration against known peer dependency conflicts.
|
|
752
|
+
*/
|
|
753
|
+
function checkPeerDependencyConflicts(config) {
|
|
754
|
+
const errors = [];
|
|
755
|
+
const warnings = [];
|
|
756
|
+
for (const conflict of PEER_DEPENDENCY_CONFLICTS) {
|
|
757
|
+
if (!conflict.triggeredBy.some((trigger) => {
|
|
758
|
+
const configValue = config[trigger.optionKey];
|
|
759
|
+
return matchesValue(configValue, trigger.values);
|
|
760
|
+
})) continue;
|
|
761
|
+
if (conflict.conflictsWithOptions.some((opt) => {
|
|
762
|
+
const configValue = config[opt.optionKey];
|
|
763
|
+
return matchesValue(configValue, opt.values);
|
|
764
|
+
})) if (conflict.severity === "error") errors.push(conflict);
|
|
765
|
+
else warnings.push(conflict);
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
errors,
|
|
769
|
+
warnings
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Prints a warning message for a dependency conflict.
|
|
774
|
+
*/
|
|
775
|
+
function warnDependencyConflict(message, resolution) {
|
|
776
|
+
if (isSilent()) return;
|
|
777
|
+
consola.warn(pc.yellow(`Peer Dependency Warning: ${message}`));
|
|
778
|
+
consola.log(pc.dim(` → ${resolution}`));
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Handles conflict results by printing warnings and exiting on errors.
|
|
782
|
+
*/
|
|
783
|
+
function handleConflictResult(result) {
|
|
784
|
+
for (const warning of result.warnings) warnDependencyConflict(warning.description, warning.resolution);
|
|
785
|
+
if (result.errors.length > 0) exitWithError(`Peer Dependency Conflict:\n${result.errors.map((e) => `${e.description}\n → ${e.resolution}`).join("\n\n")}`);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Validates peer dependencies for the given configuration.
|
|
789
|
+
* Prints warnings and exits on errors.
|
|
790
|
+
*/
|
|
791
|
+
function validatePeerDependencies(config) {
|
|
792
|
+
handleConflictResult(checkPeerDependencyConflicts(config));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/utils/config-validation.ts
|
|
797
|
+
function validateDatabaseOrmAuth(cfg, flags) {
|
|
798
|
+
const db = cfg.database;
|
|
799
|
+
const orm = cfg.orm;
|
|
800
|
+
const has = (k) => flags ? flags.has(k) : true;
|
|
801
|
+
const hasGraphOrm = cfg.stackParts?.some((part) => part.role === "orm" && part.source !== "provided");
|
|
802
|
+
const ecosystemOrm = getEcosystemOrm(cfg);
|
|
803
|
+
const hasEcosystemOrm = ecosystemOrm !== void 0 && ecosystemOrm !== "none";
|
|
804
|
+
const isNonTypeScriptSqliteDefault = cfg.ecosystem !== void 0 && cfg.ecosystem !== "typescript" && cfg.ecosystem !== "react-native" && db === "sqlite" && !hasEcosystemOrm;
|
|
805
|
+
if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") incompatibilityError({
|
|
806
|
+
message: "Mongoose ORM requires MongoDB database.",
|
|
807
|
+
provided: {
|
|
808
|
+
orm: "mongoose",
|
|
809
|
+
database: db || "none"
|
|
810
|
+
},
|
|
811
|
+
suggestions: ["Use --database mongodb", "Choose a different ORM (drizzle, prisma)"]
|
|
812
|
+
});
|
|
813
|
+
if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") incompatibilityError({
|
|
814
|
+
message: "Drizzle ORM does not support MongoDB.",
|
|
815
|
+
provided: {
|
|
816
|
+
orm: "drizzle",
|
|
817
|
+
database: "mongodb"
|
|
818
|
+
},
|
|
819
|
+
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
820
|
+
});
|
|
821
|
+
if (has("orm") && has("database") && orm === "typeorm" && db === "mongodb") incompatibilityError({
|
|
822
|
+
message: "TypeORM does not support MongoDB in Better Fullstack.",
|
|
823
|
+
provided: {
|
|
824
|
+
orm: "typeorm",
|
|
825
|
+
database: "mongodb"
|
|
826
|
+
},
|
|
827
|
+
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
828
|
+
});
|
|
829
|
+
if (has("orm") && has("database") && orm === "kysely" && db === "mongodb") incompatibilityError({
|
|
830
|
+
message: "Kysely does not support MongoDB.",
|
|
831
|
+
provided: {
|
|
832
|
+
orm: "kysely",
|
|
833
|
+
database: "mongodb"
|
|
834
|
+
},
|
|
835
|
+
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
836
|
+
});
|
|
837
|
+
if (has("orm") && has("database") && orm === "mikroorm" && db === "mongodb") incompatibilityError({
|
|
838
|
+
message: "MikroORM does not support MongoDB in Better Fullstack.",
|
|
839
|
+
provided: {
|
|
840
|
+
orm: "mikroorm",
|
|
841
|
+
database: "mongodb"
|
|
842
|
+
},
|
|
843
|
+
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
844
|
+
});
|
|
845
|
+
if (has("orm") && has("database") && orm === "sequelize" && db === "mongodb") incompatibilityError({
|
|
846
|
+
message: "Sequelize does not support MongoDB.",
|
|
847
|
+
provided: {
|
|
848
|
+
orm: "sequelize",
|
|
849
|
+
database: "mongodb"
|
|
850
|
+
},
|
|
851
|
+
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
852
|
+
});
|
|
853
|
+
if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") incompatibilityError({
|
|
854
|
+
message: "In Better-Fullstack, MongoDB is currently supported only with Mongoose or Prisma ORM.",
|
|
855
|
+
provided: {
|
|
856
|
+
database: "mongodb",
|
|
857
|
+
orm
|
|
858
|
+
},
|
|
859
|
+
suggestions: ["Use --orm mongoose", "Use --orm prisma"]
|
|
860
|
+
});
|
|
861
|
+
if (has("database") && has("orm") && db && db !== "none" && db !== "edgedb" && db !== "redis" && orm === "none" && !hasGraphOrm && !hasEcosystemOrm && !isNonTypeScriptSqliteDefault) missingRequirementError({
|
|
862
|
+
message: "Database selection requires an ORM.",
|
|
863
|
+
provided: {
|
|
864
|
+
database: db,
|
|
865
|
+
orm: "none"
|
|
866
|
+
},
|
|
867
|
+
suggestions: [
|
|
868
|
+
"Use --orm drizzle (recommended)",
|
|
869
|
+
"Use --orm prisma",
|
|
870
|
+
"Use --orm mongoose (MongoDB only)"
|
|
871
|
+
]
|
|
872
|
+
});
|
|
873
|
+
if (has("database") && has("orm") && db === "edgedb" && orm && orm !== "none") incompatibilityError({
|
|
874
|
+
message: "EdgeDB has its own built-in query builder and does not require an ORM.",
|
|
875
|
+
provided: {
|
|
876
|
+
database: "edgedb",
|
|
877
|
+
orm
|
|
878
|
+
},
|
|
879
|
+
suggestions: ["Use --orm none with EdgeDB", "Choose a different database if you want to use an ORM"]
|
|
880
|
+
});
|
|
881
|
+
if (has("database") && has("orm") && db === "redis" && orm && orm !== "none") incompatibilityError({
|
|
882
|
+
message: "Redis is a key-value store and does not require an ORM.",
|
|
883
|
+
provided: {
|
|
884
|
+
database: "redis",
|
|
885
|
+
orm
|
|
886
|
+
},
|
|
887
|
+
suggestions: ["Use --orm none with Redis", "Choose a different database if you want to use an ORM"]
|
|
888
|
+
});
|
|
889
|
+
if (has("orm") && has("database") && orm && orm !== "none" && db === "none") missingRequirementError({
|
|
890
|
+
message: "ORM selection requires a database.",
|
|
891
|
+
provided: {
|
|
892
|
+
orm,
|
|
893
|
+
database: "none"
|
|
894
|
+
},
|
|
895
|
+
suggestions: [
|
|
896
|
+
"Use --database postgres",
|
|
897
|
+
"Use --database sqlite",
|
|
898
|
+
"Use --database mysql",
|
|
899
|
+
"Set --orm none"
|
|
900
|
+
]
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
function getEcosystemOrm(cfg) {
|
|
904
|
+
switch (cfg.ecosystem) {
|
|
905
|
+
case "rust": return cfg.rustOrm;
|
|
906
|
+
case "python": return cfg.pythonOrm;
|
|
907
|
+
case "go": return cfg.goOrm;
|
|
908
|
+
case "java": return cfg.javaOrm;
|
|
909
|
+
case "dotnet": return cfg.dotnetOrm;
|
|
910
|
+
case "elixir": return cfg.elixirOrm;
|
|
911
|
+
default: return;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function getEcosystemBackend(cfg) {
|
|
915
|
+
switch (cfg.ecosystem) {
|
|
916
|
+
case "rust": return cfg.rustWebFramework && cfg.rustWebFramework !== "none" ? cfg.rustWebFramework : void 0;
|
|
917
|
+
case "python": return cfg.pythonWebFramework && cfg.pythonWebFramework !== "none" ? cfg.pythonWebFramework : void 0;
|
|
918
|
+
case "go": return cfg.goWebFramework && cfg.goWebFramework !== "none" ? cfg.goWebFramework : void 0;
|
|
919
|
+
case "java": return cfg.javaWebFramework && cfg.javaWebFramework !== "none" ? cfg.javaWebFramework : void 0;
|
|
920
|
+
case "dotnet": return cfg.dotnetWebFramework && cfg.dotnetWebFramework !== "none" ? cfg.dotnetWebFramework : void 0;
|
|
921
|
+
case "elixir": return cfg.elixirWebFramework && cfg.elixirWebFramework !== "none" ? cfg.elixirWebFramework : void 0;
|
|
922
|
+
default: return;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function validateDatabaseSetup(config, providedFlags) {
|
|
926
|
+
const { dbSetup, database, runtime } = config;
|
|
927
|
+
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'.");
|
|
928
|
+
const setupValidations = {
|
|
929
|
+
turso: {
|
|
930
|
+
database: "sqlite",
|
|
931
|
+
errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
|
|
932
|
+
},
|
|
933
|
+
neon: {
|
|
934
|
+
database: "postgres",
|
|
935
|
+
errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
936
|
+
},
|
|
937
|
+
"prisma-postgres": {
|
|
938
|
+
database: "postgres",
|
|
939
|
+
errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
940
|
+
},
|
|
941
|
+
planetscale: { errorMessage: "PlanetScale setup requires PostgreSQL or MySQL database. Please use '--database postgres' or '--database mysql' or choose a different setup." },
|
|
942
|
+
"mongodb-atlas": {
|
|
943
|
+
database: "mongodb",
|
|
944
|
+
errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
|
|
945
|
+
},
|
|
946
|
+
upstash: {
|
|
947
|
+
database: "redis",
|
|
948
|
+
errorMessage: "Upstash setup requires Redis database. Please use '--database redis' or choose a different setup."
|
|
949
|
+
},
|
|
950
|
+
supabase: {
|
|
951
|
+
database: "postgres",
|
|
952
|
+
errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
953
|
+
},
|
|
954
|
+
d1: {
|
|
955
|
+
database: "sqlite",
|
|
956
|
+
runtime: "workers",
|
|
957
|
+
errorMessage: "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime."
|
|
958
|
+
},
|
|
959
|
+
docker: { errorMessage: "In Better-Fullstack, Docker setup is currently not available with SQLite database or Cloudflare Workers runtime." },
|
|
960
|
+
none: { errorMessage: "" }
|
|
961
|
+
};
|
|
962
|
+
if (dbSetup && dbSetup !== "none") {
|
|
963
|
+
const validation = setupValidations[dbSetup];
|
|
964
|
+
if (dbSetup === "planetscale") {
|
|
965
|
+
if (database !== "postgres" && database !== "mysql") exitWithError(validation.errorMessage);
|
|
966
|
+
} else if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
|
|
967
|
+
if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
|
|
968
|
+
if (dbSetup === "docker") {
|
|
969
|
+
if (database === "sqlite") exitWithError("In Better-Fullstack, Docker setup is currently not available 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.");
|
|
970
|
+
if (runtime === "workers") exitWithError("In Better-Fullstack, Docker setup is currently not available 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.");
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function validateEcosystemAuthCompatibility(config, providedFlags) {
|
|
975
|
+
const auth = config.auth;
|
|
976
|
+
if (!auth || auth === "none") return;
|
|
977
|
+
if ((auth === "better-auth" || auth === "better-auth-organizations") && config.orm && [
|
|
978
|
+
"typeorm",
|
|
979
|
+
"sequelize",
|
|
980
|
+
"mikroorm"
|
|
981
|
+
].includes(config.orm)) {
|
|
982
|
+
config.auth = "none";
|
|
983
|
+
if (providedFlags?.has("auth") && !isSilent()) consola.warn(pc.yellow(`Unsupported auth selection '${auth}' with ${config.orm}: no Better Auth adapter exists. Falling back to '--auth none'.`));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const normalized = (0, types_exports.normalizeCapabilitySelection)("auth", {
|
|
987
|
+
ecosystem: config.ecosystem,
|
|
988
|
+
backend: config.backend,
|
|
989
|
+
frontend: config.frontend
|
|
990
|
+
}, auth);
|
|
991
|
+
if (!normalized.normalized || normalized.value === auth) return;
|
|
992
|
+
config.auth = normalized.value;
|
|
993
|
+
if (providedFlags?.has("auth") && normalized.reason && !isSilent()) consola.warn(pc.yellow(`Unsupported auth selection '${auth}' for the current stack: ${normalized.reason}. Falling back to '--auth ${normalized.value}'.`));
|
|
994
|
+
}
|
|
995
|
+
function validateConvexConstraints(config, providedFlags) {
|
|
996
|
+
const { backend } = config;
|
|
997
|
+
if (backend !== "convex") return;
|
|
998
|
+
const has = (k) => providedFlags.has(k);
|
|
999
|
+
if (has("runtime") && config.runtime !== "none") constraintError({
|
|
1000
|
+
message: "Convex backend manages its own runtime.",
|
|
1001
|
+
provided: {
|
|
1002
|
+
backend: "convex",
|
|
1003
|
+
runtime: config.runtime || ""
|
|
1004
|
+
},
|
|
1005
|
+
suggestions: ["Remove --runtime flag", "Set --runtime none"]
|
|
1006
|
+
});
|
|
1007
|
+
if (has("database") && config.database !== "none") constraintError({
|
|
1008
|
+
message: "Convex backend has its own built-in database.",
|
|
1009
|
+
provided: {
|
|
1010
|
+
backend: "convex",
|
|
1011
|
+
database: config.database || ""
|
|
1012
|
+
},
|
|
1013
|
+
suggestions: ["Remove --database flag", "Set --database none"]
|
|
1014
|
+
});
|
|
1015
|
+
if (has("orm") && config.orm !== "none") constraintError({
|
|
1016
|
+
message: "Convex backend has its own data layer (no ORM needed).",
|
|
1017
|
+
provided: {
|
|
1018
|
+
backend: "convex",
|
|
1019
|
+
orm: config.orm || ""
|
|
1020
|
+
},
|
|
1021
|
+
suggestions: ["Remove --orm flag", "Set --orm none"]
|
|
1022
|
+
});
|
|
1023
|
+
if (has("api") && config.api !== "none") constraintError({
|
|
1024
|
+
message: "Convex backend has its own built-in API layer.",
|
|
1025
|
+
provided: {
|
|
1026
|
+
backend: "convex",
|
|
1027
|
+
api: config.api || ""
|
|
1028
|
+
},
|
|
1029
|
+
suggestions: ["Remove --api flag", "Set --api none"]
|
|
1030
|
+
});
|
|
1031
|
+
if (has("dbSetup") && config.dbSetup !== "none") constraintError({
|
|
1032
|
+
message: "Convex backend manages its own database infrastructure.",
|
|
1033
|
+
provided: {
|
|
1034
|
+
backend: "convex",
|
|
1035
|
+
"db-setup": config.dbSetup || ""
|
|
1036
|
+
},
|
|
1037
|
+
suggestions: ["Remove --db-setup flag", "Set --db-setup none"]
|
|
1038
|
+
});
|
|
1039
|
+
if (has("serverDeploy") && config.serverDeploy !== "none") constraintError({
|
|
1040
|
+
message: "Convex backend has its own deployment platform.",
|
|
1041
|
+
provided: {
|
|
1042
|
+
backend: "convex",
|
|
1043
|
+
"server-deploy": config.serverDeploy || ""
|
|
1044
|
+
},
|
|
1045
|
+
suggestions: ["Remove --server-deploy flag", "Set --server-deploy none"]
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function validateBackendNoneConstraints(config, providedFlags) {
|
|
1049
|
+
const { backend } = config;
|
|
1050
|
+
const hasGraphBackend = config.stackParts?.some((part) => part.role === "backend" && !part.ownerPartId && part.source !== "provided" && part.ecosystem !== "typescript" && part.ecosystem !== "react-native" && part.ecosystem !== "universal");
|
|
1051
|
+
const hasEcosystemBackend = getEcosystemBackend(config) !== void 0;
|
|
1052
|
+
if (backend !== "none" || hasGraphBackend || hasEcosystemBackend) return;
|
|
1053
|
+
const has = (k) => providedFlags.has(k);
|
|
1054
|
+
if (has("runtime") && config.runtime !== "none") exitWithError("Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
|
|
1055
|
+
if (has("database") && config.database !== "none") exitWithError("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
|
|
1056
|
+
if (has("orm") && config.orm !== "none") exitWithError("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
|
|
1057
|
+
if (has("api") && config.api !== "none") exitWithError("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
|
|
1058
|
+
if (has("payments") && config.payments !== "none") exitWithError("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
|
|
1059
|
+
if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
|
|
1060
|
+
if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
|
|
1061
|
+
}
|
|
1062
|
+
function validateSelfBackendConstraints(config, providedFlags) {
|
|
1063
|
+
const { backend } = config;
|
|
1064
|
+
if (backend !== "self") return;
|
|
1065
|
+
const has = (k) => providedFlags.has(k);
|
|
1066
|
+
if (has("runtime") && config.runtime !== "none") exitWithError("Backend 'self' (fullstack) requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
|
|
1067
|
+
}
|
|
1068
|
+
function validateEncoreConstraints(config, providedFlags) {
|
|
1069
|
+
const { backend } = config;
|
|
1070
|
+
if (backend !== "encore") return;
|
|
1071
|
+
const has = (k) => providedFlags.has(k);
|
|
1072
|
+
if (has("runtime") && config.runtime !== "none") exitWithError("Encore.ts backend requires '--runtime none'. Encore has its own runtime via the Encore CLI. Please remove the --runtime flag or set it to 'none'.");
|
|
1073
|
+
if (has("api") && config.api !== "none") exitWithError("Encore.ts backend requires '--api none'. Encore has its own type-safe API system. Please remove the --api flag or set it to 'none'.");
|
|
1074
|
+
if (has("database") && config.database !== "none") exitWithError("Encore.ts backend requires '--database none'. Encore manages databases through its infrastructure primitives. Please remove the --database flag or set it to 'none'.");
|
|
1075
|
+
if (has("orm") && config.orm !== "none") exitWithError("Encore.ts backend requires '--orm none'. Encore has its own database abstractions. Please remove the --orm flag or set it to 'none'.");
|
|
1076
|
+
if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Encore.ts backend requires '--db-setup none'. Encore manages infrastructure automatically. Please remove the --db-setup flag or set it to 'none'.");
|
|
1077
|
+
if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Encore.ts backend requires '--server-deploy none'. Encore has its own deployment platform. Please remove the --server-deploy flag or set it to 'none'.");
|
|
1078
|
+
}
|
|
1079
|
+
function validateAdonisJSConstraints(config, providedFlags) {
|
|
1080
|
+
const { backend } = config;
|
|
1081
|
+
if (backend !== "adonisjs") return;
|
|
1082
|
+
const has = (k) => providedFlags.has(k);
|
|
1083
|
+
if (has("runtime") && config.runtime !== "node") exitWithError("AdonisJS backend requires '--runtime node'. AdonisJS currently only supports Node.js runtime. Please use '--runtime node' or remove the --runtime flag.");
|
|
1084
|
+
}
|
|
1085
|
+
function validateBackendConstraints(config, providedFlags, options) {
|
|
1086
|
+
const { backend } = config;
|
|
1087
|
+
if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none" && backend !== "self" && backend !== "encore") {
|
|
1088
|
+
if (providedFlags.has("runtime") && options.runtime === "none") exitWithError("'--runtime none' is only supported with '--backend convex', '--backend none', '--backend self', or '--backend encore'. Please choose 'bun', 'node', or remove the --runtime flag.");
|
|
1089
|
+
}
|
|
1090
|
+
if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
|
|
1091
|
+
const incompatibleFrontends = options.frontend.filter((f) => ["solid", "astro"].includes(f));
|
|
1092
|
+
if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function validateFrontendConstraints(config, providedFlags) {
|
|
1096
|
+
const { frontend } = config;
|
|
1097
|
+
if (frontend && frontend.length > 0) {
|
|
1098
|
+
if (config.ecosystem === "react-native" && frontend.some((item) => !item.startsWith("native-") && item !== "none")) incompatibilityError({
|
|
1099
|
+
message: "React Native ecosystem only supports native Expo frontends.",
|
|
1100
|
+
provided: {
|
|
1101
|
+
ecosystem: "react-native",
|
|
1102
|
+
frontend: frontend.join(" ")
|
|
1103
|
+
},
|
|
1104
|
+
suggestions: [
|
|
1105
|
+
"Use --frontend native-bare",
|
|
1106
|
+
"Use --frontend native-uniwind",
|
|
1107
|
+
"Use --frontend native-unistyles"
|
|
1108
|
+
]
|
|
1109
|
+
});
|
|
1110
|
+
ensureSingleWebAndNative(frontend);
|
|
1111
|
+
if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend, config.astroIntegration);
|
|
1112
|
+
}
|
|
1113
|
+
const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
|
|
1114
|
+
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
1115
|
+
validateWebDeployFrontendTemplates(config.webDeploy, frontend);
|
|
1116
|
+
}
|
|
1117
|
+
function validateApiConstraints(config, _options) {
|
|
1118
|
+
if (config.api !== "openapi" && config.api !== "apollo-server") return;
|
|
1119
|
+
const apiDisplayName = config.api === "apollo-server" ? "Apollo Server" : "OpenAPI";
|
|
1120
|
+
const frontend = config.frontend ?? [];
|
|
1121
|
+
if (frontend.some((item) => [
|
|
1122
|
+
"native-bare",
|
|
1123
|
+
"native-uniwind",
|
|
1124
|
+
"native-unistyles"
|
|
1125
|
+
].includes(item))) incompatibilityError({
|
|
1126
|
+
message: `${apiDisplayName} is currently available for web frontends, not React Native.`,
|
|
1127
|
+
provided: {
|
|
1128
|
+
api: config.api,
|
|
1129
|
+
frontend
|
|
1130
|
+
},
|
|
1131
|
+
suggestions: [
|
|
1132
|
+
"Use --api trpc",
|
|
1133
|
+
"Use --api orpc",
|
|
1134
|
+
"Use a web frontend"
|
|
1135
|
+
]
|
|
1136
|
+
});
|
|
1137
|
+
if (!config.backend || ![
|
|
1138
|
+
"hono",
|
|
1139
|
+
"express",
|
|
1140
|
+
"fastify",
|
|
1141
|
+
"elysia"
|
|
1142
|
+
].includes(config.backend)) incompatibilityError({
|
|
1143
|
+
message: `${apiDisplayName} currently supports Hono, Express, Fastify, and Elysia backends.`,
|
|
1144
|
+
provided: {
|
|
1145
|
+
api: config.api,
|
|
1146
|
+
backend: config.backend ?? "none"
|
|
1147
|
+
},
|
|
1148
|
+
suggestions: [
|
|
1149
|
+
"Use --backend hono",
|
|
1150
|
+
"Use --backend express",
|
|
1151
|
+
"Use --backend fastify",
|
|
1152
|
+
"Use --backend elysia"
|
|
1153
|
+
]
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set()) {
|
|
1157
|
+
if (config.ecosystem !== "java") return;
|
|
1158
|
+
const hasSpringBoot = config.javaWebFramework === "spring-boot";
|
|
1159
|
+
const hasJavaWebFramework = config.javaWebFramework !== "none";
|
|
1160
|
+
const hasNoBuildTool = config.javaBuildTool === "none";
|
|
1161
|
+
const hasJavaLibraries = (config.javaLibraries ?? []).some((library) => library !== "none");
|
|
1162
|
+
const hasJavaTestingLibraries = (config.javaTestingLibraries ?? []).some((library) => library !== "none");
|
|
1163
|
+
const hasSpringOnlyFeatures = config.javaOrm !== "none" || config.javaAuth !== "none" || hasJavaLibraries;
|
|
1164
|
+
if (hasNoBuildTool && hasJavaWebFramework) incompatibilityError({
|
|
1165
|
+
message: "Java web frameworks require Maven or Gradle in the Java scaffold.",
|
|
1166
|
+
provided: {
|
|
1167
|
+
"java-web-framework": config.javaWebFramework ?? "none",
|
|
1168
|
+
"java-build-tool": config.javaBuildTool ?? "none"
|
|
1169
|
+
},
|
|
1170
|
+
suggestions: ["Use --java-build-tool maven or --java-build-tool gradle with Java web frameworks", "Use --java-web-framework none for a plain Java source-only scaffold"]
|
|
1171
|
+
});
|
|
1172
|
+
if ((!hasSpringBoot || hasNoBuildTool) && hasSpringOnlyFeatures) incompatibilityError({
|
|
1173
|
+
message: "Spring-only Java features require the Spring Boot scaffold with Maven or Gradle.",
|
|
1174
|
+
provided: {
|
|
1175
|
+
"java-web-framework": config.javaWebFramework ?? "none",
|
|
1176
|
+
"java-build-tool": config.javaBuildTool ?? "none",
|
|
1177
|
+
"java-orm": config.javaOrm ?? "none",
|
|
1178
|
+
"java-auth": config.javaAuth ?? "none",
|
|
1179
|
+
"java-libraries": (config.javaLibraries ?? []).join(" ") || "none"
|
|
1180
|
+
},
|
|
1181
|
+
suggestions: ["Use --java-web-framework spring-boot and a real build tool for Spring features", "Clear --java-orm, --java-auth, and --java-libraries when using plain Java or Quarkus"]
|
|
1182
|
+
});
|
|
1183
|
+
if (hasNoBuildTool && hasJavaTestingLibraries) incompatibilityError({
|
|
1184
|
+
message: "Java testing libraries require Maven or Gradle to manage test dependencies.",
|
|
1185
|
+
provided: {
|
|
1186
|
+
"java-build-tool": config.javaBuildTool ?? "none",
|
|
1187
|
+
"java-testing-libraries": (config.javaTestingLibraries ?? []).join(" ") || "none"
|
|
1188
|
+
},
|
|
1189
|
+
suggestions: ["Use --java-build-tool maven or --java-build-tool gradle to enable JUnit/Mockito/Testcontainers", "Set --java-testing-libraries none for a source-only plain Java scaffold"]
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
function validateElixirConstraints(config) {
|
|
1193
|
+
if (config.ecosystem !== "elixir") return;
|
|
1194
|
+
const hasPhoenix = config.elixirWebFramework !== "none";
|
|
1195
|
+
const hasEcto = config.elixirOrm !== "none";
|
|
1196
|
+
const hasEctoSql = config.elixirOrm === "ecto-sql";
|
|
1197
|
+
if (!hasPhoenix) {
|
|
1198
|
+
const phoenixOnlySelections = [
|
|
1199
|
+
{
|
|
1200
|
+
flag: "elixir-auth",
|
|
1201
|
+
value: config.elixirAuth,
|
|
1202
|
+
message: "Elixir auth scaffolds require Phoenix."
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
flag: "elixir-api",
|
|
1206
|
+
value: config.elixirApi,
|
|
1207
|
+
message: "Elixir API scaffolds require Phoenix."
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
flag: "elixir-realtime",
|
|
1211
|
+
value: config.elixirRealtime,
|
|
1212
|
+
message: "Elixir realtime scaffolds require Phoenix."
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
flag: "elixir-testing",
|
|
1216
|
+
value: config.elixirTesting === "wallaby" ? config.elixirTesting : "none",
|
|
1217
|
+
message: "Wallaby browser tests require Phoenix."
|
|
1218
|
+
}
|
|
1219
|
+
];
|
|
1220
|
+
for (const selection of phoenixOnlySelections) {
|
|
1221
|
+
if (!selection.value || selection.value === "none") continue;
|
|
1222
|
+
incompatibilityError({
|
|
1223
|
+
message: selection.message,
|
|
1224
|
+
provided: {
|
|
1225
|
+
"elixir-web-framework": config.elixirWebFramework ?? "none",
|
|
1226
|
+
[selection.flag]: selection.value
|
|
1227
|
+
},
|
|
1228
|
+
suggestions: [
|
|
1229
|
+
"Use --elixir-web-framework phoenix",
|
|
1230
|
+
"Use --elixir-web-framework phoenix-live-view",
|
|
1231
|
+
`Use --${selection.flag} none`
|
|
1232
|
+
]
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (hasPhoenix && config.elixirJson === "none") incompatibilityError({
|
|
1237
|
+
message: "Phoenix JSON scaffolds require Jason.",
|
|
1238
|
+
provided: { "elixir-json": "none" },
|
|
1239
|
+
suggestions: ["Use --elixir-json jason"]
|
|
1240
|
+
});
|
|
1241
|
+
if (config.elixirAuth === "phx-gen-auth" && !hasEctoSql) incompatibilityError({
|
|
1242
|
+
message: "phx.gen.auth requires Ecto SQL with PostgreSQL in the generated Phoenix scaffold.",
|
|
1243
|
+
provided: {
|
|
1244
|
+
"elixir-auth": "phx-gen-auth",
|
|
1245
|
+
"elixir-orm": config.elixirOrm ?? "none"
|
|
1246
|
+
},
|
|
1247
|
+
suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-auth none"]
|
|
1248
|
+
});
|
|
1249
|
+
if (config.elixirJobs === "oban" && config.elixirOrm !== "ecto-sql") incompatibilityError({
|
|
1250
|
+
message: "Oban requires Ecto SQL with PostgreSQL in the generated Phoenix scaffold.",
|
|
1251
|
+
provided: {
|
|
1252
|
+
"elixir-jobs": "oban",
|
|
1253
|
+
"elixir-orm": config.elixirOrm ?? "none"
|
|
1254
|
+
},
|
|
1255
|
+
suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-jobs none"]
|
|
1256
|
+
});
|
|
1257
|
+
if (config.elixirRealtime === "live-view-streams" && config.elixirWebFramework !== "phoenix-live-view") incompatibilityError({
|
|
1258
|
+
message: "LiveView Streams require Phoenix LiveView.",
|
|
1259
|
+
provided: {
|
|
1260
|
+
"elixir-realtime": "live-view-streams",
|
|
1261
|
+
"elixir-web-framework": config.elixirWebFramework ?? "none"
|
|
1262
|
+
},
|
|
1263
|
+
suggestions: ["Use --elixir-web-framework phoenix-live-view", "Use --elixir-realtime channels"]
|
|
1264
|
+
});
|
|
1265
|
+
if (config.elixirApi === "absinthe" && !hasEcto) incompatibilityError({
|
|
1266
|
+
message: "Absinthe GraphQL requires Ecto in the current generated Phoenix scaffold.",
|
|
1267
|
+
provided: {
|
|
1268
|
+
"elixir-api": "absinthe",
|
|
1269
|
+
"elixir-orm": config.elixirOrm ?? "none"
|
|
1270
|
+
},
|
|
1271
|
+
suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-api rest"]
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
function validateEmailConstraints(config) {
|
|
1275
|
+
if (!config.email || config.email === "none") return;
|
|
1276
|
+
if (config.ecosystem !== "typescript" && config.email !== "resend") incompatibilityError({
|
|
1277
|
+
message: "Only Resend email is available for non-TypeScript ecosystems.",
|
|
1278
|
+
provided: {
|
|
1279
|
+
ecosystem: config.ecosystem ?? "typescript",
|
|
1280
|
+
email: config.email
|
|
1281
|
+
},
|
|
1282
|
+
suggestions: ["Use --email resend", "Use --email none"]
|
|
1283
|
+
});
|
|
1284
|
+
if (config.ecosystem === "java" && config.email === "resend" && config.javaBuildTool === "none") incompatibilityError({
|
|
1285
|
+
message: "Resend email for Java requires Maven or Gradle to manage the SDK dependency.",
|
|
1286
|
+
provided: {
|
|
1287
|
+
"java-build-tool": "none",
|
|
1288
|
+
email: "resend"
|
|
1289
|
+
},
|
|
1290
|
+
suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
function validateObservabilityConstraints(config) {
|
|
1294
|
+
if (!config.observability || config.observability === "none") return;
|
|
1295
|
+
if (config.ecosystem !== "typescript" && config.observability !== "sentry") incompatibilityError({
|
|
1296
|
+
message: "Only Sentry observability is available for non-TypeScript ecosystems.",
|
|
1297
|
+
provided: {
|
|
1298
|
+
ecosystem: config.ecosystem ?? "typescript",
|
|
1299
|
+
observability: config.observability
|
|
1300
|
+
},
|
|
1301
|
+
suggestions: ["Use --observability sentry", "Use --observability none"]
|
|
1302
|
+
});
|
|
1303
|
+
if (config.ecosystem === "java" && config.observability === "sentry" && config.javaBuildTool === "none") incompatibilityError({
|
|
1304
|
+
message: "Sentry observability for Java requires Maven or Gradle to manage the SDK dependency.",
|
|
1305
|
+
provided: {
|
|
1306
|
+
"java-build-tool": "none",
|
|
1307
|
+
observability: "sentry"
|
|
1308
|
+
},
|
|
1309
|
+
suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
function validateCachingConstraints(config) {
|
|
1313
|
+
if (!config.caching || config.caching === "none") return;
|
|
1314
|
+
if (config.ecosystem !== "typescript" && config.caching !== "upstash-redis") incompatibilityError({
|
|
1315
|
+
message: "Only Upstash Redis caching is available for non-TypeScript ecosystems.",
|
|
1316
|
+
provided: {
|
|
1317
|
+
ecosystem: config.ecosystem ?? "typescript",
|
|
1318
|
+
caching: config.caching
|
|
1319
|
+
},
|
|
1320
|
+
suggestions: ["Use --caching upstash-redis", "Use --caching none"]
|
|
1321
|
+
});
|
|
1322
|
+
if (config.ecosystem === "java" && config.caching === "upstash-redis" && config.javaBuildTool === "none") incompatibilityError({
|
|
1323
|
+
message: "Upstash Redis caching for Java requires Maven or Gradle to manage the Redis client dependency.",
|
|
1324
|
+
provided: {
|
|
1325
|
+
"java-build-tool": "none",
|
|
1326
|
+
caching: "upstash-redis"
|
|
1327
|
+
},
|
|
1328
|
+
suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
function validateRateLimitConstraints(config) {
|
|
1332
|
+
if (!config.rateLimit || config.rateLimit === "none") return;
|
|
1333
|
+
if (config.ecosystem !== "typescript") incompatibilityError({
|
|
1334
|
+
message: "Rate limiting helpers are currently available for TypeScript stacks only.",
|
|
1335
|
+
provided: {
|
|
1336
|
+
ecosystem: config.ecosystem ?? "typescript",
|
|
1337
|
+
"rate-limit": config.rateLimit
|
|
1338
|
+
},
|
|
1339
|
+
suggestions: ["Use --rate-limit none"]
|
|
1340
|
+
});
|
|
1341
|
+
if (config.backend === "convex") incompatibilityError({
|
|
1342
|
+
message: "Rate limiting helpers are not generated with Convex backend.",
|
|
1343
|
+
provided: {
|
|
1344
|
+
backend: "convex",
|
|
1345
|
+
"rate-limit": config.rateLimit
|
|
1346
|
+
},
|
|
1347
|
+
suggestions: ["Use --rate-limit none"]
|
|
1348
|
+
});
|
|
1349
|
+
if (config.backend === "none") incompatibilityError({
|
|
1350
|
+
message: "Rate limiting requires a backend.",
|
|
1351
|
+
provided: {
|
|
1352
|
+
backend: "none",
|
|
1353
|
+
"rate-limit": config.rateLimit
|
|
1354
|
+
},
|
|
1355
|
+
suggestions: ["Use --backend hono", "Use --rate-limit none"]
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
function validateSearchConstraints(config) {
|
|
1359
|
+
if (!config.search || config.search === "none") return;
|
|
1360
|
+
const ecosystem = config.ecosystem ?? "typescript";
|
|
1361
|
+
if (ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
|
|
1362
|
+
message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
|
|
1363
|
+
provided: {
|
|
1364
|
+
ecosystem,
|
|
1365
|
+
search: config.search
|
|
1366
|
+
},
|
|
1367
|
+
suggestions: ["Use --search meilisearch", "Use --search none"]
|
|
1368
|
+
});
|
|
1369
|
+
if (config.ecosystem === "java" && config.search === "meilisearch" && config.javaBuildTool === "none") incompatibilityError({
|
|
1370
|
+
message: "Meilisearch search for Java requires Maven or Gradle to manage the SDK dependency.",
|
|
1371
|
+
provided: {
|
|
1372
|
+
"java-build-tool": "none",
|
|
1373
|
+
search: "meilisearch"
|
|
1374
|
+
},
|
|
1375
|
+
suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
function validateShadcnConstraints(config, providedFlags) {
|
|
1379
|
+
const shadcnFlagMap = {
|
|
1380
|
+
shadcnBase: "--shadcn-base",
|
|
1381
|
+
shadcnStyle: "--shadcn-style",
|
|
1382
|
+
shadcnIconLibrary: "--shadcn-icon-library",
|
|
1383
|
+
shadcnColorTheme: "--shadcn-color-theme",
|
|
1384
|
+
shadcnBaseColor: "--shadcn-base-color",
|
|
1385
|
+
shadcnFont: "--shadcn-font",
|
|
1386
|
+
shadcnRadius: "--shadcn-radius"
|
|
1387
|
+
};
|
|
1388
|
+
const providedShadcnFlags = Object.keys(shadcnFlagMap).filter((f) => providedFlags.has(f));
|
|
1389
|
+
if (providedShadcnFlags.length > 0 && config.uiLibrary !== "shadcn-ui") incompatibilityError({
|
|
1390
|
+
message: "shadcn/ui customization flags require --ui-library shadcn-ui.",
|
|
1391
|
+
provided: {
|
|
1392
|
+
"ui-library": config.uiLibrary || "none",
|
|
1393
|
+
...Object.fromEntries(providedShadcnFlags.map((f) => [shadcnFlagMap[f], String(config[f] ?? "")]))
|
|
1394
|
+
},
|
|
1395
|
+
suggestions: ["Add --ui-library shadcn-ui to use shadcn customization flags", "Remove the --shadcn-* flags if not using shadcn/ui"]
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
function validatePythonApiConstraints(config) {
|
|
1399
|
+
if (config.ecosystem === "python" && config.pythonApi && config.pythonApi !== "none" && config.pythonWebFramework !== "django") incompatibilityError({
|
|
1400
|
+
message: "Python API frameworks require --python-web-framework django.",
|
|
1401
|
+
provided: {
|
|
1402
|
+
"python-web-framework": config.pythonWebFramework || "none",
|
|
1403
|
+
"python-api": config.pythonApi
|
|
1404
|
+
},
|
|
1405
|
+
suggestions: ["Use --python-web-framework django with --python-api django-rest-framework or django-ninja", "Set --python-api none for FastAPI, Flask, Litestar, or no Python web framework"]
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
function validateFullConfig(config, providedFlags, options) {
|
|
1409
|
+
if (config.stackParts && !options.yolo) {
|
|
1410
|
+
const graphValidation = (0, types_exports.validateStackParts)(config.stackParts);
|
|
1411
|
+
if (graphValidation.issues.length > 0) exitWithError(graphValidation.issues.map((issue) => issue.message).join("\n"));
|
|
1412
|
+
}
|
|
1413
|
+
validateEcosystemAuthCompatibility(config, providedFlags);
|
|
1414
|
+
validateDatabaseOrmAuth(config, providedFlags);
|
|
1415
|
+
validateDatabaseSetup(config, providedFlags);
|
|
1416
|
+
validateConvexConstraints(config, providedFlags);
|
|
1417
|
+
validateBackendNoneConstraints(config, providedFlags);
|
|
1418
|
+
validateSelfBackendConstraints(config, providedFlags);
|
|
1419
|
+
validateEncoreConstraints(config, providedFlags);
|
|
1420
|
+
validateAdonisJSConstraints(config, providedFlags);
|
|
1421
|
+
validateBackendConstraints(config, providedFlags, options);
|
|
1422
|
+
validateFrontendConstraints(config, providedFlags);
|
|
1423
|
+
validateApiConstraints(config, options);
|
|
1424
|
+
validatePythonApiConstraints(config);
|
|
1425
|
+
validateEmailConstraints(config);
|
|
1426
|
+
validateObservabilityConstraints(config);
|
|
1427
|
+
validateCachingConstraints(config);
|
|
1428
|
+
validateRateLimitConstraints(config);
|
|
1429
|
+
validateSearchConstraints(config);
|
|
1430
|
+
validateJavaConstraints(config, providedFlags);
|
|
1431
|
+
validateElixirConstraints(config);
|
|
1432
|
+
const hasGraphBackend = config.stackParts?.some((part) => part.role === "backend" && !part.ownerPartId && part.source !== "provided" && part.ecosystem !== "typescript" && part.ecosystem !== "react-native" && part.ecosystem !== "universal");
|
|
1433
|
+
if (!(providedFlags.has("serverDeploy") && !options.yes && !options.part?.length && options.ecosystem === void 0 && options.backend === void 0 && config.stackParts === void 0)) validateServerDeployRequiresBackend(config.serverDeploy, config.backend, Boolean(hasGraphBackend));
|
|
1434
|
+
validateSelfBackendCompatibility(providedFlags, options, config);
|
|
1435
|
+
validateWorkersCompatibility(providedFlags, options, config);
|
|
1436
|
+
if (config.runtime === "workers" && config.serverDeploy === "none") exitWithError("Cloudflare Workers runtime requires a server deployment. Please choose 'alchemy' for --server-deploy.");
|
|
1437
|
+
if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") exitWithError(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
|
|
1438
|
+
if (config.serverDeploy === "vercel" && [
|
|
1439
|
+
"nestjs",
|
|
1440
|
+
"adonisjs",
|
|
1441
|
+
"encore"
|
|
1442
|
+
].includes(config.backend)) incompatibilityError({
|
|
1443
|
+
message: "Vercel serverless functions cannot host persistent-process backends",
|
|
1444
|
+
provided: {
|
|
1445
|
+
backend: config.backend,
|
|
1446
|
+
serverDeploy: config.serverDeploy
|
|
1447
|
+
},
|
|
1448
|
+
suggestions: ["Use --server-deploy fly or --server-deploy railway for NestJS/AdonisJS", "Switch to a serverless-compatible backend like Hono or Express"]
|
|
1449
|
+
});
|
|
1450
|
+
if (config.serverDeploy === "netlify") {
|
|
1451
|
+
if (config.backend !== "hono") incompatibilityError({
|
|
1452
|
+
message: "Netlify Functions server deploy is currently supported only with Hono",
|
|
1453
|
+
provided: {
|
|
1454
|
+
backend: config.backend,
|
|
1455
|
+
serverDeploy: config.serverDeploy
|
|
1456
|
+
},
|
|
1457
|
+
suggestions: ["Use --backend hono", "Use --server-deploy fly, railway, render, or docker for this backend"]
|
|
1458
|
+
});
|
|
1459
|
+
if (config.runtime !== "node") incompatibilityError({
|
|
1460
|
+
message: "Netlify Functions server deploy requires Node.js runtime",
|
|
1461
|
+
provided: {
|
|
1462
|
+
runtime: config.runtime,
|
|
1463
|
+
serverDeploy: config.serverDeploy
|
|
1464
|
+
},
|
|
1465
|
+
suggestions: ["Use --runtime node", "Choose a different server deployment target"]
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
if (config.addons && config.addons.length > 0) {
|
|
1469
|
+
validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime, config.ecosystem, config.rustFrontend, config.javaWebFramework, config.database);
|
|
1470
|
+
config.addons = [...new Set(config.addons)];
|
|
1471
|
+
}
|
|
1472
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.frontend ?? [], config.runtime, config.ai);
|
|
1473
|
+
validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? []);
|
|
1474
|
+
validateAIFrontendCompatibility(config.ai, config.frontend ?? []);
|
|
1475
|
+
validateUILibraryFrontendCompatibility(config.uiLibrary, config.frontend ?? [], config.astroIntegration);
|
|
1476
|
+
validateUILibraryCSSFrameworkCompatibility(config.uiLibrary, config.cssFramework);
|
|
1477
|
+
validateShadcnConstraints(config, providedFlags);
|
|
1478
|
+
validatePeerDependencies(config);
|
|
1479
|
+
}
|
|
1480
|
+
function validateConfigForProgrammaticUse(config) {
|
|
1481
|
+
try {
|
|
1482
|
+
if (config.stackParts) {
|
|
1483
|
+
const graphValidation = (0, types_exports.validateStackParts)(config.stackParts);
|
|
1484
|
+
if (graphValidation.issues.length > 0) throw new Error(graphValidation.issues.map((issue) => issue.message).join("\n"));
|
|
1485
|
+
}
|
|
1486
|
+
validateEcosystemAuthCompatibility(config);
|
|
1487
|
+
validateDatabaseOrmAuth(config);
|
|
1488
|
+
if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
|
|
1489
|
+
validateApiFrontendCompatibility(config.api, config.frontend, config.astroIntegration);
|
|
1490
|
+
validatePythonApiConstraints(config);
|
|
1491
|
+
validateEmailConstraints(config);
|
|
1492
|
+
validateObservabilityConstraints(config);
|
|
1493
|
+
validateCachingConstraints(config);
|
|
1494
|
+
validateRateLimitConstraints(config);
|
|
1495
|
+
validateSearchConstraints(config);
|
|
1496
|
+
validateJavaConstraints(config);
|
|
1497
|
+
validateElixirConstraints(config);
|
|
1498
|
+
validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
|
|
1499
|
+
if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime, config.ecosystem, config.rustFrontend, config.javaWebFramework, config.database);
|
|
1500
|
+
validateExamplesCompatibility(config.examples ?? [], config.backend, config.frontend ?? [], config.runtime, config.ai);
|
|
1501
|
+
validateAIFrontendCompatibility(config.ai, config.frontend ?? []);
|
|
1502
|
+
validateUILibraryFrontendCompatibility(config.uiLibrary, config.frontend ?? [], config.astroIntegration);
|
|
1503
|
+
validateUILibraryCSSFrameworkCompatibility(config.uiLibrary, config.cssFramework);
|
|
1504
|
+
validatePeerDependencies(config);
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
if (error instanceof Error) throw error;
|
|
1507
|
+
throw new Error(String(error), { cause: error });
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
//#endregion
|
|
1512
|
+
export { generateReproducibleCommand as a, getTemplateDescription as i, validateFullConfig as n, CreateCommandInputSchema as o, getTemplateConfig as r, CreateCommandOptionsSchema as s, validateConfigForProgrammaticUse as t };
|