create-better-fullstack 2.1.5 → 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.
- package/dist/{add-handler-BNSL6HdM.mjs → add-handler-CuPmSJRM.mjs} +13 -5
- package/dist/{addons-setup-CyrP1IV-.mjs → addons-setup-C_lxdJqU.mjs} +8 -1
- package/dist/addons-setup-DEPfsn6z.mjs +6 -0
- package/dist/{errors-Cyol8zbN.mjs → bts-config-BceXPcpI.mjs} +3 -84
- package/dist/cli.mjs +2 -2
- package/dist/{templates-CnTOtKjm.mjs → config-processing-B_1wTe3g.mjs} +57 -2
- package/dist/{doctor-DBoq7bZ9.mjs → doctor-DucDyWfl.mjs} +3 -2
- package/dist/errors-ns_o2OKg.mjs +86 -0
- package/dist/{file-formatter-B3dsev2l.mjs → file-formatter-XU6ti05V.mjs} +66 -6
- package/dist/gen-DWx3Xu_K.mjs +274 -0
- package/dist/{generated-checks-C8hn9w2i.mjs → generated-checks-Dt4Xqp1x.mjs} +1 -1
- package/dist/index.d.mts +171 -79
- package/dist/index.mjs +15 -8
- package/dist/{install-dependencies-CgNh-aOy.mjs → install-dependencies-DHoYa3P-.mjs} +75 -21
- package/dist/mcp-D9O5zgAA.mjs +8 -0
- package/dist/mcp-entry.mjs +153 -16
- package/dist/registry-CxeEOPot.mjs +394 -0
- package/dist/run-3AkXloH1.mjs +13 -0
- package/dist/{run-BYse4yJy.mjs → run-D80ZtSO8.mjs} +361 -88
- package/dist/scaffold-manifest-GV1fbhpD.mjs +123 -0
- package/dist/update-C9_x2yBF.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-DLZAuT3V.mjs} +0 -0
|
@@ -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 {
|
|
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";
|