create-bcp-app 0.1.2 → 0.1.4
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 +64 -4
- package/bin/create-bcp-app.mjs +321 -4
- package/package.json +3 -3
- package/src/index.d.mts +31 -0
- package/src/index.mjs +23 -0
- package/src/project-options.mjs +508 -0
- package/template/tsconfig.json +1 -0
package/README.md
CHANGED
|
@@ -8,16 +8,76 @@ cd my-app
|
|
|
8
8
|
npm run dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
When running interactively, the generator asks:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
Use Tailwind CSS? (Y/n)
|
|
15
|
+
Select a database:
|
|
16
|
+
1) None
|
|
17
|
+
2) MySQL
|
|
18
|
+
3) PostgreSQL
|
|
19
|
+
4) SQLite
|
|
20
|
+
5) MongoDB
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Generated optional setup
|
|
24
|
+
|
|
25
|
+
### Tailwind CSS
|
|
26
|
+
|
|
27
|
+
When Tailwind is enabled, the project includes:
|
|
28
|
+
|
|
29
|
+
- `tailwindcss`
|
|
30
|
+
- `@tailwindcss/cli`
|
|
31
|
+
- `concurrently`
|
|
32
|
+
- `app/globals.css`
|
|
33
|
+
- `app/api/bcp-styles/route.ts`
|
|
34
|
+
- a stylesheet link in `app/layout.tsx`
|
|
35
|
+
- starter Tailwind utility classes in `app/page.tsx`
|
|
36
|
+
|
|
37
|
+
The generated npm scripts compile Tailwind before starting BCP and keep Tailwind running in watch mode during development:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
npm run css:build
|
|
41
|
+
npm run css:watch
|
|
42
|
+
npm run css:build:prod
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`npm run dev` performs an initial CSS build, then runs the Tailwind watcher and `bcp dev` together. `npm run build` creates a minified stylesheet before the BCP production build.
|
|
46
|
+
|
|
47
|
+
### Database
|
|
48
|
+
|
|
49
|
+
The database choice adds a starter `lib/database.ts`, updates `.env.example`, and installs the matching driver:
|
|
50
|
+
|
|
51
|
+
- MySQL: `mysql2`
|
|
52
|
+
- PostgreSQL: `pg`
|
|
53
|
+
- SQLite: `better-sqlite3`
|
|
54
|
+
- MongoDB: `mongodb`
|
|
55
|
+
- None: no database dependency
|
|
56
|
+
|
|
57
|
+
The generator intentionally installs database drivers directly and does not force an ORM.
|
|
58
|
+
|
|
11
59
|
## Options
|
|
12
60
|
|
|
13
61
|
```text
|
|
14
|
-
--no-install
|
|
15
|
-
--
|
|
16
|
-
-
|
|
62
|
+
--no-install Create files without running npm install
|
|
63
|
+
--tailwind Enable Tailwind CSS without prompting
|
|
64
|
+
--no-tailwind Disable Tailwind CSS without prompting
|
|
65
|
+
--database <database> none | mysql | postgresql | sqlite | mongodb
|
|
66
|
+
-y, --yes Accept defaults (Tailwind enabled, no database)
|
|
67
|
+
--bcp <specifier> Override dependencies.bcp
|
|
68
|
+
-h, --help Show help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx create-bcp-app my-app --tailwind --database mysql
|
|
75
|
+
npx create-bcp-app my-app --no-tailwind --database mongodb
|
|
76
|
+
npx create-bcp-app my-app --yes
|
|
17
77
|
```
|
|
18
78
|
|
|
19
79
|
The `--bcp` option is primarily useful for prerelease and local package testing, for example:
|
|
20
80
|
|
|
21
81
|
```bash
|
|
22
|
-
npx create-bcp-app my-app --bcp file:../bcp-0.1.
|
|
82
|
+
npx create-bcp-app my-app --bcp file:../bcp-0.1.4.tgz
|
|
23
83
|
```
|
package/bin/create-bcp-app.mjs
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
createInterface,
|
|
6
|
+
} from "node:readline/promises";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
stdin as input,
|
|
10
|
+
stdout as output,
|
|
11
|
+
} from "node:process";
|
|
4
12
|
|
|
5
13
|
import {
|
|
6
14
|
createBcpApp,
|
|
7
15
|
} from "../src/index.mjs";
|
|
16
|
+
import {
|
|
17
|
+
DATABASE_CHOICES,
|
|
18
|
+
normalizeDatabase,
|
|
19
|
+
} from "../src/project-options.mjs";
|
|
8
20
|
|
|
9
21
|
const args =
|
|
10
22
|
process.argv.slice(2);
|
|
@@ -23,6 +35,12 @@ let install =
|
|
|
23
35
|
true;
|
|
24
36
|
let bcpPackage =
|
|
25
37
|
undefined;
|
|
38
|
+
let tailwind =
|
|
39
|
+
undefined;
|
|
40
|
+
let database =
|
|
41
|
+
undefined;
|
|
42
|
+
let acceptDefaults =
|
|
43
|
+
false;
|
|
26
44
|
|
|
27
45
|
for (
|
|
28
46
|
let index = 0;
|
|
@@ -40,6 +58,53 @@ for (
|
|
|
40
58
|
continue;
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
if (
|
|
62
|
+
value === "--yes" ||
|
|
63
|
+
value === "-y"
|
|
64
|
+
) {
|
|
65
|
+
acceptDefaults =
|
|
66
|
+
true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
value === "--tailwind"
|
|
72
|
+
) {
|
|
73
|
+
tailwind =
|
|
74
|
+
true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
value === "--no-tailwind"
|
|
80
|
+
) {
|
|
81
|
+
tailwind =
|
|
82
|
+
false;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
value === "--database"
|
|
88
|
+
) {
|
|
89
|
+
const nextValue =
|
|
90
|
+
args[
|
|
91
|
+
index + 1
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
if (!nextValue) {
|
|
95
|
+
fail(
|
|
96
|
+
"--database requires a value."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
database =
|
|
101
|
+
normalizeDatabase(
|
|
102
|
+
nextValue
|
|
103
|
+
);
|
|
104
|
+
index++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
43
108
|
if (
|
|
44
109
|
value === "--bcp"
|
|
45
110
|
) {
|
|
@@ -87,11 +152,22 @@ if (!projectDirectory) {
|
|
|
87
152
|
1;
|
|
88
153
|
} else {
|
|
89
154
|
try {
|
|
155
|
+
const selected =
|
|
156
|
+
await resolveInteractiveOptions({
|
|
157
|
+
tailwind,
|
|
158
|
+
database,
|
|
159
|
+
acceptDefaults,
|
|
160
|
+
});
|
|
161
|
+
|
|
90
162
|
const result =
|
|
91
163
|
await createBcpApp({
|
|
92
164
|
projectDirectory,
|
|
93
165
|
install,
|
|
94
166
|
bcpPackage,
|
|
167
|
+
tailwind:
|
|
168
|
+
selected.tailwind,
|
|
169
|
+
database:
|
|
170
|
+
selected.database,
|
|
95
171
|
packageManager:
|
|
96
172
|
"npm",
|
|
97
173
|
});
|
|
@@ -109,6 +185,12 @@ if (!projectDirectory) {
|
|
|
109
185
|
console.log(
|
|
110
186
|
`Location: ${result.targetDirectory}`
|
|
111
187
|
);
|
|
188
|
+
console.log(
|
|
189
|
+
`Tailwind CSS: ${result.tailwind ? "Yes" : "No"}`
|
|
190
|
+
);
|
|
191
|
+
console.log(
|
|
192
|
+
`Database: ${formatDatabaseName(result.database)}`
|
|
193
|
+
);
|
|
112
194
|
console.log("");
|
|
113
195
|
console.log(
|
|
114
196
|
"Next steps:"
|
|
@@ -136,6 +218,230 @@ if (!projectDirectory) {
|
|
|
136
218
|
}
|
|
137
219
|
}
|
|
138
220
|
|
|
221
|
+
async function resolveInteractiveOptions({
|
|
222
|
+
tailwind: selectedTailwind,
|
|
223
|
+
database: selectedDatabase,
|
|
224
|
+
acceptDefaults: useDefaults,
|
|
225
|
+
}) {
|
|
226
|
+
let resolvedTailwind =
|
|
227
|
+
selectedTailwind;
|
|
228
|
+
let resolvedDatabase =
|
|
229
|
+
selectedDatabase;
|
|
230
|
+
|
|
231
|
+
const interactive =
|
|
232
|
+
Boolean(
|
|
233
|
+
input.isTTY &&
|
|
234
|
+
output.isTTY
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (useDefaults) {
|
|
238
|
+
return {
|
|
239
|
+
tailwind:
|
|
240
|
+
resolvedTailwind ??
|
|
241
|
+
true,
|
|
242
|
+
database:
|
|
243
|
+
normalizeDatabase(
|
|
244
|
+
resolvedDatabase ??
|
|
245
|
+
"none"
|
|
246
|
+
),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!interactive) {
|
|
251
|
+
return {
|
|
252
|
+
tailwind:
|
|
253
|
+
resolvedTailwind ??
|
|
254
|
+
false,
|
|
255
|
+
database:
|
|
256
|
+
normalizeDatabase(
|
|
257
|
+
resolvedDatabase ??
|
|
258
|
+
"none"
|
|
259
|
+
),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const prompt =
|
|
264
|
+
createInterface({
|
|
265
|
+
input,
|
|
266
|
+
output,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
console.log("");
|
|
271
|
+
console.log(
|
|
272
|
+
"Configure your BCP app"
|
|
273
|
+
);
|
|
274
|
+
console.log("");
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
resolvedTailwind ===
|
|
278
|
+
undefined
|
|
279
|
+
) {
|
|
280
|
+
resolvedTailwind =
|
|
281
|
+
await askYesNo(
|
|
282
|
+
prompt,
|
|
283
|
+
"Use Tailwind CSS?",
|
|
284
|
+
true
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
resolvedDatabase ===
|
|
290
|
+
undefined
|
|
291
|
+
) {
|
|
292
|
+
resolvedDatabase =
|
|
293
|
+
await askDatabase(
|
|
294
|
+
prompt
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
prompt.close();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
tailwind:
|
|
303
|
+
Boolean(
|
|
304
|
+
resolvedTailwind
|
|
305
|
+
),
|
|
306
|
+
database:
|
|
307
|
+
normalizeDatabase(
|
|
308
|
+
resolvedDatabase
|
|
309
|
+
),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function askYesNo(
|
|
314
|
+
prompt,
|
|
315
|
+
label,
|
|
316
|
+
defaultValue
|
|
317
|
+
) {
|
|
318
|
+
const suffix =
|
|
319
|
+
defaultValue
|
|
320
|
+
? " (Y/n) "
|
|
321
|
+
: " (y/N) ";
|
|
322
|
+
|
|
323
|
+
while (true) {
|
|
324
|
+
const answer =
|
|
325
|
+
(
|
|
326
|
+
await prompt.question(
|
|
327
|
+
`${label}${suffix}`
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
.trim()
|
|
331
|
+
.toLowerCase();
|
|
332
|
+
|
|
333
|
+
if (!answer) {
|
|
334
|
+
return defaultValue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (
|
|
338
|
+
answer === "y" ||
|
|
339
|
+
answer === "yes"
|
|
340
|
+
) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
answer === "n" ||
|
|
346
|
+
answer === "no"
|
|
347
|
+
) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log(
|
|
352
|
+
"Please answer yes or no."
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function askDatabase(
|
|
358
|
+
prompt
|
|
359
|
+
) {
|
|
360
|
+
const labels = {
|
|
361
|
+
none: "None",
|
|
362
|
+
mysql: "MySQL",
|
|
363
|
+
postgresql: "PostgreSQL",
|
|
364
|
+
sqlite: "SQLite",
|
|
365
|
+
mongodb: "MongoDB",
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
console.log(
|
|
369
|
+
"Select a database:"
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
DATABASE_CHOICES.forEach(
|
|
373
|
+
(
|
|
374
|
+
value,
|
|
375
|
+
index
|
|
376
|
+
) => {
|
|
377
|
+
console.log(
|
|
378
|
+
` ${index + 1}) ${labels[value]}`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
while (true) {
|
|
384
|
+
const answer =
|
|
385
|
+
(
|
|
386
|
+
await prompt.question(
|
|
387
|
+
"Database (1): "
|
|
388
|
+
)
|
|
389
|
+
).trim();
|
|
390
|
+
|
|
391
|
+
if (!answer) {
|
|
392
|
+
return "none";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const index =
|
|
396
|
+
Number(
|
|
397
|
+
answer
|
|
398
|
+
) - 1;
|
|
399
|
+
|
|
400
|
+
if (
|
|
401
|
+
Number.isInteger(
|
|
402
|
+
index
|
|
403
|
+
) &&
|
|
404
|
+
index >= 0 &&
|
|
405
|
+
index <
|
|
406
|
+
DATABASE_CHOICES.length
|
|
407
|
+
) {
|
|
408
|
+
return DATABASE_CHOICES[
|
|
409
|
+
index
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const normalized =
|
|
414
|
+
answer.toLowerCase();
|
|
415
|
+
|
|
416
|
+
if (
|
|
417
|
+
DATABASE_CHOICES.includes(
|
|
418
|
+
normalized
|
|
419
|
+
)
|
|
420
|
+
) {
|
|
421
|
+
return normalized;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log(
|
|
425
|
+
"Choose a number from the list or enter a database name."
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function formatDatabaseName(
|
|
431
|
+
value
|
|
432
|
+
) {
|
|
433
|
+
const names = {
|
|
434
|
+
none: "None",
|
|
435
|
+
mysql: "MySQL",
|
|
436
|
+
postgresql: "PostgreSQL",
|
|
437
|
+
sqlite: "SQLite",
|
|
438
|
+
mongodb: "MongoDB",
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
return names[value] ??
|
|
442
|
+
value;
|
|
443
|
+
}
|
|
444
|
+
|
|
139
445
|
function printHelp() {
|
|
140
446
|
console.log(`
|
|
141
447
|
create-bcp-app
|
|
@@ -143,15 +449,26 @@ create-bcp-app
|
|
|
143
449
|
Usage:
|
|
144
450
|
npx create-bcp-app <project-name> [options]
|
|
145
451
|
|
|
452
|
+
Interactive setup:
|
|
453
|
+
- Choose whether to use Tailwind CSS
|
|
454
|
+
- Choose a database: None, MySQL, PostgreSQL, SQLite or MongoDB
|
|
455
|
+
|
|
146
456
|
Options:
|
|
147
|
-
--no-install
|
|
148
|
-
--
|
|
149
|
-
-
|
|
457
|
+
--no-install Create files without running npm install
|
|
458
|
+
--tailwind Enable Tailwind CSS without prompting
|
|
459
|
+
--no-tailwind Disable Tailwind CSS without prompting
|
|
460
|
+
--database <database> none | mysql | postgresql | sqlite | mongodb
|
|
461
|
+
-y, --yes Accept defaults (Tailwind enabled, no database)
|
|
462
|
+
--bcp <specifier> Override the package specifier stored under dependencies.bcp
|
|
463
|
+
-h, --help Show this help message
|
|
150
464
|
|
|
151
465
|
Examples:
|
|
152
466
|
npx create-bcp-app my-app
|
|
467
|
+
npx create-bcp-app my-app --tailwind --database mysql
|
|
468
|
+
npx create-bcp-app my-app --no-tailwind --database mongodb
|
|
469
|
+
npx create-bcp-app my-app --yes
|
|
153
470
|
npx create-bcp-app my-app --no-install
|
|
154
|
-
npx create-bcp-app my-app --bcp file:../bcp-0.1.
|
|
471
|
+
npx create-bcp-app my-app --bcp file:../bcp-0.1.2.tgz
|
|
155
472
|
`);
|
|
156
473
|
}
|
|
157
474
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-bcp-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Create a new BCP Framework application.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"bcpFrameworkPackage": "@chidchanun/bcp",
|
|
7
8
|
"bin": {
|
|
8
9
|
"create-bcp-app": "bin/create-bcp-app.mjs"
|
|
9
10
|
},
|
|
@@ -24,6 +25,5 @@
|
|
|
24
25
|
},
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public"
|
|
27
|
-
}
|
|
28
|
-
"bcpFrameworkPackage": "@chidchanun/bcp"
|
|
28
|
+
}
|
|
29
29
|
}
|
package/src/index.d.mts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type BcpDatabase =
|
|
2
|
+
| "none"
|
|
3
|
+
| "mysql"
|
|
4
|
+
| "postgresql"
|
|
5
|
+
| "sqlite"
|
|
6
|
+
| "mongodb";
|
|
7
|
+
|
|
8
|
+
export interface CreateBcpAppOptions {
|
|
9
|
+
projectDirectory: string;
|
|
10
|
+
install?: boolean;
|
|
11
|
+
bcpPackage?: string;
|
|
12
|
+
tailwind?: boolean;
|
|
13
|
+
database?: BcpDatabase;
|
|
14
|
+
packageManager?: "npm";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CreateBcpAppResult {
|
|
18
|
+
targetDirectory: string;
|
|
19
|
+
projectName: string;
|
|
20
|
+
packageSpecifier: string;
|
|
21
|
+
tailwind: boolean;
|
|
22
|
+
database: BcpDatabase;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createBcpApp(
|
|
26
|
+
options: CreateBcpAppOptions
|
|
27
|
+
): Promise<CreateBcpAppResult>;
|
|
28
|
+
|
|
29
|
+
export function validateProjectName(
|
|
30
|
+
value: unknown
|
|
31
|
+
): string;
|
package/src/index.mjs
CHANGED
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
fileURLToPath,
|
|
8
8
|
} from "node:url";
|
|
9
9
|
|
|
10
|
+
import {
|
|
11
|
+
applyProjectOptions,
|
|
12
|
+
normalizeDatabase,
|
|
13
|
+
} from "./project-options.mjs";
|
|
14
|
+
|
|
10
15
|
const packageRoot =
|
|
11
16
|
path.resolve(
|
|
12
17
|
path.dirname(
|
|
@@ -31,6 +36,12 @@ export async function createBcpApp(
|
|
|
31
36
|
targetDirectory
|
|
32
37
|
)
|
|
33
38
|
);
|
|
39
|
+
const tailwind =
|
|
40
|
+
options.tailwind === true;
|
|
41
|
+
const database =
|
|
42
|
+
normalizeDatabase(
|
|
43
|
+
options.database
|
|
44
|
+
);
|
|
34
45
|
|
|
35
46
|
ensureTargetDirectory(
|
|
36
47
|
targetDirectory
|
|
@@ -102,6 +113,14 @@ export async function createBcpApp(
|
|
|
102
113
|
},
|
|
103
114
|
};
|
|
104
115
|
|
|
116
|
+
const selectedOptions =
|
|
117
|
+
applyProjectOptions({
|
|
118
|
+
targetDirectory,
|
|
119
|
+
packageJson,
|
|
120
|
+
tailwind,
|
|
121
|
+
database,
|
|
122
|
+
});
|
|
123
|
+
|
|
105
124
|
fs.writeFileSync(
|
|
106
125
|
path.join(
|
|
107
126
|
targetDirectory,
|
|
@@ -129,6 +148,10 @@ export async function createBcpApp(
|
|
|
129
148
|
targetDirectory,
|
|
130
149
|
projectName,
|
|
131
150
|
packageSpecifier,
|
|
151
|
+
tailwind:
|
|
152
|
+
selectedOptions.tailwind,
|
|
153
|
+
database:
|
|
154
|
+
selectedOptions.database,
|
|
132
155
|
};
|
|
133
156
|
}
|
|
134
157
|
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DATABASE_CHOICES = [
|
|
5
|
+
"none",
|
|
6
|
+
"mysql",
|
|
7
|
+
"postgresql",
|
|
8
|
+
"sqlite",
|
|
9
|
+
"mongodb",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const DATABASE_PRESETS = {
|
|
13
|
+
mysql: {
|
|
14
|
+
dependencies: {
|
|
15
|
+
mysql2: "^3.24.2",
|
|
16
|
+
},
|
|
17
|
+
devDependencies: {},
|
|
18
|
+
env: [
|
|
19
|
+
"DATABASE_URL=mysql://root:password@localhost:3306/bcp_app",
|
|
20
|
+
],
|
|
21
|
+
source: `import mysql from "mysql2/promise";
|
|
22
|
+
|
|
23
|
+
const connectionString =
|
|
24
|
+
process.env.DATABASE_URL;
|
|
25
|
+
|
|
26
|
+
if (!connectionString) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"DATABASE_URL is required for MySQL."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const db =
|
|
33
|
+
mysql.createPool(
|
|
34
|
+
connectionString
|
|
35
|
+
);
|
|
36
|
+
`,
|
|
37
|
+
},
|
|
38
|
+
postgresql: {
|
|
39
|
+
dependencies: {
|
|
40
|
+
pg: "^8.23.0",
|
|
41
|
+
},
|
|
42
|
+
devDependencies: {
|
|
43
|
+
"@types/pg": "^8.23.1",
|
|
44
|
+
},
|
|
45
|
+
env: [
|
|
46
|
+
"DATABASE_URL=postgresql://postgres:password@localhost:5432/bcp_app",
|
|
47
|
+
],
|
|
48
|
+
source: `import {
|
|
49
|
+
Pool,
|
|
50
|
+
} from "pg";
|
|
51
|
+
|
|
52
|
+
const connectionString =
|
|
53
|
+
process.env.DATABASE_URL;
|
|
54
|
+
|
|
55
|
+
if (!connectionString) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"DATABASE_URL is required for PostgreSQL."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const db =
|
|
62
|
+
new Pool({
|
|
63
|
+
connectionString,
|
|
64
|
+
});
|
|
65
|
+
`,
|
|
66
|
+
},
|
|
67
|
+
sqlite: {
|
|
68
|
+
dependencies: {
|
|
69
|
+
"better-sqlite3": "^13.0.3",
|
|
70
|
+
},
|
|
71
|
+
devDependencies: {
|
|
72
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
73
|
+
},
|
|
74
|
+
env: [
|
|
75
|
+
"DATABASE_URL=./data/bcp.sqlite",
|
|
76
|
+
],
|
|
77
|
+
source: `import fs from "node:fs";
|
|
78
|
+
import path from "node:path";
|
|
79
|
+
|
|
80
|
+
import Database from "better-sqlite3";
|
|
81
|
+
|
|
82
|
+
const databaseFile =
|
|
83
|
+
process.env.DATABASE_URL ??
|
|
84
|
+
"./data/bcp.sqlite";
|
|
85
|
+
const resolvedFile =
|
|
86
|
+
path.resolve(
|
|
87
|
+
databaseFile
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
fs.mkdirSync(
|
|
91
|
+
path.dirname(
|
|
92
|
+
resolvedFile
|
|
93
|
+
),
|
|
94
|
+
{
|
|
95
|
+
recursive: true,
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
export const db =
|
|
100
|
+
new Database(
|
|
101
|
+
resolvedFile
|
|
102
|
+
);
|
|
103
|
+
`,
|
|
104
|
+
},
|
|
105
|
+
mongodb: {
|
|
106
|
+
dependencies: {
|
|
107
|
+
mongodb: "^7.6.0",
|
|
108
|
+
},
|
|
109
|
+
devDependencies: {},
|
|
110
|
+
env: [
|
|
111
|
+
"DATABASE_URL=mongodb://localhost:27017",
|
|
112
|
+
"DATABASE_NAME=bcp_app",
|
|
113
|
+
],
|
|
114
|
+
source: `import {
|
|
115
|
+
MongoClient,
|
|
116
|
+
} from "mongodb";
|
|
117
|
+
|
|
118
|
+
const connectionString =
|
|
119
|
+
process.env.DATABASE_URL;
|
|
120
|
+
|
|
121
|
+
if (!connectionString) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"DATABASE_URL is required for MongoDB."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const mongoClient =
|
|
128
|
+
new MongoClient(
|
|
129
|
+
connectionString
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
export async function getDatabase() {
|
|
133
|
+
await mongoClient.connect();
|
|
134
|
+
|
|
135
|
+
return mongoClient.db(
|
|
136
|
+
process.env.DATABASE_NAME ??
|
|
137
|
+
"bcp_app"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
`,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export function normalizeDatabase(
|
|
145
|
+
value
|
|
146
|
+
) {
|
|
147
|
+
const normalized =
|
|
148
|
+
String(
|
|
149
|
+
value ?? "none"
|
|
150
|
+
)
|
|
151
|
+
.trim()
|
|
152
|
+
.toLowerCase();
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
!DATABASE_CHOICES.includes(
|
|
156
|
+
normalized
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Unsupported database: ${normalized}. Choose one of: ${DATABASE_CHOICES.join(", ")}.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return normalized;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function applyProjectOptions({
|
|
168
|
+
targetDirectory,
|
|
169
|
+
packageJson,
|
|
170
|
+
tailwind = false,
|
|
171
|
+
database = "none",
|
|
172
|
+
}) {
|
|
173
|
+
const selectedDatabase =
|
|
174
|
+
normalizeDatabase(
|
|
175
|
+
database
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (tailwind) {
|
|
179
|
+
configureTailwind(
|
|
180
|
+
targetDirectory,
|
|
181
|
+
packageJson
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
selectedDatabase !== "none"
|
|
187
|
+
) {
|
|
188
|
+
const preset =
|
|
189
|
+
DATABASE_PRESETS[
|
|
190
|
+
selectedDatabase
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
packageJson.dependencies = {
|
|
194
|
+
...packageJson.dependencies,
|
|
195
|
+
...preset.dependencies,
|
|
196
|
+
};
|
|
197
|
+
packageJson.devDependencies = {
|
|
198
|
+
...packageJson.devDependencies,
|
|
199
|
+
...preset.devDependencies,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
writeFile(
|
|
203
|
+
targetDirectory,
|
|
204
|
+
"lib/database.ts",
|
|
205
|
+
preset.source
|
|
206
|
+
);
|
|
207
|
+
appendEnvExample(
|
|
208
|
+
targetDirectory,
|
|
209
|
+
preset.env
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
tailwind:
|
|
215
|
+
Boolean(
|
|
216
|
+
tailwind
|
|
217
|
+
),
|
|
218
|
+
database:
|
|
219
|
+
selectedDatabase,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function configureTailwind(
|
|
224
|
+
targetDirectory,
|
|
225
|
+
packageJson
|
|
226
|
+
) {
|
|
227
|
+
packageJson.devDependencies = {
|
|
228
|
+
...packageJson.devDependencies,
|
|
229
|
+
tailwindcss:
|
|
230
|
+
"^4.3.3",
|
|
231
|
+
"@tailwindcss/cli":
|
|
232
|
+
"^4.3.3",
|
|
233
|
+
concurrently:
|
|
234
|
+
"^10.0.5",
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
packageJson.scripts = {
|
|
238
|
+
...packageJson.scripts,
|
|
239
|
+
dev:
|
|
240
|
+
"npm run css:build && concurrently -k -n tailwind,bcp \"npm run css:watch\" \"bcp dev\"",
|
|
241
|
+
build:
|
|
242
|
+
"npm run css:build:prod && bcp build",
|
|
243
|
+
"css:build":
|
|
244
|
+
"tailwindcss -i ./app/globals.css -o ./public/bcp.css",
|
|
245
|
+
"css:watch":
|
|
246
|
+
"tailwindcss -i ./app/globals.css -o ./public/bcp.css --watch",
|
|
247
|
+
"css:build:prod":
|
|
248
|
+
"tailwindcss -i ./app/globals.css -o ./public/bcp.css --minify",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
writeFile(
|
|
252
|
+
targetDirectory,
|
|
253
|
+
"app/globals.css",
|
|
254
|
+
`@import "tailwindcss" source("../");
|
|
255
|
+
|
|
256
|
+
@theme {
|
|
257
|
+
--font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
258
|
+
}
|
|
259
|
+
`
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
writeFile(
|
|
263
|
+
targetDirectory,
|
|
264
|
+
"app/api/bcp-styles/route.ts",
|
|
265
|
+
`import fs from "node:fs/promises";
|
|
266
|
+
import path from "node:path";
|
|
267
|
+
|
|
268
|
+
export async function GET() {
|
|
269
|
+
const stylesheet =
|
|
270
|
+
path.join(
|
|
271
|
+
process.cwd(),
|
|
272
|
+
"public",
|
|
273
|
+
"bcp.css"
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const css =
|
|
278
|
+
await fs.readFile(
|
|
279
|
+
stylesheet,
|
|
280
|
+
"utf8"
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return new Response(
|
|
284
|
+
css,
|
|
285
|
+
{
|
|
286
|
+
headers: {
|
|
287
|
+
"Content-Type":
|
|
288
|
+
"text/css; charset=utf-8",
|
|
289
|
+
"Cache-Control":
|
|
290
|
+
"no-store, max-age=0",
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
} catch {
|
|
295
|
+
return new Response(
|
|
296
|
+
"/* BCP Tailwind stylesheet has not been built yet. */",
|
|
297
|
+
{
|
|
298
|
+
status: 503,
|
|
299
|
+
headers: {
|
|
300
|
+
"Content-Type":
|
|
301
|
+
"text/css; charset=utf-8",
|
|
302
|
+
"Cache-Control":
|
|
303
|
+
"no-store, max-age=0",
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
`
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
fs.mkdirSync(
|
|
313
|
+
path.join(
|
|
314
|
+
targetDirectory,
|
|
315
|
+
"public"
|
|
316
|
+
),
|
|
317
|
+
{
|
|
318
|
+
recursive: true,
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
injectTailwindStylesheet(
|
|
323
|
+
targetDirectory
|
|
324
|
+
);
|
|
325
|
+
applyTailwindStarterStyles(
|
|
326
|
+
targetDirectory
|
|
327
|
+
);
|
|
328
|
+
appendGitIgnore(
|
|
329
|
+
targetDirectory,
|
|
330
|
+
"public/bcp.css"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function injectTailwindStylesheet(
|
|
335
|
+
targetDirectory
|
|
336
|
+
) {
|
|
337
|
+
const layoutFile =
|
|
338
|
+
path.join(
|
|
339
|
+
targetDirectory,
|
|
340
|
+
"app",
|
|
341
|
+
"layout.tsx"
|
|
342
|
+
);
|
|
343
|
+
const source =
|
|
344
|
+
fs.readFileSync(
|
|
345
|
+
layoutFile,
|
|
346
|
+
"utf8"
|
|
347
|
+
);
|
|
348
|
+
const rootPattern =
|
|
349
|
+
/^([ \t]*)<div>[ \t]*(\r?\n)/m;
|
|
350
|
+
const match =
|
|
351
|
+
rootPattern.exec(
|
|
352
|
+
source
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
if (!match) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
"BCP create-app could not attach the Tailwind stylesheet to app/layout.tsx."
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const indent =
|
|
362
|
+
match[1];
|
|
363
|
+
const newline =
|
|
364
|
+
match[2];
|
|
365
|
+
const childIndent =
|
|
366
|
+
`${indent} `;
|
|
367
|
+
const propertyIndent =
|
|
368
|
+
`${childIndent} `;
|
|
369
|
+
const replacement =
|
|
370
|
+
`${indent}<div>${newline}` +
|
|
371
|
+
`${childIndent}<link${newline}` +
|
|
372
|
+
`${propertyIndent}rel="stylesheet"${newline}` +
|
|
373
|
+
`${propertyIndent}href="/api/bcp-styles"${newline}` +
|
|
374
|
+
`${childIndent}/>${newline}`;
|
|
375
|
+
|
|
376
|
+
fs.writeFileSync(
|
|
377
|
+
layoutFile,
|
|
378
|
+
source.replace(
|
|
379
|
+
rootPattern,
|
|
380
|
+
replacement
|
|
381
|
+
),
|
|
382
|
+
"utf8"
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function applyTailwindStarterStyles(
|
|
387
|
+
targetDirectory
|
|
388
|
+
) {
|
|
389
|
+
const pageFile =
|
|
390
|
+
path.join(
|
|
391
|
+
targetDirectory,
|
|
392
|
+
"app",
|
|
393
|
+
"page.tsx"
|
|
394
|
+
);
|
|
395
|
+
let source =
|
|
396
|
+
fs.readFileSync(
|
|
397
|
+
pageFile,
|
|
398
|
+
"utf8"
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
source =
|
|
402
|
+
source.replace(
|
|
403
|
+
" <section>",
|
|
404
|
+
" <section className=\"mx-auto max-w-3xl space-y-6 px-6 py-16\">"
|
|
405
|
+
);
|
|
406
|
+
source =
|
|
407
|
+
source.replace(
|
|
408
|
+
" <h1>",
|
|
409
|
+
" <h1 className=\"text-4xl font-bold tracking-tight text-slate-900\">"
|
|
410
|
+
);
|
|
411
|
+
source =
|
|
412
|
+
source.replace(
|
|
413
|
+
" <p>",
|
|
414
|
+
" <p className=\"text-lg text-slate-600\">"
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
fs.writeFileSync(
|
|
418
|
+
pageFile,
|
|
419
|
+
source,
|
|
420
|
+
"utf8"
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function appendGitIgnore(
|
|
425
|
+
targetDirectory,
|
|
426
|
+
entry
|
|
427
|
+
) {
|
|
428
|
+
const filePath =
|
|
429
|
+
path.join(
|
|
430
|
+
targetDirectory,
|
|
431
|
+
".gitignore"
|
|
432
|
+
);
|
|
433
|
+
const current =
|
|
434
|
+
fs.existsSync(
|
|
435
|
+
filePath
|
|
436
|
+
)
|
|
437
|
+
? fs.readFileSync(
|
|
438
|
+
filePath,
|
|
439
|
+
"utf8"
|
|
440
|
+
).trimEnd()
|
|
441
|
+
: "";
|
|
442
|
+
const lines =
|
|
443
|
+
new Set(
|
|
444
|
+
current
|
|
445
|
+
.split(/\r?\n/)
|
|
446
|
+
.filter(Boolean)
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
lines.add(entry);
|
|
450
|
+
|
|
451
|
+
fs.writeFileSync(
|
|
452
|
+
filePath,
|
|
453
|
+
`${Array.from(lines).join("\n")}\n`,
|
|
454
|
+
"utf8"
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function appendEnvExample(
|
|
459
|
+
targetDirectory,
|
|
460
|
+
lines
|
|
461
|
+
) {
|
|
462
|
+
const filePath =
|
|
463
|
+
path.join(
|
|
464
|
+
targetDirectory,
|
|
465
|
+
".env.example"
|
|
466
|
+
);
|
|
467
|
+
const current =
|
|
468
|
+
fs.existsSync(
|
|
469
|
+
filePath
|
|
470
|
+
)
|
|
471
|
+
? fs.readFileSync(
|
|
472
|
+
filePath,
|
|
473
|
+
"utf8"
|
|
474
|
+
).trimEnd()
|
|
475
|
+
: "";
|
|
476
|
+
|
|
477
|
+
fs.writeFileSync(
|
|
478
|
+
filePath,
|
|
479
|
+
`${current}${current ? "\n\n" : ""}# Database\n${lines.join("\n")}\n`,
|
|
480
|
+
"utf8"
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function writeFile(
|
|
485
|
+
targetDirectory,
|
|
486
|
+
relativePath,
|
|
487
|
+
content
|
|
488
|
+
) {
|
|
489
|
+
const filePath =
|
|
490
|
+
path.join(
|
|
491
|
+
targetDirectory,
|
|
492
|
+
relativePath
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
fs.mkdirSync(
|
|
496
|
+
path.dirname(
|
|
497
|
+
filePath
|
|
498
|
+
),
|
|
499
|
+
{
|
|
500
|
+
recursive: true,
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
fs.writeFileSync(
|
|
504
|
+
filePath,
|
|
505
|
+
content,
|
|
506
|
+
"utf8"
|
|
507
|
+
);
|
|
508
|
+
}
|