create-better-fullstack 2.1.4 → 2.1.6

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.
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ import { r as readBtsConfig } from "./bts-config-BceXPcpI.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 { O as getPrimaryGraphPart } from "./errors-Cyol8zbN.mjs";
2
+ import { _ as getPrimaryGraphPart } from "./bts-config-BceXPcpI.mjs";
3
3
  import { log, spinner } from "@clack/prompts";
4
4
  import pc from "picocolors";
5
5
  import path from "node:path";