create-better-fullstack 2.1.5 → 2.1.7
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-BNSL6HdM.mjs → add-handler-ztNjzoMN.mjs} +52 -6
- package/dist/addons-setup-DnLjAzTw.mjs +7 -0
- package/dist/{addons-setup-CyrP1IV-.mjs → addons-setup-LaAj43NP.mjs} +10 -24
- package/dist/analytics-DVltG11u.mjs +170 -0
- package/dist/{errors-Cyol8zbN.mjs → bts-config-DQVWvPDs.mjs} +72 -149
- package/dist/cli.mjs +2 -2
- package/dist/{templates-CnTOtKjm.mjs → config-processing-D9-F2Us9.mjs} +57 -2
- package/dist/{doctor-DBoq7bZ9.mjs → doctor-a4ca3SMd.mjs} +3 -2
- package/dist/errors-ns_o2OKg.mjs +86 -0
- package/dist/{file-formatter-B3dsev2l.mjs → file-formatter-gvmrpd-g.mjs} +78 -6
- package/dist/gen-CCClL7Ve.mjs +274 -0
- package/dist/{generated-checks-C8hn9w2i.mjs → generated-checks-BV9jol5h.mjs} +1 -1
- package/dist/index.d.mts +162 -64
- package/dist/index.mjs +17 -8
- package/dist/{install-dependencies-CgNh-aOy.mjs → install-dependencies-RoUyaE8o.mjs} +80 -21
- package/dist/mcp-492OkjcS.mjs +11 -0
- package/dist/mcp-entry.mjs +205 -30
- package/dist/prompt-environment-BR0Kkw2W.mjs +26 -0
- package/dist/registry-DSf2CEaU.mjs +408 -0
- package/dist/{run-BYse4yJy.mjs → run-D9I7NdES.mjs} +2081 -464
- package/dist/run-buK4aZS8.mjs +15 -0
- package/dist/scaffold-manifest-DGRyepdb.mjs +4 -0
- package/dist/scaffold-manifest-Dyi0voqE.mjs +135 -0
- package/dist/update-DJ8CI5KW.mjs +401 -0
- package/package.json +3 -3
- package/dist/addons-setup-DyMm42a5.mjs +0 -5
- package/dist/mcp-CsW3i66c.mjs +0 -6
- package/dist/run-_cf_sFwM.mjs +0 -10
- /package/dist/{update-deps-D5OG0KmJ.mjs → update-deps-aD-iQw4U.mjs} +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { b as types_exports } from "./bts-config-DQVWvPDs.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { cliInputToProjectConfigPartial } from "@better-fullstack/types/stack-translation";
|
|
3
5
|
|
|
4
6
|
//#region src/utils/generate-reproducible-command.ts
|
|
5
7
|
function getBaseCommand(packageManager) {
|
|
@@ -21,6 +23,7 @@ function appendCommonFlags(flags, config) {
|
|
|
21
23
|
else flags.push("--ai-docs none");
|
|
22
24
|
flags.push(config.git ? "--git" : "--no-git");
|
|
23
25
|
flags.push(`--package-manager ${config.packageManager}`);
|
|
26
|
+
if (config.workspaceShape && config.workspaceShape !== "monorepo") flags.push(`--workspace-shape ${config.workspaceShape}`);
|
|
24
27
|
if (config.versionChannel !== "stable") flags.push(`--version-channel ${config.versionChannel}`);
|
|
25
28
|
flags.push(config.install ? "--install" : "--no-install");
|
|
26
29
|
}
|
|
@@ -326,6 +329,7 @@ function getGoFlags(config) {
|
|
|
326
329
|
function getJavaFlags(config) {
|
|
327
330
|
const flags = ["--ecosystem java"];
|
|
328
331
|
flags.push(`--java-web-framework ${config.javaWebFramework}`);
|
|
332
|
+
if (config.javaLanguage === "kotlin") flags.push(`--java-language ${config.javaLanguage}`);
|
|
329
333
|
flags.push(`--java-build-tool ${config.javaBuildTool}`);
|
|
330
334
|
flags.push(`--java-orm ${config.javaOrm}`);
|
|
331
335
|
flags.push(`--java-auth ${config.javaAuth}`);
|
|
@@ -460,6 +464,21 @@ const TEMPLATE_PRESETS = {
|
|
|
460
464
|
webDeploy: "none",
|
|
461
465
|
serverDeploy: "none"
|
|
462
466
|
},
|
|
467
|
+
saas: {
|
|
468
|
+
database: "postgres",
|
|
469
|
+
orm: "drizzle",
|
|
470
|
+
backend: "self",
|
|
471
|
+
runtime: "none",
|
|
472
|
+
frontend: ["next"],
|
|
473
|
+
api: "trpc",
|
|
474
|
+
auth: "better-auth-organizations",
|
|
475
|
+
payments: "creem",
|
|
476
|
+
addons: ["turborepo"],
|
|
477
|
+
examples: ["none"],
|
|
478
|
+
dbSetup: "none",
|
|
479
|
+
webDeploy: "none",
|
|
480
|
+
serverDeploy: "none"
|
|
481
|
+
},
|
|
463
482
|
uniwind: {
|
|
464
483
|
database: "none",
|
|
465
484
|
orm: "none",
|
|
@@ -488,10 +507,46 @@ function getTemplateDescription(template) {
|
|
|
488
507
|
mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
|
|
489
508
|
pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
|
|
490
509
|
t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
|
|
510
|
+
saas: "SaaS Starter - Next.js + tRPC + Drizzle + PostgreSQL + Better Auth Organizations + Creem billing",
|
|
491
511
|
uniwind: "Expo + Uniwind native app with no backend services",
|
|
492
512
|
none: "No template - Full customization"
|
|
493
513
|
}[template] || "";
|
|
494
514
|
}
|
|
495
515
|
|
|
496
516
|
//#endregion
|
|
497
|
-
|
|
517
|
+
//#region src/utils/config-processing.ts
|
|
518
|
+
function deriveProjectName(projectName, projectDirectory) {
|
|
519
|
+
if (projectName) return projectName;
|
|
520
|
+
if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
|
|
521
|
+
return "";
|
|
522
|
+
}
|
|
523
|
+
function processFlags(options, projectName) {
|
|
524
|
+
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
|
525
|
+
return cliInputToProjectConfigPartial(options, projectName || derivedName);
|
|
526
|
+
}
|
|
527
|
+
function applyEffectBackendDefaults(config, providedFlags = /* @__PURE__ */ new Set()) {
|
|
528
|
+
if (config.backend !== "effect") return config;
|
|
529
|
+
if (!providedFlags.has("effect")) config.effect = "effect-full";
|
|
530
|
+
if (!providedFlags.has("validation")) config.validation = "effect-schema";
|
|
531
|
+
return config;
|
|
532
|
+
}
|
|
533
|
+
function getProvidedFlags(options) {
|
|
534
|
+
return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
|
|
535
|
+
}
|
|
536
|
+
function validateNoneExclusivity(options, optionName) {
|
|
537
|
+
if (!options || options.length === 0) return;
|
|
538
|
+
if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
|
|
539
|
+
}
|
|
540
|
+
function validateArrayOptions(options) {
|
|
541
|
+
validateNoneExclusivity(options.frontend, "frontend options");
|
|
542
|
+
validateNoneExclusivity(options.addons, "addons");
|
|
543
|
+
validateNoneExclusivity(options.examples, "examples");
|
|
544
|
+
validateNoneExclusivity(options.aiDocs, "ai docs");
|
|
545
|
+
validateNoneExclusivity(options.rustLibraries, "rust libraries");
|
|
546
|
+
validateNoneExclusivity(options.pythonAi, "python ai libraries");
|
|
547
|
+
validateNoneExclusivity(options.javaLibraries, "java libraries");
|
|
548
|
+
validateNoneExclusivity(options.javaTestingLibraries, "java testing libraries");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
export { getTemplateConfig as a, validateArrayOptions as i, getProvidedFlags as n, getTemplateDescription as o, processFlags as r, generateReproducibleCommand as s, applyEffectBackendDefaults as t };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { r as readBtsConfig } from "./bts-config-DQVWvPDs.mjs";
|
|
3
3
|
import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { a as handleError } from "./errors-ns_o2OKg.mjs";
|
|
5
|
+
import { t as runGeneratedChecks } from "./generated-checks-BV9jol5h.mjs";
|
|
5
6
|
import { intro, log, spinner } from "@clack/prompts";
|
|
6
7
|
import pc from "picocolors";
|
|
7
8
|
import fs from "fs-extra";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cancel } from "@clack/prompts";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
|
|
7
|
+
//#region src/utils/context.ts
|
|
8
|
+
const cliStorage = new AsyncLocalStorage();
|
|
9
|
+
function defaultContext() {
|
|
10
|
+
return {
|
|
11
|
+
navigation: {
|
|
12
|
+
isFirstPrompt: false,
|
|
13
|
+
lastPromptShownUI: false
|
|
14
|
+
},
|
|
15
|
+
silent: false
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function getContext() {
|
|
19
|
+
const ctx = cliStorage.getStore();
|
|
20
|
+
if (!ctx) return defaultContext();
|
|
21
|
+
return ctx;
|
|
22
|
+
}
|
|
23
|
+
function tryGetContext() {
|
|
24
|
+
return cliStorage.getStore();
|
|
25
|
+
}
|
|
26
|
+
function isSilent() {
|
|
27
|
+
return getContext().silent;
|
|
28
|
+
}
|
|
29
|
+
function isFirstPrompt() {
|
|
30
|
+
return getContext().navigation.isFirstPrompt;
|
|
31
|
+
}
|
|
32
|
+
function didLastPromptShowUI() {
|
|
33
|
+
return getContext().navigation.lastPromptShownUI;
|
|
34
|
+
}
|
|
35
|
+
function setIsFirstPrompt(value) {
|
|
36
|
+
const ctx = tryGetContext();
|
|
37
|
+
if (ctx) ctx.navigation.isFirstPrompt = value;
|
|
38
|
+
}
|
|
39
|
+
function setLastPromptShownUI(value) {
|
|
40
|
+
const ctx = tryGetContext();
|
|
41
|
+
if (ctx) ctx.navigation.lastPromptShownUI = value;
|
|
42
|
+
}
|
|
43
|
+
async function runWithContextAsync(options, fn) {
|
|
44
|
+
const ctx = {
|
|
45
|
+
navigation: {
|
|
46
|
+
isFirstPrompt: false,
|
|
47
|
+
lastPromptShownUI: false
|
|
48
|
+
},
|
|
49
|
+
silent: options.silent ?? false
|
|
50
|
+
};
|
|
51
|
+
return cliStorage.run(ctx, fn);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/utils/errors.ts
|
|
56
|
+
var UserCancelledError = class extends Error {
|
|
57
|
+
constructor(message = "Operation cancelled") {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "UserCancelledError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var CLIError = class extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "CLIError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function exitWithError(message) {
|
|
69
|
+
if (isSilent()) throw new CLIError(message);
|
|
70
|
+
consola.error(pc.red(message));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
function exitCancelled(message = "Operation cancelled") {
|
|
74
|
+
if (isSilent()) throw new UserCancelledError(message);
|
|
75
|
+
cancel(pc.red(message));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
function handleError(error, fallbackMessage) {
|
|
79
|
+
const message = error instanceof Error ? error.message : fallbackMessage || String(error);
|
|
80
|
+
if (isSilent()) throw error instanceof Error ? error : new Error(message);
|
|
81
|
+
consola.error(pc.red(message));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
export { handleError as a, isSilent as c, setLastPromptShownUI as d, exitWithError as i, runWithContextAsync as l, UserCancelledError as n, didLastPromptShowUI as o, exitCancelled as r, isFirstPrompt as s, CLIError as t, setIsFirstPrompt as u };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { b as types_exports } from "./bts-config-DQVWvPDs.mjs";
|
|
3
|
+
import { c as isSilent, i as exitWithError, t as CLIError } from "./errors-ns_o2OKg.mjs";
|
|
3
4
|
import pc from "picocolors";
|
|
4
5
|
import z from "zod";
|
|
5
6
|
import fs from "fs-extra";
|
|
@@ -59,6 +60,7 @@ const CreateCommandOptionsSchema = z.object({
|
|
|
59
60
|
examples: z.array(types_exports.ExamplesSchema).optional(),
|
|
60
61
|
git: z.boolean().optional(),
|
|
61
62
|
packageManager: types_exports.PackageManagerSchema.optional(),
|
|
63
|
+
workspaceShape: types_exports.WorkspaceShapeSchema.optional().describe("Workspace layout: monorepo (default) or single-app (flat root app; only for a thin self app)"),
|
|
62
64
|
install: z.boolean().optional(),
|
|
63
65
|
versionChannel: types_exports.VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
|
|
64
66
|
dbSetup: types_exports.DatabaseSetupSchema.optional(),
|
|
@@ -121,6 +123,7 @@ const CreateCommandOptionsSchema = z.object({
|
|
|
121
123
|
goConfig: types_exports.GoConfigSchema.optional().describe("Go config management (viper, koanf)"),
|
|
122
124
|
goObservability: types_exports.GoObservabilitySchema.optional().describe("Go observability (opentelemetry)"),
|
|
123
125
|
javaWebFramework: types_exports.JavaWebFrameworkSchema.optional().describe("Java web framework (spring-boot, quarkus, none)"),
|
|
126
|
+
javaLanguage: types_exports.JavaLanguageSchema.optional().describe("JVM language (java, kotlin)"),
|
|
124
127
|
javaBuildTool: types_exports.JavaBuildToolSchema.optional().describe("Java build tool (maven, gradle, none)"),
|
|
125
128
|
javaOrm: types_exports.JavaOrmSchema.optional().describe("Java ORM/database (spring-data-jpa)"),
|
|
126
129
|
javaAuth: types_exports.JavaAuthSchema.optional().describe("Java auth (spring-security)"),
|
|
@@ -426,6 +429,10 @@ function validatePaymentsCompatibility(payments, auth, _backend, frontends = [])
|
|
|
426
429
|
const { web } = splitFrontends$1(frontends);
|
|
427
430
|
if (web.length === 0 && frontends.length > 0) exitWithError("Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.");
|
|
428
431
|
}
|
|
432
|
+
if (payments === "revenuecat") {
|
|
433
|
+
const { native } = splitFrontends$1(frontends);
|
|
434
|
+
if (native.length === 0) exitWithError("RevenueCat payments requires a native frontend. Please select a native frontend or choose a different payments provider.");
|
|
435
|
+
}
|
|
429
436
|
}
|
|
430
437
|
function validateExamplesCompatibility(examples, backend, frontend, runtime, ai) {
|
|
431
438
|
const examplesArr = examples ?? [];
|
|
@@ -677,6 +684,14 @@ function validatePeerDependencies(config) {
|
|
|
677
684
|
|
|
678
685
|
//#endregion
|
|
679
686
|
//#region src/utils/config-validation.ts
|
|
687
|
+
const INTLAYER_COMPATIBLE_FRONTENDS = new Set([
|
|
688
|
+
"next",
|
|
689
|
+
"vinext",
|
|
690
|
+
"tanstack-router",
|
|
691
|
+
"tanstack-start",
|
|
692
|
+
"react-router",
|
|
693
|
+
"react-vite"
|
|
694
|
+
]);
|
|
680
695
|
function validateDatabaseOrmAuth(cfg, flags) {
|
|
681
696
|
const db = cfg.database;
|
|
682
697
|
const orm = cfg.orm;
|
|
@@ -938,7 +953,8 @@ function validateBackendNoneConstraints(config, providedFlags) {
|
|
|
938
953
|
if (has("database") && config.database !== "none") exitWithError("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
|
|
939
954
|
if (has("orm") && config.orm !== "none") exitWithError("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
|
|
940
955
|
if (has("api") && config.api !== "none") exitWithError("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
|
|
941
|
-
|
|
956
|
+
const isNativeRevenueCat = config.payments === "revenuecat" && splitFrontends$1(config.frontend ?? []).native.length > 0;
|
|
957
|
+
if (has("payments") && config.payments !== "none" && !isNativeRevenueCat) exitWithError("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
|
|
942
958
|
if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
|
|
943
959
|
if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
|
|
944
960
|
}
|
|
@@ -1019,23 +1035,44 @@ function validateApiConstraints(config, _options) {
|
|
|
1019
1035
|
});
|
|
1020
1036
|
if (!config.backend || ![
|
|
1021
1037
|
"hono",
|
|
1038
|
+
"effect",
|
|
1022
1039
|
"express",
|
|
1023
1040
|
"fastify",
|
|
1024
1041
|
"elysia"
|
|
1025
1042
|
].includes(config.backend)) incompatibilityError({
|
|
1026
|
-
message: `${apiDisplayName} currently supports Hono, Express, Fastify, and Elysia backends.`,
|
|
1043
|
+
message: `${apiDisplayName} currently supports Hono, Effect, Express, Fastify, and Elysia backends.`,
|
|
1027
1044
|
provided: {
|
|
1028
1045
|
api: config.api,
|
|
1029
1046
|
backend: config.backend ?? "none"
|
|
1030
1047
|
},
|
|
1031
1048
|
suggestions: [
|
|
1032
1049
|
"Use --backend hono",
|
|
1050
|
+
"Use --backend effect",
|
|
1033
1051
|
"Use --backend express",
|
|
1034
1052
|
"Use --backend fastify",
|
|
1035
1053
|
"Use --backend elysia"
|
|
1036
1054
|
]
|
|
1037
1055
|
});
|
|
1038
1056
|
}
|
|
1057
|
+
function validateEffectBackendConstraints(config) {
|
|
1058
|
+
if (config.backend !== "effect") return;
|
|
1059
|
+
if (config.effect !== "effect-full") missingRequirementError({
|
|
1060
|
+
message: "Effect backend requires Effect Platform + SQL services.",
|
|
1061
|
+
provided: {
|
|
1062
|
+
backend: "effect",
|
|
1063
|
+
effect: config.effect ?? "none"
|
|
1064
|
+
},
|
|
1065
|
+
suggestions: ["Use --effect effect-full"]
|
|
1066
|
+
});
|
|
1067
|
+
if (config.validation !== "effect-schema") missingRequirementError({
|
|
1068
|
+
message: "Effect backend requires Effect Schema validation.",
|
|
1069
|
+
provided: {
|
|
1070
|
+
backend: "effect",
|
|
1071
|
+
validation: config.validation ?? "none"
|
|
1072
|
+
},
|
|
1073
|
+
suggestions: ["Use --validation effect-schema"]
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1039
1076
|
function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set()) {
|
|
1040
1077
|
if (config.ecosystem !== "java") return;
|
|
1041
1078
|
const hasSpringBoot = config.javaWebFramework === "spring-boot";
|
|
@@ -1043,7 +1080,8 @@ function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set
|
|
|
1043
1080
|
const hasNoBuildTool = config.javaBuildTool === "none";
|
|
1044
1081
|
const hasJavaLibraries = (config.javaLibraries ?? []).some((library) => library !== "none");
|
|
1045
1082
|
const hasJavaTestingLibraries = (config.javaTestingLibraries ?? []).some((library) => library !== "none");
|
|
1046
|
-
const
|
|
1083
|
+
const hasJavaApi = (config.javaApi ?? "none") !== "none";
|
|
1084
|
+
const hasSpringOnlyFeatures = config.javaOrm !== "none" || config.javaAuth !== "none" || hasJavaLibraries || hasJavaApi;
|
|
1047
1085
|
if (hasNoBuildTool && hasJavaWebFramework) incompatibilityError({
|
|
1048
1086
|
message: "Java web frameworks require Maven or Gradle in the Java scaffold.",
|
|
1049
1087
|
provided: {
|
|
@@ -1059,9 +1097,10 @@ function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set
|
|
|
1059
1097
|
"java-build-tool": config.javaBuildTool ?? "none",
|
|
1060
1098
|
"java-orm": config.javaOrm ?? "none",
|
|
1061
1099
|
"java-auth": config.javaAuth ?? "none",
|
|
1100
|
+
"java-api": config.javaApi ?? "none",
|
|
1062
1101
|
"java-libraries": (config.javaLibraries ?? []).join(" ") || "none"
|
|
1063
1102
|
},
|
|
1064
|
-
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"]
|
|
1103
|
+
suggestions: ["Use --java-web-framework spring-boot and a real build tool for Spring features", "Clear --java-orm, --java-auth, --java-api, and --java-libraries when using plain Java or Quarkus"]
|
|
1065
1104
|
});
|
|
1066
1105
|
if (hasNoBuildTool && hasJavaTestingLibraries) incompatibilityError({
|
|
1067
1106
|
message: "Java testing libraries require Maven or Gradle to manage test dependencies.",
|
|
@@ -1241,7 +1280,23 @@ function validateRateLimitConstraints(config) {
|
|
|
1241
1280
|
function validateSearchConstraints(config) {
|
|
1242
1281
|
if (!config.search || config.search === "none") return;
|
|
1243
1282
|
const ecosystem = config.ecosystem ?? "typescript";
|
|
1244
|
-
if (
|
|
1283
|
+
if (config.search === "bleve" && ecosystem !== "go") incompatibilityError({
|
|
1284
|
+
message: "Bleve search is available for Go projects only.",
|
|
1285
|
+
provided: {
|
|
1286
|
+
ecosystem,
|
|
1287
|
+
search: config.search
|
|
1288
|
+
},
|
|
1289
|
+
suggestions: [
|
|
1290
|
+
"Use --ecosystem go",
|
|
1291
|
+
"Use --search meilisearch",
|
|
1292
|
+
"Use --search none"
|
|
1293
|
+
]
|
|
1294
|
+
});
|
|
1295
|
+
const allowedSearch = {
|
|
1296
|
+
go: ["meilisearch", "bleve"],
|
|
1297
|
+
python: ["meilisearch", "elasticsearch"]
|
|
1298
|
+
}[ecosystem] ?? ["meilisearch"];
|
|
1299
|
+
if (ecosystem !== "typescript" && !allowedSearch.includes(config.search)) incompatibilityError({
|
|
1245
1300
|
message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
|
|
1246
1301
|
provided: {
|
|
1247
1302
|
ecosystem,
|
|
@@ -1288,6 +1343,19 @@ function validatePythonApiConstraints(config) {
|
|
|
1288
1343
|
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"]
|
|
1289
1344
|
});
|
|
1290
1345
|
}
|
|
1346
|
+
function validateI18nConstraints(config) {
|
|
1347
|
+
if (config.i18n !== "intlayer") return;
|
|
1348
|
+
const { web } = splitFrontends$1(config.frontend ?? []);
|
|
1349
|
+
const unsupportedFrontend = web.find((frontend) => !INTLAYER_COMPATIBLE_FRONTENDS.has(frontend));
|
|
1350
|
+
if (web.length === 0 || unsupportedFrontend) incompatibilityError({
|
|
1351
|
+
message: "Intlayer i18n is currently wired only for Next.js, Vinext, TanStack Router/Start, React Router, and React + Vite frontends.",
|
|
1352
|
+
provided: {
|
|
1353
|
+
frontend: web.join(" ") || "none",
|
|
1354
|
+
i18n: "intlayer"
|
|
1355
|
+
},
|
|
1356
|
+
suggestions: ["Use --frontend next, vinext, tanstack-router, tanstack-start, react-router, or react-vite", "Set --i18n none or choose another i18n provider for this frontend"]
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1291
1359
|
function validateFullConfig(config, providedFlags, options) {
|
|
1292
1360
|
if (config.stackParts && !options.yolo) {
|
|
1293
1361
|
const graphValidation = (0, types_exports.validateStackParts)(config.stackParts);
|
|
@@ -1302,6 +1370,7 @@ function validateFullConfig(config, providedFlags, options) {
|
|
|
1302
1370
|
validateEncoreConstraints(config, providedFlags);
|
|
1303
1371
|
validateAdonisJSConstraints(config, providedFlags);
|
|
1304
1372
|
validateBackendConstraints(config, providedFlags, options);
|
|
1373
|
+
validateEffectBackendConstraints(config);
|
|
1305
1374
|
validateFrontendConstraints(config, providedFlags);
|
|
1306
1375
|
validateApiConstraints(config, options);
|
|
1307
1376
|
validatePythonApiConstraints(config);
|
|
@@ -1312,6 +1381,7 @@ function validateFullConfig(config, providedFlags, options) {
|
|
|
1312
1381
|
validateSearchConstraints(config);
|
|
1313
1382
|
validateJavaConstraints(config, providedFlags);
|
|
1314
1383
|
validateElixirConstraints(config);
|
|
1384
|
+
validateI18nConstraints(config);
|
|
1315
1385
|
const hasGraphBackend = config.stackParts?.some((part) => part.role === "backend" && !part.ownerPartId && part.source !== "provided" && part.ecosystem !== "typescript" && part.ecosystem !== "react-native" && part.ecosystem !== "universal");
|
|
1316
1386
|
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));
|
|
1317
1387
|
validateSelfBackendCompatibility(providedFlags, options, config);
|
|
@@ -1368,6 +1438,7 @@ function validateConfigForProgrammaticUse(config) {
|
|
|
1368
1438
|
}
|
|
1369
1439
|
validateEcosystemAuthCompatibility(config);
|
|
1370
1440
|
validateDatabaseOrmAuth(config);
|
|
1441
|
+
validateEffectBackendConstraints(config);
|
|
1371
1442
|
if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
|
|
1372
1443
|
validateApiFrontendCompatibility(config.api, config.frontend, config.astroIntegration);
|
|
1373
1444
|
validatePythonApiConstraints(config);
|
|
@@ -1378,6 +1449,7 @@ function validateConfigForProgrammaticUse(config) {
|
|
|
1378
1449
|
validateSearchConstraints(config);
|
|
1379
1450
|
validateJavaConstraints(config);
|
|
1380
1451
|
validateElixirConstraints(config);
|
|
1452
|
+
validateI18nConstraints(config);
|
|
1381
1453
|
validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
|
|
1382
1454
|
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);
|
|
1383
1455
|
validateExamplesCompatibility(config.examples ?? [], config.backend, config.frontend ?? [], config.runtime, config.ai);
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { r as readBtsConfig } from "./bts-config-DQVWvPDs.mjs";
|
|
3
|
+
import { log } from "@clack/prompts";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { processTemplateString } from "@better-fullstack/template-generator";
|
|
8
|
+
|
|
9
|
+
//#region src/commands/gen.ts
|
|
10
|
+
/**
|
|
11
|
+
* Inline resource template (rendered with the project's ProjectConfig via the
|
|
12
|
+
* template-generator's Handlebars pipeline). Kept as a string constant so `gen`
|
|
13
|
+
* works in a published CLI with no template directory / EMBEDDED_TEMPLATES
|
|
14
|
+
* dependency. Branches on `api` (trpc | orpc) and `auth` (better-auth ->
|
|
15
|
+
* protectedProcedure).
|
|
16
|
+
*/
|
|
17
|
+
const RESOURCE_TEMPLATE = `{{#if (eq api "trpc")}}
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
import { {{#if (isBetterAuth auth)}}protectedProcedure{{else}}publicProcedure{{/if}}, router } from "../index{{importExt}}";
|
|
21
|
+
|
|
22
|
+
export type {{ResourceName}} = {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const {{resourceName}}Store: {{ResourceName}}[] = [];
|
|
29
|
+
let {{resourceName}}NextId = 1;
|
|
30
|
+
|
|
31
|
+
const {{resourceName}}Procedure = {{#if (isBetterAuth auth)}}protectedProcedure{{else}}publicProcedure{{/if}};
|
|
32
|
+
|
|
33
|
+
export const {{resourceName}}Router = router({
|
|
34
|
+
list: {{resourceName}}Procedure.query(() => {
|
|
35
|
+
return {{resourceName}}Store;
|
|
36
|
+
}),
|
|
37
|
+
byId: {{resourceName}}Procedure.input(z.object({ id: z.string() })).query(({ input }) => {
|
|
38
|
+
return {{resourceName}}Store.find((item) => item.id === input.id) ?? null;
|
|
39
|
+
}),
|
|
40
|
+
create: {{resourceName}}Procedure
|
|
41
|
+
.input(z.object({ name: z.string().min(1) }))
|
|
42
|
+
.mutation(({ input }) => {
|
|
43
|
+
const item: {{ResourceName}} = {
|
|
44
|
+
id: String({{resourceName}}NextId++),
|
|
45
|
+
name: input.name,
|
|
46
|
+
createdAt: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
{{resourceName}}Store.push(item);
|
|
49
|
+
return item;
|
|
50
|
+
}),
|
|
51
|
+
update: {{resourceName}}Procedure
|
|
52
|
+
.input(z.object({ id: z.string(), name: z.string().min(1) }))
|
|
53
|
+
.mutation(({ input }) => {
|
|
54
|
+
const item = {{resourceName}}Store.find((entry) => entry.id === input.id);
|
|
55
|
+
if (!item) return null;
|
|
56
|
+
item.name = input.name;
|
|
57
|
+
return item;
|
|
58
|
+
}),
|
|
59
|
+
remove: {{resourceName}}Procedure.input(z.object({ id: z.string() })).mutation(({ input }) => {
|
|
60
|
+
const index = {{resourceName}}Store.findIndex((entry) => entry.id === input.id);
|
|
61
|
+
if (index === -1) return { success: false };
|
|
62
|
+
{{resourceName}}Store.splice(index, 1);
|
|
63
|
+
return { success: true };
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
{{else}}
|
|
67
|
+
import { z } from "zod";
|
|
68
|
+
|
|
69
|
+
import { {{#if (isBetterAuth auth)}}protectedProcedure{{else}}publicProcedure{{/if}} } from "../index{{importExt}}";
|
|
70
|
+
|
|
71
|
+
export type {{ResourceName}} = {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const {{resourceName}}Store: {{ResourceName}}[] = [];
|
|
78
|
+
let {{resourceName}}NextId = 1;
|
|
79
|
+
|
|
80
|
+
const {{resourceName}}Procedure = {{#if (isBetterAuth auth)}}protectedProcedure{{else}}publicProcedure{{/if}};
|
|
81
|
+
|
|
82
|
+
export const {{resourceName}}Router = {
|
|
83
|
+
list: {{resourceName}}Procedure.handler(() => {
|
|
84
|
+
return {{resourceName}}Store;
|
|
85
|
+
}),
|
|
86
|
+
byId: {{resourceName}}Procedure
|
|
87
|
+
.input(z.object({ id: z.string() }))
|
|
88
|
+
.handler(({ input }) => {
|
|
89
|
+
return {{resourceName}}Store.find((item) => item.id === input.id) ?? null;
|
|
90
|
+
}),
|
|
91
|
+
create: {{resourceName}}Procedure
|
|
92
|
+
.input(z.object({ name: z.string().min(1) }))
|
|
93
|
+
.handler(({ input }) => {
|
|
94
|
+
const item: {{ResourceName}} = {
|
|
95
|
+
id: String({{resourceName}}NextId++),
|
|
96
|
+
name: input.name,
|
|
97
|
+
createdAt: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
{{resourceName}}Store.push(item);
|
|
100
|
+
return item;
|
|
101
|
+
}),
|
|
102
|
+
update: {{resourceName}}Procedure
|
|
103
|
+
.input(z.object({ id: z.string(), name: z.string().min(1) }))
|
|
104
|
+
.handler(({ input }) => {
|
|
105
|
+
const item = {{resourceName}}Store.find((entry) => entry.id === input.id);
|
|
106
|
+
if (!item) return null;
|
|
107
|
+
item.name = input.name;
|
|
108
|
+
return item;
|
|
109
|
+
}),
|
|
110
|
+
remove: {{resourceName}}Procedure
|
|
111
|
+
.input(z.object({ id: z.string() }))
|
|
112
|
+
.handler(({ input }) => {
|
|
113
|
+
const index = {{resourceName}}Store.findIndex((entry) => entry.id === input.id);
|
|
114
|
+
if (index === -1) return { success: false };
|
|
115
|
+
{{resourceName}}Store.splice(index, 1);
|
|
116
|
+
return { success: true };
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
{{/if}}
|
|
120
|
+
`;
|
|
121
|
+
/** Candidate roots (relative to the project) that may hold a routers/index.ts. */
|
|
122
|
+
const ROUTER_INDEX_CANDIDATES = ["packages/api/src/routers/index.ts", "apps/server/src/routers/index.ts"];
|
|
123
|
+
function splitWords(raw) {
|
|
124
|
+
return raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).map((part) => part.trim()).filter(Boolean);
|
|
125
|
+
}
|
|
126
|
+
function toCamelCase(raw) {
|
|
127
|
+
return splitWords(raw).map((word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
128
|
+
}
|
|
129
|
+
function toPascalCase(raw) {
|
|
130
|
+
const camel = toCamelCase(raw);
|
|
131
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Scans for an existing routers/index.ts to register the new resource in.
|
|
135
|
+
* Checks the well-known locations first, then any `<root>/src/routers/index.ts`
|
|
136
|
+
* under apps/* and packages/* (robust across hono/express/elysia split backends
|
|
137
|
+
* and the packages/api layout).
|
|
138
|
+
*/
|
|
139
|
+
async function findRouterIndex(projectDir) {
|
|
140
|
+
for (const candidate of ROUTER_INDEX_CANDIDATES) {
|
|
141
|
+
const full = path.join(projectDir, candidate);
|
|
142
|
+
if (await fs.pathExists(full)) return full;
|
|
143
|
+
}
|
|
144
|
+
for (const workspace of ["apps", "packages"]) {
|
|
145
|
+
const workspaceDir = path.join(projectDir, workspace);
|
|
146
|
+
if (!await fs.pathExists(workspaceDir)) continue;
|
|
147
|
+
let entries;
|
|
148
|
+
try {
|
|
149
|
+
entries = await fs.readdir(workspaceDir);
|
|
150
|
+
} catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const full = path.join(workspaceDir, entry, "src", "routers", "index.ts");
|
|
155
|
+
if (await fs.pathExists(full)) return full;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/** Mirrors the relative-import extension used by the existing routers/index.ts. */
|
|
161
|
+
function detectImportExtension(indexContent) {
|
|
162
|
+
return /from\s+["']\.\.\/index\.js["']/.test(indexContent) ? ".js" : "";
|
|
163
|
+
}
|
|
164
|
+
function injectResource(indexContent, api, resourceName, importExt) {
|
|
165
|
+
const anchor = api === "trpc" ? "export const appRouter = router({" : "export const appRouter = {";
|
|
166
|
+
const lines = indexContent.split("\n");
|
|
167
|
+
const anchorIdx = lines.findIndex((line) => line.includes(anchor));
|
|
168
|
+
if (anchorIdx === -1) return {
|
|
169
|
+
ok: false,
|
|
170
|
+
reason: `Could not find the appRouter anchor (\`${anchor}\`)`
|
|
171
|
+
};
|
|
172
|
+
const registrationKey = `${resourceName}:`;
|
|
173
|
+
if (lines.some((line, idx) => idx > anchorIdx && line.trim().startsWith(registrationKey))) return {
|
|
174
|
+
ok: false,
|
|
175
|
+
reason: `\`${resourceName}\` is already registered in appRouter`
|
|
176
|
+
};
|
|
177
|
+
const importLine = `import { ${resourceName}Router } from "./${resourceName}${importExt}";`;
|
|
178
|
+
const registrationLine = ` ${resourceName}: ${resourceName}Router,`;
|
|
179
|
+
let lastImportIdx = -1;
|
|
180
|
+
for (let i = 0; i < anchorIdx; i += 1) {
|
|
181
|
+
const line = lines[i];
|
|
182
|
+
if (line !== void 0 && line.trimStart().startsWith("import ")) lastImportIdx = i;
|
|
183
|
+
}
|
|
184
|
+
lines.splice(lastImportIdx + 1, 0, importLine);
|
|
185
|
+
const newAnchorIdx = lines.findIndex((line) => line.includes(anchor));
|
|
186
|
+
lines.splice(newAnchorIdx + 1, 0, registrationLine);
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
content: lines.join("\n")
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function genCommand(input) {
|
|
193
|
+
const projectDir = path.resolve(input.dir || process.cwd());
|
|
194
|
+
const dryRun = input.dryRun ?? false;
|
|
195
|
+
const btsConfig = await readBtsConfig(projectDir);
|
|
196
|
+
if (!btsConfig) throw new Error(`No Better Fullstack project found in ${projectDir}. Make sure bts.jsonc exists.`);
|
|
197
|
+
const resourceName = toCamelCase(input.name);
|
|
198
|
+
const ResourceName = toPascalCase(input.name);
|
|
199
|
+
if (!resourceName || !/^[a-z]/i.test(resourceName)) throw new Error(`Invalid resource name "${input.name}". Use a name that starts with a letter, e.g. "post".`);
|
|
200
|
+
const ecosystem = btsConfig.ecosystem;
|
|
201
|
+
const api = btsConfig.api;
|
|
202
|
+
if (ecosystem !== "typescript" || api !== "trpc" && api !== "orpc") {
|
|
203
|
+
const stack = `${ecosystem}${api && api !== "none" ? ` + ${api}` : ""}`;
|
|
204
|
+
const message$1 = `\`gen ${input.kind}\` is not yet supported for this stack (${stack}). Currently supported: TypeScript projects with a trpc or orpc API.`;
|
|
205
|
+
log.warn(pc.yellow(message$1));
|
|
206
|
+
return {
|
|
207
|
+
status: "unsupported",
|
|
208
|
+
message: message$1
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const routerIndexPath = await findRouterIndex(projectDir);
|
|
212
|
+
if (!routerIndexPath) {
|
|
213
|
+
const message$1 = `Could not locate a \`routers/index.ts\` in this project. Is this a trpc/orpc project? No files were written.`;
|
|
214
|
+
log.warn(pc.yellow(message$1));
|
|
215
|
+
return {
|
|
216
|
+
status: "unsupported",
|
|
217
|
+
message: message$1
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const routersDir = path.dirname(routerIndexPath);
|
|
221
|
+
const resourceFile = path.join(routersDir, `${resourceName}.ts`);
|
|
222
|
+
const relResourceFile = path.relative(projectDir, resourceFile);
|
|
223
|
+
if (await fs.pathExists(resourceFile)) throw new Error(`Resource "${resourceName}" already exists at ${relResourceFile}. Delete it first or choose another name.`);
|
|
224
|
+
const indexContent = await fs.readFile(routerIndexPath, "utf-8");
|
|
225
|
+
const importExt = detectImportExtension(indexContent);
|
|
226
|
+
const resourceContent = processTemplateString(RESOURCE_TEMPLATE, {
|
|
227
|
+
...btsConfig,
|
|
228
|
+
resourceName,
|
|
229
|
+
ResourceName,
|
|
230
|
+
importExt
|
|
231
|
+
}).trimStart();
|
|
232
|
+
const injection = injectResource(indexContent, api, resourceName, importExt);
|
|
233
|
+
if (dryRun) {
|
|
234
|
+
log.info(pc.cyan(`[dry run] Would create ${relResourceFile}`));
|
|
235
|
+
log.message(resourceContent);
|
|
236
|
+
if (injection.ok) log.info(pc.cyan(`[dry run] Would register \`${resourceName}: ${resourceName}Router\` in ${path.relative(projectDir, routerIndexPath)}`));
|
|
237
|
+
else log.warn(pc.yellow(`[dry run] Manual wiring required: ${injection.reason}`));
|
|
238
|
+
return {
|
|
239
|
+
status: injection.ok ? "created" : "manual-wiring",
|
|
240
|
+
message: "dry run",
|
|
241
|
+
resourceFile,
|
|
242
|
+
registered: injection.ok
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
await fs.writeFile(resourceFile, resourceContent, "utf-8");
|
|
246
|
+
if (!injection.ok) {
|
|
247
|
+
const relIndex = path.relative(projectDir, routerIndexPath);
|
|
248
|
+
const message$1 = `Created ${relResourceFile}, but could not auto-register it: ${injection.reason}. Wire it manually.`;
|
|
249
|
+
log.warn(pc.yellow(message$1));
|
|
250
|
+
log.message([
|
|
251
|
+
`Add these two lines to ${relIndex}:`,
|
|
252
|
+
pc.dim(` import { ${resourceName}Router } from "./${resourceName}${importExt}";`),
|
|
253
|
+
pc.dim(` ${resourceName}: ${resourceName}Router, // inside appRouter`)
|
|
254
|
+
].join("\n"));
|
|
255
|
+
return {
|
|
256
|
+
status: "manual-wiring",
|
|
257
|
+
message: message$1,
|
|
258
|
+
resourceFile,
|
|
259
|
+
registered: false
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
await fs.writeFile(routerIndexPath, injection.content, "utf-8");
|
|
263
|
+
const message = `Created ${relResourceFile} and registered \`${resourceName}: ${resourceName}Router\` in ${path.relative(projectDir, routerIndexPath)}.`;
|
|
264
|
+
log.success(pc.green(message));
|
|
265
|
+
return {
|
|
266
|
+
status: "created",
|
|
267
|
+
message,
|
|
268
|
+
resourceFile,
|
|
269
|
+
registered: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
export { genCommand };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { f as getPrimaryGraphPart } from "./bts-config-DQVWvPDs.mjs";
|
|
3
3
|
import { log, spinner } from "@clack/prompts";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import path from "node:path";
|