create-bcp-app 0.1.2 → 0.1.3

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 CHANGED
@@ -8,16 +8,63 @@ 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/postcss`
31
+ - `postcss.config.mjs`
32
+ - `app/globals.css`
33
+
34
+ ### Database
35
+
36
+ The database choice adds a starter `lib/database.ts`, updates `.env.example`, and installs the matching driver:
37
+
38
+ - MySQL: `mysql2`
39
+ - PostgreSQL: `pg`
40
+ - SQLite: `better-sqlite3`
41
+ - MongoDB: `mongodb`
42
+ - None: no database dependency
43
+
44
+ The generator intentionally installs database drivers directly and does not force an ORM.
45
+
11
46
  ## Options
12
47
 
13
48
  ```text
14
- --no-install Create files without running npm install
15
- --bcp <specifier> Override dependencies.bcp
16
- -h, --help Show help
49
+ --no-install Create files without running npm install
50
+ --tailwind Enable Tailwind CSS without prompting
51
+ --no-tailwind Disable Tailwind CSS without prompting
52
+ --database <database> none | mysql | postgresql | sqlite | mongodb
53
+ -y, --yes Accept defaults (Tailwind enabled, no database)
54
+ --bcp <specifier> Override dependencies.bcp
55
+ -h, --help Show help
56
+ ```
57
+
58
+ Examples:
59
+
60
+ ```bash
61
+ npx create-bcp-app my-app --tailwind --database mysql
62
+ npx create-bcp-app my-app --no-tailwind --database mongodb
63
+ npx create-bcp-app my-app --yes
17
64
  ```
18
65
 
19
66
  The `--bcp` option is primarily useful for prerelease and local package testing, for example:
20
67
 
21
68
  ```bash
22
- npx create-bcp-app my-app --bcp file:../bcp-0.1.0.tgz
69
+ npx create-bcp-app my-app --bcp file:../bcp-0.1.2.tgz
23
70
  ```
@@ -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 Create files without running npm install
148
- --bcp <specifier> Override the package specifier stored under dependencies.bcp
149
- -h, --help Show this help message
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.0.tgz
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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bcp-app",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Create a new BCP Framework application.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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,298 @@
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
+ packageJson.devDependencies = {
180
+ ...packageJson.devDependencies,
181
+ tailwindcss:
182
+ "^4.3.3",
183
+ "@tailwindcss/postcss":
184
+ "^4.3.3",
185
+ };
186
+
187
+ writeFile(
188
+ targetDirectory,
189
+ "postcss.config.mjs",
190
+ `export default {
191
+ plugins: {
192
+ "@tailwindcss/postcss": {},
193
+ },
194
+ };
195
+ `
196
+ );
197
+
198
+ writeFile(
199
+ targetDirectory,
200
+ "app/globals.css",
201
+ `@import "tailwindcss";
202
+
203
+ @theme {
204
+ --font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
205
+ }
206
+ `
207
+ );
208
+ }
209
+
210
+ if (
211
+ selectedDatabase !== "none"
212
+ ) {
213
+ const preset =
214
+ DATABASE_PRESETS[
215
+ selectedDatabase
216
+ ];
217
+
218
+ packageJson.dependencies = {
219
+ ...packageJson.dependencies,
220
+ ...preset.dependencies,
221
+ };
222
+ packageJson.devDependencies = {
223
+ ...packageJson.devDependencies,
224
+ ...preset.devDependencies,
225
+ };
226
+
227
+ writeFile(
228
+ targetDirectory,
229
+ "lib/database.ts",
230
+ preset.source
231
+ );
232
+ appendEnvExample(
233
+ targetDirectory,
234
+ preset.env
235
+ );
236
+ }
237
+
238
+ return {
239
+ tailwind:
240
+ Boolean(
241
+ tailwind
242
+ ),
243
+ database:
244
+ selectedDatabase,
245
+ };
246
+ }
247
+
248
+ function appendEnvExample(
249
+ targetDirectory,
250
+ lines
251
+ ) {
252
+ const filePath =
253
+ path.join(
254
+ targetDirectory,
255
+ ".env.example"
256
+ );
257
+ const current =
258
+ fs.existsSync(
259
+ filePath
260
+ )
261
+ ? fs.readFileSync(
262
+ filePath,
263
+ "utf8"
264
+ ).trimEnd()
265
+ : "";
266
+
267
+ fs.writeFileSync(
268
+ filePath,
269
+ `${current}${current ? "\n\n" : ""}# Database\n${lines.join("\n")}\n`,
270
+ "utf8"
271
+ );
272
+ }
273
+
274
+ function writeFile(
275
+ targetDirectory,
276
+ relativePath,
277
+ content
278
+ ) {
279
+ const filePath =
280
+ path.join(
281
+ targetDirectory,
282
+ relativePath
283
+ );
284
+
285
+ fs.mkdirSync(
286
+ path.dirname(
287
+ filePath
288
+ ),
289
+ {
290
+ recursive: true,
291
+ }
292
+ );
293
+ fs.writeFileSync(
294
+ filePath,
295
+ content,
296
+ "utf8"
297
+ );
298
+ }
@@ -18,6 +18,7 @@
18
18
  "include": [
19
19
  "app/**/*.ts",
20
20
  "app/**/*.tsx",
21
+ "lib/**/*.ts",
21
22
  "bcp.config.ts"
22
23
  ],
23
24
  "exclude": [