create-bcp-app 0.1.11 → 0.1.13

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.
@@ -2,9 +2,8 @@
2
2
 
3
3
  import path from "node:path";
4
4
  import {
5
- createInterface,
6
- } from "node:readline/promises";
7
-
5
+ emitKeypressEvents,
6
+ } from "node:readline";
8
7
  import {
9
8
  stdin as input,
10
9
  stdout as output,
@@ -20,402 +19,253 @@ import {
20
19
  normalizeDatabase,
21
20
  } from "../src/project-options.mjs";
22
21
 
23
- const args =
24
- process.argv.slice(2);
22
+ const ANSI = {
23
+ reset: "\u001B[0m",
24
+ selected: "\u001B[46m\u001B[30m\u001B[1m",
25
+ muted: "\u001B[2m",
26
+ };
25
27
 
26
- if (
27
- args.includes("--help") ||
28
- args.includes("-h")
29
- ) {
30
- printHelp();
31
- process.exit(0);
32
- }
28
+ await main();
33
29
 
34
- let projectDirectory =
35
- null;
36
- let install =
37
- true;
38
- let bcpPackage =
39
- undefined;
40
- let tailwind =
41
- undefined;
42
- let database =
43
- undefined;
44
- let auth =
45
- undefined;
46
- let acceptDefaults =
47
- false;
48
-
49
- for (
50
- let index = 0;
51
- index < args.length;
52
- index++
53
- ) {
54
- const value =
55
- args[index];
30
+ async function main() {
31
+ const args = process.argv.slice(2);
56
32
 
57
33
  if (
58
- value === "--no-install"
34
+ args.includes("--help") ||
35
+ args.includes("-h")
59
36
  ) {
60
- install =
61
- false;
62
- continue;
37
+ printHelp();
38
+ return;
63
39
  }
64
40
 
65
- if (
66
- value === "--yes" ||
67
- value === "-y"
68
- ) {
69
- acceptDefaults =
70
- true;
71
- continue;
41
+ let parsed;
42
+
43
+ try {
44
+ parsed = parseArguments(args);
45
+ } catch (error) {
46
+ reportError(error);
47
+ return;
72
48
  }
73
49
 
74
- if (
75
- value === "--tailwind"
76
- ) {
77
- tailwind =
78
- true;
79
- continue;
50
+ if (!parsed.projectDirectory) {
51
+ printHelp();
52
+ process.exitCode = 1;
53
+ return;
80
54
  }
81
55
 
82
- if (
83
- value === "--no-tailwind"
84
- ) {
85
- tailwind =
86
- false;
87
- continue;
56
+ try {
57
+ const resolved = await resolveOptions(parsed);
58
+
59
+ console.log("");
60
+ console.log(
61
+ parsed.install
62
+ ? `Creating BCP app and installing dependencies in ${path.resolve(parsed.projectDirectory)}...`
63
+ : `Creating BCP app in ${path.resolve(parsed.projectDirectory)}...`
64
+ );
65
+ console.log("");
66
+
67
+ const result = await createBcpApp({
68
+ projectDirectory: parsed.projectDirectory,
69
+ install: parsed.install,
70
+ bcpPackage: parsed.bcpPackage,
71
+ tailwind: resolved.tailwind,
72
+ database: resolved.database,
73
+ auth: resolved.auth,
74
+ packageManager: "npm",
75
+ });
76
+
77
+ printSuccess(result, parsed.install);
78
+ } catch (error) {
79
+ reportError(error);
88
80
  }
81
+ }
89
82
 
90
- if (
91
- value === "--database"
83
+ function parseArguments(args) {
84
+ let projectDirectory = null;
85
+ let install = true;
86
+ let bcpPackage = undefined;
87
+ let tailwind = undefined;
88
+ let database = undefined;
89
+ let auth = undefined;
90
+ let acceptDefaults = false;
91
+
92
+ for (
93
+ let index = 0;
94
+ index < args.length;
95
+ index++
92
96
  ) {
93
- const nextValue =
94
- args[
95
- index + 1
96
- ];
97
-
98
- if (!nextValue) {
99
- fail(
100
- "--database requires a value."
101
- );
97
+ const value = args[index];
98
+
99
+ if (value === "--no-install") {
100
+ install = false;
101
+ continue;
102
102
  }
103
103
 
104
- database =
105
- normalizeDatabase(
106
- nextValue
107
- );
108
- index++;
109
- continue;
110
- }
104
+ if (
105
+ value === "--yes" ||
106
+ value === "-y"
107
+ ) {
108
+ acceptDefaults = true;
109
+ continue;
110
+ }
111
111
 
112
- if (
113
- value === "--auth"
114
- ) {
115
- const nextValue =
116
- args[
117
- index + 1
118
- ];
119
-
120
- if (!nextValue) {
121
- fail(
122
- "--auth requires a value."
123
- );
112
+ if (value === "--tailwind") {
113
+ tailwind = true;
114
+ continue;
124
115
  }
125
116
 
126
- auth =
127
- normalizeAuth(
128
- nextValue
129
- );
130
- index++;
131
- continue;
132
- }
117
+ if (value === "--no-tailwind") {
118
+ tailwind = false;
119
+ continue;
120
+ }
133
121
 
134
- if (
135
- value === "--bcp"
136
- ) {
137
- const nextValue =
138
- args[
139
- index + 1
140
- ];
141
-
142
- if (!nextValue) {
143
- fail(
144
- "--bcp requires a package specifier."
145
- );
122
+ if (value === "--database") {
123
+ const nextValue = args[index + 1];
124
+
125
+ if (!nextValue) {
126
+ throw new Error(
127
+ "--database requires a value."
128
+ );
129
+ }
130
+
131
+ database = normalizeDatabase(nextValue);
132
+ index++;
133
+ continue;
146
134
  }
147
135
 
148
- bcpPackage =
149
- nextValue;
150
- index++;
151
- continue;
152
- }
136
+ if (value === "--auth") {
137
+ const nextValue = args[index + 1];
153
138
 
154
- if (
155
- value.startsWith("-")
156
- ) {
157
- fail(
158
- `Unknown option: ${value}`
159
- );
160
- }
139
+ if (!nextValue) {
140
+ throw new Error(
141
+ "--auth requires a value."
142
+ );
143
+ }
161
144
 
162
- if (
163
- projectDirectory !==
164
- null
165
- ) {
166
- fail(
167
- "Only one project directory may be provided."
168
- );
169
- }
145
+ auth = normalizeAuth(nextValue);
146
+ index++;
147
+ continue;
148
+ }
170
149
 
171
- projectDirectory =
172
- value;
173
- }
150
+ if (value === "--bcp") {
151
+ const nextValue = args[index + 1];
174
152
 
175
- if (!projectDirectory) {
176
- printHelp();
177
- process.exitCode =
178
- 1;
179
- } else {
180
- try {
181
- const selected =
182
- await resolveInteractiveOptions({
183
- tailwind,
184
- database,
185
- auth,
186
- acceptDefaults,
187
- });
188
-
189
- const result =
190
- await createBcpApp({
191
- projectDirectory,
192
- install,
193
- bcpPackage,
194
- tailwind:
195
- selected.tailwind,
196
- database:
197
- selected.database,
198
- auth:
199
- selected.auth,
200
- packageManager:
201
- "npm",
202
- });
203
-
204
- const relative =
205
- path.relative(
206
- process.cwd(),
207
- result.targetDirectory
208
- ) || ".";
153
+ if (!nextValue) {
154
+ throw new Error(
155
+ "--bcp requires a package specifier."
156
+ );
157
+ }
209
158
 
210
- console.log("");
211
- console.log(
212
- `Created BCP app: ${result.projectName}`
213
- );
214
- console.log(
215
- `Location: ${result.targetDirectory}`
216
- );
217
- console.log(
218
- `Tailwind CSS: ${result.tailwind ? "Yes" : "No"}`
219
- );
220
- console.log(
221
- `Database: ${formatDatabaseName(result.database)}`
222
- );
223
- console.log(
224
- `Authentication: ${formatAuthName(result.auth)}`
225
- );
226
- console.log("");
227
- console.log(
228
- "Next steps:"
229
- );
230
- console.log(
231
- ` cd ${quoteIfNeeded(relative)}`
232
- );
159
+ bcpPackage = nextValue;
160
+ index++;
161
+ continue;
162
+ }
233
163
 
234
- if (!install) {
235
- console.log(
236
- " npm install"
164
+ if (value.startsWith("-")) {
165
+ throw new Error(
166
+ `Unknown option: ${value}`
237
167
  );
238
168
  }
239
169
 
240
- console.log(
241
- " npm run dev"
242
- );
243
- console.log("");
244
- } catch (error) {
245
- fail(
246
- error instanceof Error
247
- ? error.message
248
- : String(error)
249
- );
170
+ if (projectDirectory !== null) {
171
+ throw new Error(
172
+ "Only one project directory may be provided."
173
+ );
174
+ }
175
+
176
+ projectDirectory = value;
250
177
  }
178
+
179
+ return {
180
+ projectDirectory,
181
+ install,
182
+ bcpPackage,
183
+ tailwind,
184
+ database,
185
+ auth,
186
+ acceptDefaults,
187
+ };
251
188
  }
252
189
 
253
- async function resolveInteractiveOptions({
190
+ async function resolveOptions({
254
191
  tailwind: selectedTailwind,
255
192
  database: selectedDatabase,
256
193
  auth: selectedAuth,
257
- acceptDefaults: useDefaults,
194
+ acceptDefaults,
258
195
  }) {
259
- let resolvedTailwind =
260
- selectedTailwind;
261
- let resolvedDatabase =
262
- selectedDatabase;
263
- let resolvedAuth =
264
- selectedAuth;
265
-
266
- const interactive =
267
- Boolean(
268
- input.isTTY &&
269
- output.isTTY
270
- );
196
+ let tailwind = selectedTailwind;
197
+ let database = selectedDatabase;
198
+ let auth = selectedAuth;
271
199
 
272
- if (useDefaults) {
200
+ if (acceptDefaults) {
273
201
  return {
274
- tailwind:
275
- resolvedTailwind ??
276
- true,
277
- database:
278
- normalizeDatabase(
279
- resolvedDatabase ??
280
- "none"
281
- ),
282
- auth:
283
- normalizeAuth(
284
- resolvedAuth ??
285
- "none"
286
- ),
202
+ tailwind: tailwind ?? true,
203
+ database: normalizeDatabase(
204
+ database ?? "none"
205
+ ),
206
+ auth: normalizeAuth(
207
+ auth ?? "none"
208
+ ),
287
209
  };
288
210
  }
289
211
 
212
+ const interactive = Boolean(
213
+ input.isTTY &&
214
+ output.isTTY
215
+ );
216
+
290
217
  if (!interactive) {
291
218
  return {
292
- tailwind:
293
- resolvedTailwind ??
294
- false,
295
- database:
296
- normalizeDatabase(
297
- resolvedDatabase ??
298
- "none"
299
- ),
300
- auth:
301
- normalizeAuth(
302
- resolvedAuth ??
303
- "none"
304
- ),
219
+ tailwind: tailwind ?? false,
220
+ database: normalizeDatabase(
221
+ database ?? "none"
222
+ ),
223
+ auth: normalizeAuth(
224
+ auth ?? "none"
225
+ ),
305
226
  };
306
227
  }
307
228
 
308
- const prompt =
309
- createInterface({
310
- input,
311
- output,
312
- });
229
+ console.log("");
230
+ console.log("Configure your BCP app");
231
+ console.log("");
313
232
 
314
- try {
315
- console.log("");
316
- console.log(
317
- "Configure your BCP app"
318
- );
319
- console.log("");
320
-
321
- if (
322
- resolvedTailwind ===
323
- undefined
324
- ) {
325
- resolvedTailwind =
326
- await askYesNo(
327
- prompt,
328
- "Use Tailwind CSS?",
329
- true
330
- );
331
- }
233
+ if (tailwind === undefined) {
234
+ tailwind = await askTailwind();
235
+ }
332
236
 
333
- if (
334
- resolvedDatabase ===
335
- undefined
336
- ) {
337
- resolvedDatabase =
338
- await askDatabase(
339
- prompt
340
- );
341
- }
237
+ if (database === undefined) {
238
+ database = await askDatabase();
239
+ }
342
240
 
343
- if (
344
- resolvedAuth ===
345
- undefined
346
- ) {
347
- resolvedAuth =
348
- await askAuth(
349
- prompt
350
- );
351
- }
352
- } finally {
353
- prompt.close();
241
+ if (auth === undefined) {
242
+ auth = await askAuth();
354
243
  }
355
244
 
356
245
  return {
357
- tailwind:
358
- Boolean(
359
- resolvedTailwind
360
- ),
361
- database:
362
- normalizeDatabase(
363
- resolvedDatabase
364
- ),
365
- auth:
366
- normalizeAuth(
367
- resolvedAuth
368
- ),
246
+ tailwind: Boolean(tailwind),
247
+ database: normalizeDatabase(database),
248
+ auth: normalizeAuth(auth),
369
249
  };
370
250
  }
371
251
 
372
- async function askYesNo(
373
- prompt,
374
- label,
375
- defaultValue
376
- ) {
377
- const suffix =
378
- defaultValue
379
- ? " (Y/n) "
380
- : " (y/N) ";
381
-
382
- while (true) {
383
- const answer =
384
- (
385
- await prompt.question(
386
- `${label}${suffix}`
387
- )
388
- )
389
- .trim()
390
- .toLowerCase();
391
-
392
- if (!answer) {
393
- return defaultValue;
394
- }
395
-
396
- if (
397
- answer === "y" ||
398
- answer === "yes"
399
- ) {
400
- return true;
401
- }
402
-
403
- if (
404
- answer === "n" ||
405
- answer === "no"
406
- ) {
407
- return false;
408
- }
409
-
410
- console.log(
411
- "Please answer yes or no."
412
- );
413
- }
252
+ function askTailwind() {
253
+ return askTerminalSelect(
254
+ "Use Tailwind CSS?",
255
+ [
256
+ {
257
+ value: true,
258
+ label: "Yes",
259
+ },
260
+ {
261
+ value: false,
262
+ label: "No",
263
+ },
264
+ ]
265
+ );
414
266
  }
415
267
 
416
- async function askDatabase(
417
- prompt
418
- ) {
268
+ function askDatabase() {
419
269
  const labels = {
420
270
  none: "None",
421
271
  mysql: "MySQL",
@@ -424,142 +274,233 @@ async function askDatabase(
424
274
  mongodb: "MongoDB",
425
275
  };
426
276
 
427
- console.log(
428
- "Select a database:"
429
- );
430
-
431
- DATABASE_CHOICES.forEach(
432
- (
433
- value,
434
- index
435
- ) => {
436
- console.log(
437
- ` ${index + 1}) ${labels[value]}`
438
- );
439
- }
277
+ return askTerminalSelect(
278
+ "Select a database:",
279
+ DATABASE_CHOICES.map(
280
+ (value) => ({
281
+ value,
282
+ label: labels[value],
283
+ })
284
+ )
440
285
  );
441
-
442
- while (true) {
443
- const answer =
444
- (
445
- await prompt.question(
446
- "Database (1): "
447
- )
448
- ).trim();
449
-
450
- if (!answer) {
451
- return "none";
452
- }
453
-
454
- const index =
455
- Number(
456
- answer
457
- ) - 1;
458
-
459
- if (
460
- Number.isInteger(
461
- index
462
- ) &&
463
- index >= 0 &&
464
- index <
465
- DATABASE_CHOICES.length
466
- ) {
467
- return DATABASE_CHOICES[
468
- index
469
- ];
470
- }
471
-
472
- const normalized =
473
- answer.toLowerCase();
474
-
475
- if (
476
- DATABASE_CHOICES.includes(
477
- normalized
478
- )
479
- ) {
480
- return normalized;
481
- }
482
-
483
- console.log(
484
- "Choose a number from the list or enter a database name."
485
- );
486
- }
487
286
  }
488
287
 
489
- async function askAuth(
490
- prompt
491
- ) {
288
+ function askAuth() {
492
289
  const labels = {
493
290
  none: "None",
494
- "jwt-cookie":
495
- "JWT Cookie",
291
+ "jwt-cookie": "JWT Cookie",
496
292
  };
497
293
 
498
- console.log(
499
- "Select authentication:"
294
+ return askTerminalSelect(
295
+ "Select authentication:",
296
+ AUTH_CHOICES.map(
297
+ (value) => ({
298
+ value,
299
+ label: labels[value],
300
+ })
301
+ )
500
302
  );
303
+ }
304
+
305
+ function askTerminalSelect(
306
+ label,
307
+ choices
308
+ ) {
309
+ if (
310
+ !Array.isArray(choices) ||
311
+ choices.length === 0
312
+ ) {
313
+ return Promise.reject(
314
+ new Error(
315
+ "Interactive selection requires at least one choice."
316
+ )
317
+ );
318
+ }
319
+
320
+ if (
321
+ typeof input.setRawMode !== "function"
322
+ ) {
323
+ return Promise.resolve(
324
+ choices[0].value
325
+ );
326
+ }
501
327
 
502
- AUTH_CHOICES.forEach(
328
+ return new Promise(
503
329
  (
504
- value,
505
- index
330
+ resolve,
331
+ reject
506
332
  ) => {
333
+ let selectedIndex = 0;
334
+ const previousRawMode =
335
+ input.isRaw === true;
336
+
337
+ const render = (
338
+ redraw = false
339
+ ) => {
340
+ if (redraw) {
341
+ output.write(
342
+ `\u001B[${choices.length}A`
343
+ );
344
+ }
345
+
346
+ for (
347
+ let index = 0;
348
+ index < choices.length;
349
+ index++
350
+ ) {
351
+ const choice = choices[index];
352
+ const selected =
353
+ index === selectedIndex;
354
+
355
+ output.write(
356
+ "\u001B[2K\r"
357
+ );
358
+
359
+ if (selected) {
360
+ output.write(
361
+ `${ANSI.selected} > ${choice.label} ${ANSI.reset}\n`
362
+ );
363
+ } else {
364
+ output.write(
365
+ ` ${choice.label}\n`
366
+ );
367
+ }
368
+ }
369
+ };
370
+
371
+ const cleanup = () => {
372
+ input.removeListener(
373
+ "keypress",
374
+ onKeypress
375
+ );
376
+ input.setRawMode(
377
+ previousRawMode
378
+ );
379
+
380
+ if (!previousRawMode) {
381
+ input.pause();
382
+ }
383
+ };
384
+
385
+ const onKeypress = (
386
+ _character,
387
+ key = {}
388
+ ) => {
389
+ if (
390
+ key.ctrl &&
391
+ key.name === "c"
392
+ ) {
393
+ cleanup();
394
+ output.write("\n");
395
+ reject(
396
+ new Error(
397
+ "Setup cancelled."
398
+ )
399
+ );
400
+ return;
401
+ }
402
+
403
+ if (
404
+ key.name === "escape"
405
+ ) {
406
+ cleanup();
407
+ output.write("\n");
408
+ reject(
409
+ new Error(
410
+ "Setup cancelled."
411
+ )
412
+ );
413
+ return;
414
+ }
415
+
416
+ if (key.name === "up") {
417
+ selectedIndex = (
418
+ selectedIndex - 1 +
419
+ choices.length
420
+ ) % choices.length;
421
+ render(true);
422
+ return;
423
+ }
424
+
425
+ if (key.name === "down") {
426
+ selectedIndex = (
427
+ selectedIndex + 1
428
+ ) % choices.length;
429
+ render(true);
430
+ return;
431
+ }
432
+
433
+ if (
434
+ key.name === "return" ||
435
+ key.name === "enter"
436
+ ) {
437
+ const selected =
438
+ choices[selectedIndex];
439
+
440
+ cleanup();
441
+ output.write("\n");
442
+ resolve(selected.value);
443
+ }
444
+ };
445
+
446
+ console.log(label);
507
447
  console.log(
508
- ` ${index + 1}) ${labels[value]}`
448
+ `${ANSI.muted}Use ↑/↓ to move, Enter to select.${ANSI.reset}`
509
449
  );
510
- }
511
- );
512
450
 
513
- while (true) {
514
- const answer =
515
- (
516
- await prompt.question(
517
- "Authentication (1): "
518
- )
519
- ).trim();
451
+ emitKeypressEvents(input);
452
+ input.on(
453
+ "keypress",
454
+ onKeypress
455
+ );
456
+ input.setRawMode(true);
457
+ input.resume();
520
458
 
521
- if (!answer) {
522
- return "none";
523
- }
524
-
525
- const index =
526
- Number(
527
- answer
528
- ) - 1;
529
-
530
- if (
531
- Number.isInteger(
532
- index
533
- ) &&
534
- index >= 0 &&
535
- index <
536
- AUTH_CHOICES.length
537
- ) {
538
- return AUTH_CHOICES[
539
- index
540
- ];
459
+ render();
541
460
  }
461
+ );
462
+ }
542
463
 
543
- const normalized =
544
- answer.toLowerCase();
464
+ function printSuccess(
465
+ result,
466
+ installed
467
+ ) {
468
+ const relative = path.relative(
469
+ process.cwd(),
470
+ result.targetDirectory
471
+ ) || ".";
545
472
 
546
- if (
547
- AUTH_CHOICES.includes(
548
- normalized
549
- )
550
- ) {
551
- return normalized;
552
- }
473
+ console.log("");
474
+ console.log(
475
+ `Created BCP app: ${result.projectName}`
476
+ );
477
+ console.log(
478
+ `Location: ${result.targetDirectory}`
479
+ );
480
+ console.log(
481
+ `Tailwind CSS: ${result.tailwind ? "Yes" : "No"}`
482
+ );
483
+ console.log(
484
+ `Database: ${formatDatabaseName(result.database)}`
485
+ );
486
+ console.log(
487
+ `Authentication: ${formatAuthName(result.auth)}`
488
+ );
489
+ console.log("");
490
+ console.log("Next steps:");
491
+ console.log(
492
+ ` cd ${quoteIfNeeded(relative)}`
493
+ );
553
494
 
554
- console.log(
555
- "Choose a number from the list or enter an authentication preset name."
556
- );
495
+ if (!installed) {
496
+ console.log(" npm install");
557
497
  }
498
+
499
+ console.log(" npm run dev");
500
+ console.log("");
558
501
  }
559
502
 
560
- function formatDatabaseName(
561
- value
562
- ) {
503
+ function formatDatabaseName(value) {
563
504
  const names = {
564
505
  none: "None",
565
506
  mysql: "MySQL",
@@ -568,21 +509,16 @@ function formatDatabaseName(
568
509
  mongodb: "MongoDB",
569
510
  };
570
511
 
571
- return names[value] ??
572
- value;
512
+ return names[value] ?? value;
573
513
  }
574
514
 
575
- function formatAuthName(
576
- value
577
- ) {
515
+ function formatAuthName(value) {
578
516
  const names = {
579
517
  none: "None",
580
- "jwt-cookie":
581
- "JWT Cookie",
518
+ "jwt-cookie": "JWT Cookie",
582
519
  };
583
520
 
584
- return names[value] ??
585
- value;
521
+ return names[value] ?? value;
586
522
  }
587
523
 
588
524
  function printHelp() {
@@ -593,7 +529,8 @@ Usage:
593
529
  npx create-bcp-app <project-name> [options]
594
530
 
595
531
  Interactive setup:
596
- - Choose whether to use Tailwind CSS
532
+ - Use ↑/↓ and Enter for every setup choice
533
+ - Choose Tailwind CSS: Yes or No
597
534
  - Choose a database: None, MySQL, PostgreSQL, SQLite or MongoDB
598
535
  - Choose authentication: None or JWT Cookie
599
536
 
@@ -613,27 +550,23 @@ Examples:
613
550
  npx create-bcp-app my-app --no-tailwind --database mongodb
614
551
  npx create-bcp-app my-app --yes
615
552
  npx create-bcp-app my-app --no-install
616
- npx create-bcp-app my-app --bcp file:../bcp-0.1.8.tgz
617
553
  `);
618
554
  }
619
555
 
620
- function quoteIfNeeded(
621
- value
622
- ) {
623
- return /\s/.test(
624
- value
625
- )
626
- ? JSON.stringify(
627
- value
628
- )
556
+ function quoteIfNeeded(value) {
557
+ return /\s/.test(value)
558
+ ? JSON.stringify(value)
629
559
  : value;
630
560
  }
631
561
 
632
- function fail(
633
- message
634
- ) {
562
+ function reportError(error) {
563
+ const message =
564
+ error instanceof Error
565
+ ? error.message
566
+ : String(error);
567
+
635
568
  console.error(
636
569
  `create-bcp-app: ${message}`
637
570
  );
638
- process.exit(1);
571
+ process.exitCode = 1;
639
572
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bcp-app",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Create a new BCP Framework application.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.d.mts CHANGED
@@ -28,6 +28,20 @@ export interface CreateBcpAppResult {
28
28
  auth: BcpAuthPreset;
29
29
  }
30
30
 
31
+ export interface ResolveNpmInvocationOptions {
32
+ env?: Record<
33
+ string,
34
+ string | undefined
35
+ >;
36
+ platform?: string;
37
+ execPath?: string;
38
+ }
39
+
40
+ export interface NpmInvocation {
41
+ command: string;
42
+ args: string[];
43
+ }
44
+
31
45
  export function createBcpApp(
32
46
  options: CreateBcpAppOptions
33
47
  ): Promise<CreateBcpAppResult>;
@@ -35,3 +49,8 @@ export function createBcpApp(
35
49
  export function validateProjectName(
36
50
  value: unknown
37
51
  ): string;
52
+
53
+ export function resolveNpmInvocation(
54
+ args: string[],
55
+ options?: ResolveNpmInvocationOptions
56
+ ): NpmInvocation;
package/src/index.mjs CHANGED
@@ -250,12 +250,9 @@ function copyTemplate(
250
250
  sourceDirectory,
251
251
  targetDirectory
252
252
  ) {
253
- fs.cpSync(
253
+ copyDirectoryContents(
254
254
  sourceDirectory,
255
- targetDirectory,
256
- {
257
- recursive: true,
258
- }
255
+ targetDirectory
259
256
  );
260
257
 
261
258
  const gitignoreTemplate =
@@ -279,6 +276,66 @@ function copyTemplate(
279
276
  }
280
277
  }
281
278
 
279
+ function copyDirectoryContents(
280
+ sourceDirectory,
281
+ targetDirectory
282
+ ) {
283
+ fs.mkdirSync(
284
+ targetDirectory,
285
+ {
286
+ recursive: true,
287
+ }
288
+ );
289
+
290
+ const entries =
291
+ fs.readdirSync(
292
+ sourceDirectory,
293
+ {
294
+ withFileTypes: true,
295
+ }
296
+ );
297
+
298
+ for (
299
+ const entry
300
+ of entries
301
+ ) {
302
+ const sourcePath =
303
+ path.join(
304
+ sourceDirectory,
305
+ entry.name
306
+ );
307
+ const targetPath =
308
+ path.join(
309
+ targetDirectory,
310
+ entry.name
311
+ );
312
+
313
+ if (
314
+ entry.isDirectory()
315
+ ) {
316
+ copyDirectoryContents(
317
+ sourcePath,
318
+ targetPath
319
+ );
320
+ continue;
321
+ }
322
+
323
+ if (
324
+ entry.isFile()
325
+ ) {
326
+ fs.copyFileSync(
327
+ sourcePath,
328
+ targetPath
329
+ );
330
+ continue;
331
+ }
332
+
333
+ throw new Error(
334
+ `Unsupported template entry: ${sourcePath}`
335
+ );
336
+ }
337
+ }
338
+
282
339
  function readPackageMetadata() {
283
340
  const packageJson =
284
341
  JSON.parse(
@@ -359,8 +416,11 @@ function runInstall(
359
416
  {
360
417
  cwd:
361
418
  targetDirectory,
362
- stdio:
419
+ stdio: [
420
+ "ignore",
421
+ "inherit",
363
422
  "inherit",
423
+ ],
364
424
  env:
365
425
  process.env,
366
426
  }
@@ -371,7 +431,7 @@ function runInstall(
371
431
  reject
372
432
  );
373
433
  child.once(
374
- "exit",
434
+ "close",
375
435
  (
376
436
  code,
377
437
  signal
@@ -396,16 +456,28 @@ function runInstall(
396
456
  );
397
457
  }
398
458
 
399
- function resolveNpmInvocation(
400
- args
459
+ export function resolveNpmInvocation(
460
+ args,
461
+ options = {}
401
462
  ) {
463
+ const environment =
464
+ options.env ??
465
+ process.env;
466
+ const platform =
467
+ options.platform ??
468
+ process.platform;
469
+ const execPath =
470
+ options.execPath ??
471
+ process.execPath;
402
472
  const npmExecPath =
403
- process.env.npm_execpath;
473
+ resolveNpmCliPath(
474
+ environment.npm_execpath
475
+ );
404
476
 
405
477
  if (npmExecPath) {
406
478
  return {
407
479
  command:
408
- process.execPath,
480
+ execPath,
409
481
  args: [
410
482
  npmExecPath,
411
483
  ...args,
@@ -414,11 +486,11 @@ function resolveNpmInvocation(
414
486
  }
415
487
 
416
488
  if (
417
- process.platform === "win32"
489
+ platform === "win32"
418
490
  ) {
419
491
  return {
420
492
  command:
421
- process.env.ComSpec ??
493
+ environment.ComSpec ??
422
494
  "cmd.exe",
423
495
  args: [
424
496
  "/d",
@@ -436,3 +508,51 @@ function resolveNpmInvocation(
436
508
  args,
437
509
  };
438
510
  }
511
+
512
+ function resolveNpmCliPath(
513
+ npmExecPath
514
+ ) {
515
+ if (
516
+ typeof npmExecPath !==
517
+ "string" ||
518
+ npmExecPath.trim() ===
519
+ ""
520
+ ) {
521
+ return null;
522
+ }
523
+
524
+ const normalized =
525
+ npmExecPath.trim();
526
+ const baseName =
527
+ path.basename(
528
+ normalized
529
+ ).toLowerCase();
530
+
531
+ if (
532
+ baseName ===
533
+ "npm-cli.js"
534
+ ) {
535
+ return normalized;
536
+ }
537
+
538
+ if (
539
+ baseName ===
540
+ "npx-cli.js"
541
+ ) {
542
+ const npmCliPath =
543
+ path.join(
544
+ path.dirname(
545
+ normalized
546
+ ),
547
+ "npm-cli.js"
548
+ );
549
+
550
+ return fs.existsSync(
551
+ npmCliPath
552
+ )
553
+ ? npmCliPath
554
+ : null;
555
+ }
556
+
557
+ return null;
558
+ }