@chidchanun/bcp 0.1.28 → 0.2.0
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/README.md +220 -256
- package/docs/README.md +257 -241
- package/docs/developer-tools.md +175 -78
- package/docs/docs-web-manifest.json +97 -0
- package/docs/generators.md +149 -0
- package/docs/migration-0.2.md +173 -0
- package/docs/platform-contract.md +159 -0
- package/docs/platform-manifest.json +75 -0
- package/docs/project-metadata.md +112 -0
- package/docs/releases/0.1.29.md +170 -0
- package/docs/releases/0.2.0.md +146 -0
- package/package.json +1 -1
- package/packages/cli/src/args.ts +96 -0
- package/packages/cli/src/developer-tools-v2.ts +629 -0
- package/packages/cli/src/developer-tools.ts +115 -0
- package/packages/cli/src/generate.ts +475 -0
- package/packages/cli/src/index.ts +69 -0
|
@@ -19,6 +19,10 @@ import {
|
|
|
19
19
|
scanApiRoutes,
|
|
20
20
|
scanRoutes,
|
|
21
21
|
} from "../../router/src/index.js";
|
|
22
|
+
import {
|
|
23
|
+
collectProjectDiagnostics,
|
|
24
|
+
type ProjectDiagnostics,
|
|
25
|
+
} from "./developer-tools-v2.js";
|
|
22
26
|
|
|
23
27
|
export type DoctorStatus =
|
|
24
28
|
| "pass"
|
|
@@ -56,6 +60,7 @@ export interface InspectReport {
|
|
|
56
60
|
platform: NodeJS.Platform;
|
|
57
61
|
arch: string;
|
|
58
62
|
};
|
|
63
|
+
project: ProjectDiagnostics;
|
|
59
64
|
environment: {
|
|
60
65
|
mode: "development";
|
|
61
66
|
files: string[];
|
|
@@ -264,6 +269,90 @@ export async function collectDoctorReport(
|
|
|
264
269
|
}
|
|
265
270
|
}
|
|
266
271
|
|
|
272
|
+
const diagnostics =
|
|
273
|
+
collectProjectDiagnostics(
|
|
274
|
+
rootDirectory
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
addCheck(
|
|
278
|
+
checks,
|
|
279
|
+
"project.lockfile",
|
|
280
|
+
diagnostics.lockfile
|
|
281
|
+
? "pass"
|
|
282
|
+
: "warn",
|
|
283
|
+
"Dependency lockfile",
|
|
284
|
+
diagnostics.lockfile
|
|
285
|
+
? `${diagnostics.lockfile} detected (${diagnostics.packageManager ?? "unknown package manager"}).`
|
|
286
|
+
: "No supported dependency lockfile was found. Reproducible installs are recommended for CI and Docker."
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
addCheck(
|
|
290
|
+
checks,
|
|
291
|
+
"dependencies.framework-declarations",
|
|
292
|
+
diagnostics.frameworkDeclarations.length <= 1
|
|
293
|
+
? "pass"
|
|
294
|
+
: "fail",
|
|
295
|
+
"Framework dependency declarations",
|
|
296
|
+
diagnostics.frameworkDeclarations.length === 0
|
|
297
|
+
? "No BCP dependency declaration detected yet."
|
|
298
|
+
: diagnostics.frameworkDeclarations.length === 1
|
|
299
|
+
? `${diagnostics.frameworkDeclarations[0].name} is declared once in ${diagnostics.frameworkDeclarations[0].section}.`
|
|
300
|
+
: `Multiple BCP declarations detected: ${diagnostics.frameworkDeclarations.map((item) => `${item.name} (${item.section})`).join(", ")}. Keep only the application dependency key bcp.`
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
addCheck(
|
|
304
|
+
checks,
|
|
305
|
+
"runtime.production-hardening",
|
|
306
|
+
diagnostics.hardening.valid
|
|
307
|
+
? "pass"
|
|
308
|
+
: "fail",
|
|
309
|
+
"Production hardening configuration",
|
|
310
|
+
diagnostics.hardening.message
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
addCheck(
|
|
314
|
+
checks,
|
|
315
|
+
"production.build",
|
|
316
|
+
diagnostics.productionBuild
|
|
317
|
+
? "pass"
|
|
318
|
+
: "warn",
|
|
319
|
+
"Standalone production artifact",
|
|
320
|
+
diagnostics.productionBuild
|
|
321
|
+
? ".bcp-framework/build/server/server.mjs is present."
|
|
322
|
+
: "No standalone production artifact is present. Run `bcp build` before production deployment."
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (
|
|
326
|
+
diagnostics.dockerfile
|
|
327
|
+
) {
|
|
328
|
+
addCheck(
|
|
329
|
+
checks,
|
|
330
|
+
"production.docker-lockfile",
|
|
331
|
+
diagnostics.lockfile ===
|
|
332
|
+
"package-lock.json"
|
|
333
|
+
? "pass"
|
|
334
|
+
: "warn",
|
|
335
|
+
"Docker reproducible install",
|
|
336
|
+
diagnostics.lockfile ===
|
|
337
|
+
"package-lock.json"
|
|
338
|
+
? `${diagnostics.dockerfile} and package-lock.json are present for npm ci.`
|
|
339
|
+
: `${diagnostics.dockerfile} is present but package-lock.json was not detected. Ensure the Dockerfile uses the matching package-manager lockfile.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (
|
|
344
|
+
diagnostics.storage.provider !==
|
|
345
|
+
"none"
|
|
346
|
+
) {
|
|
347
|
+
addCheck(
|
|
348
|
+
checks,
|
|
349
|
+
"storage.provider",
|
|
350
|
+
"pass",
|
|
351
|
+
"Storage provider",
|
|
352
|
+
`${diagnostics.storage.provider} detected from ${diagnostics.storage.file}.`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
267
356
|
const appDirectory =
|
|
268
357
|
path.join(
|
|
269
358
|
rootDirectory,
|
|
@@ -687,6 +776,10 @@ export async function collectInspectReport(
|
|
|
687
776
|
arch:
|
|
688
777
|
process.arch,
|
|
689
778
|
},
|
|
779
|
+
project:
|
|
780
|
+
collectProjectDiagnostics(
|
|
781
|
+
rootDirectory
|
|
782
|
+
),
|
|
690
783
|
environment: {
|
|
691
784
|
mode:
|
|
692
785
|
"development",
|
|
@@ -832,6 +925,28 @@ function printInspectReport(
|
|
|
832
925
|
`Runtime: Node.js ${report.runtime.node} | ${report.runtime.platform} ${report.runtime.arch}`
|
|
833
926
|
);
|
|
834
927
|
console.log("");
|
|
928
|
+
console.log(
|
|
929
|
+
"Project diagnostics:"
|
|
930
|
+
);
|
|
931
|
+
console.log(
|
|
932
|
+
` package manager: ${report.project.packageManager ?? "unknown"}`
|
|
933
|
+
);
|
|
934
|
+
console.log(
|
|
935
|
+
` lockfile: ${report.project.lockfile ?? "(none)"}`
|
|
936
|
+
);
|
|
937
|
+
console.log(
|
|
938
|
+
` production build: ${report.project.productionBuild ? "present" : "missing"}`
|
|
939
|
+
);
|
|
940
|
+
console.log(
|
|
941
|
+
` Dockerfile: ${report.project.dockerfile ?? "(none)"}`
|
|
942
|
+
);
|
|
943
|
+
console.log(
|
|
944
|
+
` storage: ${report.project.storage.provider}${report.project.storage.file ? ` (${report.project.storage.file})` : ""}`
|
|
945
|
+
);
|
|
946
|
+
console.log(
|
|
947
|
+
` hardening: ${report.project.hardening.valid ? "valid" : "invalid"} - ${report.project.hardening.message}`
|
|
948
|
+
);
|
|
949
|
+
console.log("");
|
|
835
950
|
console.log(
|
|
836
951
|
"Environment:"
|
|
837
952
|
);
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
runDatabaseMigrationCommand,
|
|
6
|
+
} from "./database-migrations.js";
|
|
7
|
+
|
|
8
|
+
export type GenerateKind =
|
|
9
|
+
| "page"
|
|
10
|
+
| "api"
|
|
11
|
+
| "middleware"
|
|
12
|
+
| "migration";
|
|
13
|
+
|
|
14
|
+
export interface GenerateOptions {
|
|
15
|
+
rootDirectory: string;
|
|
16
|
+
kind: GenerateKind;
|
|
17
|
+
name?: string;
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GeneratedArtifact {
|
|
22
|
+
kind: GenerateKind;
|
|
23
|
+
file: string;
|
|
24
|
+
route?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runGenerate(
|
|
28
|
+
options: GenerateOptions
|
|
29
|
+
): Promise<GeneratedArtifact> {
|
|
30
|
+
const rootDirectory =
|
|
31
|
+
path.resolve(
|
|
32
|
+
options.rootDirectory
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
switch (options.kind) {
|
|
36
|
+
case "page":
|
|
37
|
+
return generatePage(
|
|
38
|
+
rootDirectory,
|
|
39
|
+
requireGenerateName(
|
|
40
|
+
options.kind,
|
|
41
|
+
options.name
|
|
42
|
+
),
|
|
43
|
+
options.force === true
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
case "api":
|
|
47
|
+
return generateApiRoute(
|
|
48
|
+
rootDirectory,
|
|
49
|
+
requireGenerateName(
|
|
50
|
+
options.kind,
|
|
51
|
+
options.name
|
|
52
|
+
),
|
|
53
|
+
options.force === true
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
case "middleware":
|
|
57
|
+
return generateMiddleware(
|
|
58
|
+
rootDirectory,
|
|
59
|
+
options.force === true
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
case "migration": {
|
|
63
|
+
const name =
|
|
64
|
+
requireGenerateName(
|
|
65
|
+
options.kind,
|
|
66
|
+
options.name
|
|
67
|
+
);
|
|
68
|
+
const migrationDirectory =
|
|
69
|
+
path.join(
|
|
70
|
+
rootDirectory,
|
|
71
|
+
"migrations"
|
|
72
|
+
);
|
|
73
|
+
const before =
|
|
74
|
+
listFiles(
|
|
75
|
+
migrationDirectory
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await runDatabaseMigrationCommand({
|
|
79
|
+
rootDirectory,
|
|
80
|
+
action:
|
|
81
|
+
"create",
|
|
82
|
+
name,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const after =
|
|
86
|
+
listFiles(
|
|
87
|
+
migrationDirectory
|
|
88
|
+
);
|
|
89
|
+
let created:
|
|
90
|
+
string | undefined;
|
|
91
|
+
|
|
92
|
+
for (const file of after) {
|
|
93
|
+
if (
|
|
94
|
+
!before.has(
|
|
95
|
+
file
|
|
96
|
+
)
|
|
97
|
+
) {
|
|
98
|
+
created =
|
|
99
|
+
file;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!created) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"BCP Generate: migration command completed without creating a migration file."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
kind:
|
|
112
|
+
"migration",
|
|
113
|
+
file:
|
|
114
|
+
path.join(
|
|
115
|
+
migrationDirectory,
|
|
116
|
+
created
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeGenerateRoute(
|
|
124
|
+
value: string
|
|
125
|
+
): string {
|
|
126
|
+
const normalized =
|
|
127
|
+
value
|
|
128
|
+
.trim()
|
|
129
|
+
.replace(
|
|
130
|
+
/\\/g,
|
|
131
|
+
"/"
|
|
132
|
+
)
|
|
133
|
+
.replace(
|
|
134
|
+
/^\/+|\/+$/g,
|
|
135
|
+
""
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (!normalized) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"BCP Generate: route must not be empty."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const segments =
|
|
145
|
+
normalized.split(
|
|
146
|
+
"/"
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
for (const segment of segments) {
|
|
150
|
+
if (
|
|
151
|
+
!segment ||
|
|
152
|
+
segment === "." ||
|
|
153
|
+
segment === ".." ||
|
|
154
|
+
segment.includes(
|
|
155
|
+
"\0"
|
|
156
|
+
)
|
|
157
|
+
) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`BCP Generate: invalid route segment "${segment}".`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
!/^(?:\([A-Za-z0-9_-]+\)|\[\[\.\.\.[A-Za-z0-9_-]+\]\]|\[\.\.\.[A-Za-z0-9_-]+\]|\[[A-Za-z0-9_-]+\]|[A-Za-z0-9_-]+)$/.test(
|
|
165
|
+
segment
|
|
166
|
+
)
|
|
167
|
+
) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`BCP Generate: invalid route segment "${segment}".`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return segments.join(
|
|
175
|
+
"/"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function generatePage(
|
|
180
|
+
rootDirectory: string,
|
|
181
|
+
routeValue: string,
|
|
182
|
+
force: boolean
|
|
183
|
+
): GeneratedArtifact {
|
|
184
|
+
const route =
|
|
185
|
+
normalizeGenerateRoute(
|
|
186
|
+
routeValue
|
|
187
|
+
);
|
|
188
|
+
const file =
|
|
189
|
+
path.join(
|
|
190
|
+
rootDirectory,
|
|
191
|
+
"app",
|
|
192
|
+
...route.split("/"),
|
|
193
|
+
"page.tsx"
|
|
194
|
+
);
|
|
195
|
+
const componentName =
|
|
196
|
+
`${routeToIdentifier(route)}Page`;
|
|
197
|
+
const title =
|
|
198
|
+
routeToTitle(
|
|
199
|
+
route
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
writeGeneratedFile(
|
|
203
|
+
file,
|
|
204
|
+
`export default function ${componentName}() {\n return (\n <main>\n <h1>${escapeJsxText(title)}</h1>\n </main>\n );\n}\n`,
|
|
205
|
+
force
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
printGenerated(
|
|
209
|
+
rootDirectory,
|
|
210
|
+
"page",
|
|
211
|
+
file,
|
|
212
|
+
`/${route}`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
kind:
|
|
217
|
+
"page",
|
|
218
|
+
file,
|
|
219
|
+
route:
|
|
220
|
+
`/${route}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function generateApiRoute(
|
|
225
|
+
rootDirectory: string,
|
|
226
|
+
routeValue: string,
|
|
227
|
+
force: boolean
|
|
228
|
+
): GeneratedArtifact {
|
|
229
|
+
const normalized =
|
|
230
|
+
normalizeGenerateRoute(
|
|
231
|
+
routeValue
|
|
232
|
+
);
|
|
233
|
+
const route =
|
|
234
|
+
normalized.startsWith(
|
|
235
|
+
"api/"
|
|
236
|
+
)
|
|
237
|
+
? normalized.slice(
|
|
238
|
+
"api/".length
|
|
239
|
+
)
|
|
240
|
+
: normalized;
|
|
241
|
+
|
|
242
|
+
if (!route) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"BCP Generate: API route must include a path after api/."
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const file =
|
|
249
|
+
path.join(
|
|
250
|
+
rootDirectory,
|
|
251
|
+
"app",
|
|
252
|
+
"api",
|
|
253
|
+
...route.split("/"),
|
|
254
|
+
"route.ts"
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
writeGeneratedFile(
|
|
258
|
+
file,
|
|
259
|
+
`export async function GET() {\n return Response.json({\n ok: true,\n });\n}\n`,
|
|
260
|
+
force
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
printGenerated(
|
|
264
|
+
rootDirectory,
|
|
265
|
+
"api",
|
|
266
|
+
file,
|
|
267
|
+
`/api/${route}`
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
kind:
|
|
272
|
+
"api",
|
|
273
|
+
file,
|
|
274
|
+
route:
|
|
275
|
+
`/api/${route}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function generateMiddleware(
|
|
280
|
+
rootDirectory: string,
|
|
281
|
+
force: boolean
|
|
282
|
+
): GeneratedArtifact {
|
|
283
|
+
const file =
|
|
284
|
+
path.join(
|
|
285
|
+
rootDirectory,
|
|
286
|
+
"middleware.ts"
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
writeGeneratedFile(
|
|
290
|
+
file,
|
|
291
|
+
`import type {\n MiddlewarePipelineHandler,\n} from "bcp/middleware";\n\nexport const middleware:\n MiddlewarePipelineHandler =\n async (\n _request,\n next\n ) => {\n return next();\n };\n`,
|
|
292
|
+
force
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
printGenerated(
|
|
296
|
+
rootDirectory,
|
|
297
|
+
"middleware",
|
|
298
|
+
file
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
kind:
|
|
303
|
+
"middleware",
|
|
304
|
+
file,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function writeGeneratedFile(
|
|
309
|
+
file: string,
|
|
310
|
+
content: string,
|
|
311
|
+
force: boolean
|
|
312
|
+
): void {
|
|
313
|
+
if (
|
|
314
|
+
fs.existsSync(
|
|
315
|
+
file
|
|
316
|
+
) &&
|
|
317
|
+
!force
|
|
318
|
+
) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`BCP Generate: ${file} already exists. Pass --force to replace it.`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fs.mkdirSync(
|
|
325
|
+
path.dirname(
|
|
326
|
+
file
|
|
327
|
+
),
|
|
328
|
+
{
|
|
329
|
+
recursive: true,
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
fs.writeFileSync(
|
|
333
|
+
file,
|
|
334
|
+
content,
|
|
335
|
+
"utf8"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function requireGenerateName(
|
|
340
|
+
kind: GenerateKind,
|
|
341
|
+
value: string | undefined
|
|
342
|
+
): string {
|
|
343
|
+
const normalized =
|
|
344
|
+
value?.trim();
|
|
345
|
+
|
|
346
|
+
if (!normalized) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`BCP Generate: ${kind} requires a name or route.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return normalized;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function routeToIdentifier(
|
|
356
|
+
route: string
|
|
357
|
+
): string {
|
|
358
|
+
const words =
|
|
359
|
+
route
|
|
360
|
+
.replace(
|
|
361
|
+
/[\[\]().]+/g,
|
|
362
|
+
" "
|
|
363
|
+
)
|
|
364
|
+
.split(
|
|
365
|
+
/[\/_-]+|\s+/g
|
|
366
|
+
)
|
|
367
|
+
.filter(Boolean)
|
|
368
|
+
.map(
|
|
369
|
+
capitalize
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const value =
|
|
373
|
+
words.join("") ||
|
|
374
|
+
"Generated";
|
|
375
|
+
|
|
376
|
+
return /^\d/.test(
|
|
377
|
+
value
|
|
378
|
+
)
|
|
379
|
+
? `Route${value}`
|
|
380
|
+
: value;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function routeToTitle(
|
|
384
|
+
route: string
|
|
385
|
+
): string {
|
|
386
|
+
const segment =
|
|
387
|
+
route
|
|
388
|
+
.split("/")
|
|
389
|
+
.filter(
|
|
390
|
+
(value) =>
|
|
391
|
+
!value.startsWith("(")
|
|
392
|
+
)
|
|
393
|
+
.at(-1) ??
|
|
394
|
+
route;
|
|
395
|
+
const cleaned =
|
|
396
|
+
segment
|
|
397
|
+
.replace(
|
|
398
|
+
/^\[+|\]+$/g,
|
|
399
|
+
""
|
|
400
|
+
)
|
|
401
|
+
.replace(
|
|
402
|
+
/^\.\.\./,
|
|
403
|
+
""
|
|
404
|
+
)
|
|
405
|
+
.replace(
|
|
406
|
+
/[-_]+/g,
|
|
407
|
+
" "
|
|
408
|
+
)
|
|
409
|
+
.trim();
|
|
410
|
+
|
|
411
|
+
return cleaned
|
|
412
|
+
.split(/\s+/)
|
|
413
|
+
.filter(Boolean)
|
|
414
|
+
.map(capitalize)
|
|
415
|
+
.join(" ") ||
|
|
416
|
+
"Generated Page";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function capitalize(
|
|
420
|
+
value: string
|
|
421
|
+
): string {
|
|
422
|
+
return value.length === 0
|
|
423
|
+
? value
|
|
424
|
+
: value[0].toUpperCase() +
|
|
425
|
+
value.slice(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function escapeJsxText(
|
|
429
|
+
value: string
|
|
430
|
+
): string {
|
|
431
|
+
return value
|
|
432
|
+
.replace(/&/g, "&")
|
|
433
|
+
.replace(/</g, "<")
|
|
434
|
+
.replace(/>/g, ">")
|
|
435
|
+
.replace(/{/g, "{")
|
|
436
|
+
.replace(/}/g, "}");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function listFiles(
|
|
440
|
+
directory: string
|
|
441
|
+
): Set<string> {
|
|
442
|
+
if (
|
|
443
|
+
!fs.existsSync(
|
|
444
|
+
directory
|
|
445
|
+
)
|
|
446
|
+
) {
|
|
447
|
+
return new Set();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return new Set(
|
|
451
|
+
fs.readdirSync(
|
|
452
|
+
directory
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function printGenerated(
|
|
458
|
+
rootDirectory: string,
|
|
459
|
+
kind: GenerateKind,
|
|
460
|
+
file: string,
|
|
461
|
+
route?: string
|
|
462
|
+
): void {
|
|
463
|
+
const relative =
|
|
464
|
+
path.relative(
|
|
465
|
+
rootDirectory,
|
|
466
|
+
file
|
|
467
|
+
).replace(
|
|
468
|
+
/\\/g,
|
|
469
|
+
"/"
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
console.log(
|
|
473
|
+
`[BCP Generate] ${kind}: ${relative}${route ? ` -> ${route}` : ""}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
@@ -71,6 +71,11 @@ switch (cliOptions.command) {
|
|
|
71
71
|
break;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
case "generate": {
|
|
75
|
+
await runGenerateCommand();
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
case "doctor": {
|
|
75
80
|
await runDeveloperCommand(
|
|
76
81
|
"doctor"
|
|
@@ -480,6 +485,39 @@ async function runDatabaseCommand() {
|
|
|
480
485
|
}
|
|
481
486
|
}
|
|
482
487
|
|
|
488
|
+
async function runGenerateCommand(): Promise<void> {
|
|
489
|
+
const kind =
|
|
490
|
+
cliOptions.generateKind;
|
|
491
|
+
|
|
492
|
+
if (
|
|
493
|
+
kind === undefined ||
|
|
494
|
+
kind === "help"
|
|
495
|
+
) {
|
|
496
|
+
printGenerateHelp();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const rootDirectory =
|
|
501
|
+
resolveProjectRoot(
|
|
502
|
+
cliOptions.rootDirectory
|
|
503
|
+
);
|
|
504
|
+
const {
|
|
505
|
+
runGenerate,
|
|
506
|
+
} =
|
|
507
|
+
await import(
|
|
508
|
+
"./generate.js"
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
await runGenerate({
|
|
512
|
+
rootDirectory,
|
|
513
|
+
kind,
|
|
514
|
+
name:
|
|
515
|
+
cliOptions.generateName,
|
|
516
|
+
force:
|
|
517
|
+
cliOptions.force,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
483
521
|
async function runDeveloperCommand(
|
|
484
522
|
command:
|
|
485
523
|
"doctor" |
|
|
@@ -695,6 +733,32 @@ Examples:
|
|
|
695
733
|
`);
|
|
696
734
|
}
|
|
697
735
|
|
|
736
|
+
function printGenerateHelp() {
|
|
737
|
+
console.log(`
|
|
738
|
+
BCP Project Generators
|
|
739
|
+
|
|
740
|
+
Usage:
|
|
741
|
+
bcp generate <kind> [name-or-route] [--force]
|
|
742
|
+
|
|
743
|
+
Generators:
|
|
744
|
+
page <route> Create app/<route>/page.tsx
|
|
745
|
+
api <route> Create app/api/<route>/route.ts
|
|
746
|
+
middleware Create middleware.ts in the project root
|
|
747
|
+
migration <name> Create a timestamped migration in migrations/
|
|
748
|
+
|
|
749
|
+
Options:
|
|
750
|
+
--force Replace an existing page/API/middleware scaffold
|
|
751
|
+
--root <path> Project root directory
|
|
752
|
+
|
|
753
|
+
Examples:
|
|
754
|
+
bcp generate page dashboard/users
|
|
755
|
+
bcp generate page users/[id]
|
|
756
|
+
bcp generate api users
|
|
757
|
+
bcp generate middleware
|
|
758
|
+
bcp generate migration create_users
|
|
759
|
+
`);
|
|
760
|
+
}
|
|
761
|
+
|
|
698
762
|
function printHelp() {
|
|
699
763
|
console.log(`
|
|
700
764
|
BCP Framework v${FRAMEWORK_VERSION}
|
|
@@ -709,6 +773,7 @@ Commands:
|
|
|
709
773
|
routes Print discovered page and API routes
|
|
710
774
|
update [target] Update BCP Framework (default target: latest)
|
|
711
775
|
db <command> Manage database migrations
|
|
776
|
+
generate <kind> Generate pages, API routes, middleware or migrations
|
|
712
777
|
doctor Run project/runtime health diagnostics
|
|
713
778
|
inspect Print resolved env, config, dependencies and routes
|
|
714
779
|
help Show this help message
|
|
@@ -722,6 +787,7 @@ Options:
|
|
|
722
787
|
--check Check for a BCP update without changing files
|
|
723
788
|
--dry-run Preview a BCP update without changing files
|
|
724
789
|
--json JSON output for doctor/inspect
|
|
790
|
+
--force Replace existing generated scaffold files
|
|
725
791
|
-h, --help Show help
|
|
726
792
|
-v, --version Show version
|
|
727
793
|
|
|
@@ -734,6 +800,9 @@ Examples:
|
|
|
734
800
|
bcp build
|
|
735
801
|
bcp start
|
|
736
802
|
bcp routes
|
|
803
|
+
bcp generate page dashboard/users
|
|
804
|
+
bcp generate api users
|
|
805
|
+
bcp generate middleware
|
|
737
806
|
bcp doctor
|
|
738
807
|
bcp doctor --json
|
|
739
808
|
bcp inspect
|