@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.
@@ -0,0 +1,629 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ getProductionHardeningConfig,
6
+ } from "../../server/src/production-hardening.js";
7
+
8
+ export interface ProjectPresetMetadata {
9
+ tailwind: boolean | null;
10
+ database: string | null;
11
+ auth: string | null;
12
+ storage: string | null;
13
+ }
14
+
15
+ export interface ProjectDiagnostics {
16
+ packageManager:
17
+ | "npm"
18
+ | "pnpm"
19
+ | "yarn"
20
+ | "bun"
21
+ | null;
22
+ lockfile: string | null;
23
+ metadataFile: string | null;
24
+ presets: ProjectPresetMetadata | null;
25
+ productionBuild: boolean;
26
+ dockerfile: string | null;
27
+ frameworkDeclarations: Array<{
28
+ name:
29
+ "bcp" |
30
+ "@chidchanun/bcp";
31
+ range: string;
32
+ section:
33
+ "dependencies" |
34
+ "devDependencies";
35
+ }>;
36
+ storage: {
37
+ provider:
38
+ | "none"
39
+ | "local"
40
+ | "amazon-s3"
41
+ | "cloudflare-r2"
42
+ | "s3-compatible"
43
+ | "custom";
44
+ file: string | null;
45
+ };
46
+ hardening: {
47
+ valid: boolean;
48
+ message: string;
49
+ };
50
+ }
51
+
52
+ interface ProjectPackageJson {
53
+ packageManager?: unknown;
54
+ dependencies?: Record<string, string>;
55
+ devDependencies?: Record<string, string>;
56
+ }
57
+
58
+ interface BcpProjectManifest {
59
+ schemaVersion?: unknown;
60
+ packageManager?: unknown;
61
+ presets?: {
62
+ tailwind?: unknown;
63
+ database?: unknown;
64
+ auth?: unknown;
65
+ storage?: unknown;
66
+ };
67
+ }
68
+
69
+ export function collectProjectDiagnostics(
70
+ rootDirectory: string
71
+ ): ProjectDiagnostics {
72
+ const root =
73
+ path.resolve(
74
+ rootDirectory
75
+ );
76
+ const lockfile =
77
+ detectLockfile(
78
+ root
79
+ );
80
+ const packageFile =
81
+ path.join(
82
+ root,
83
+ "package.json"
84
+ );
85
+ const projectPackage =
86
+ readProjectPackage(
87
+ packageFile
88
+ );
89
+ const projectMetadata =
90
+ readProjectMetadata(
91
+ root
92
+ );
93
+ const storage =
94
+ detectStorageProvider(
95
+ root,
96
+ projectMetadata.manifest
97
+ );
98
+
99
+ let hardening:
100
+ ProjectDiagnostics[
101
+ "hardening"
102
+ ];
103
+
104
+ try {
105
+ const config =
106
+ getProductionHardeningConfig();
107
+
108
+ hardening = {
109
+ valid:
110
+ true,
111
+ message:
112
+ `request=${config.requestTimeoutMs}ms headers=${config.headersTimeoutMs}ms keepAlive=${config.keepAliveTimeoutMs}ms shutdown=${config.shutdownTimeoutMs}ms trustProxy=${config.trustProxy ? "on" : "off"}`,
113
+ };
114
+ } catch (error) {
115
+ hardening = {
116
+ valid:
117
+ false,
118
+ message:
119
+ error instanceof Error
120
+ ? error.message
121
+ : String(
122
+ error
123
+ ),
124
+ };
125
+ }
126
+
127
+ return {
128
+ packageManager:
129
+ detectPackageManager(
130
+ projectPackage,
131
+ projectMetadata.manifest,
132
+ lockfile
133
+ ),
134
+ lockfile,
135
+ metadataFile:
136
+ projectMetadata.file,
137
+ presets:
138
+ normalizePresetMetadata(
139
+ projectMetadata.manifest
140
+ ),
141
+ productionBuild:
142
+ fs.existsSync(
143
+ path.join(
144
+ root,
145
+ ".bcp-framework",
146
+ "build",
147
+ "server",
148
+ "server.mjs"
149
+ )
150
+ ),
151
+ dockerfile:
152
+ detectDockerfile(
153
+ root
154
+ ),
155
+ frameworkDeclarations:
156
+ collectFrameworkDeclarations(
157
+ projectPackage
158
+ ),
159
+ storage,
160
+ hardening,
161
+ };
162
+ }
163
+
164
+ function detectLockfile(
165
+ rootDirectory: string
166
+ ): string | null {
167
+ for (
168
+ const file
169
+ of [
170
+ "package-lock.json",
171
+ "pnpm-lock.yaml",
172
+ "yarn.lock",
173
+ "bun.lock",
174
+ "bun.lockb",
175
+ ]
176
+ ) {
177
+ if (
178
+ fs.existsSync(
179
+ path.join(
180
+ rootDirectory,
181
+ file
182
+ )
183
+ )
184
+ ) {
185
+ return file;
186
+ }
187
+ }
188
+
189
+ return null;
190
+ }
191
+
192
+ function detectPackageManager(
193
+ projectPackage:
194
+ ProjectPackageJson | null,
195
+ projectMetadata:
196
+ BcpProjectManifest | null,
197
+ lockfile: string | null
198
+ ): ProjectDiagnostics[
199
+ "packageManager"
200
+ ] {
201
+ const metadataManager =
202
+ normalizePackageManager(
203
+ projectMetadata?.packageManager
204
+ );
205
+
206
+ if (metadataManager) {
207
+ return metadataManager;
208
+ }
209
+
210
+ if (
211
+ typeof projectPackage
212
+ ?.packageManager ===
213
+ "string"
214
+ ) {
215
+ const name =
216
+ projectPackage
217
+ .packageManager
218
+ .split("@", 1)[0];
219
+ const normalized =
220
+ normalizePackageManager(
221
+ name
222
+ );
223
+
224
+ if (normalized) {
225
+ return normalized;
226
+ }
227
+ }
228
+
229
+ if (
230
+ lockfile ===
231
+ "package-lock.json"
232
+ ) {
233
+ return "npm";
234
+ }
235
+
236
+ if (
237
+ lockfile ===
238
+ "pnpm-lock.yaml"
239
+ ) {
240
+ return "pnpm";
241
+ }
242
+
243
+ if (
244
+ lockfile ===
245
+ "yarn.lock"
246
+ ) {
247
+ return "yarn";
248
+ }
249
+
250
+ if (
251
+ lockfile === "bun.lock" ||
252
+ lockfile === "bun.lockb"
253
+ ) {
254
+ return "bun";
255
+ }
256
+
257
+ return null;
258
+ }
259
+
260
+ function normalizePackageManager(
261
+ value: unknown
262
+ ): ProjectDiagnostics[
263
+ "packageManager"
264
+ ] {
265
+ if (
266
+ typeof value !==
267
+ "string"
268
+ ) {
269
+ return null;
270
+ }
271
+
272
+ const name =
273
+ value
274
+ .trim()
275
+ .split("@", 1)[0];
276
+
277
+ return (
278
+ name === "npm" ||
279
+ name === "pnpm" ||
280
+ name === "yarn" ||
281
+ name === "bun"
282
+ )
283
+ ? name
284
+ : null;
285
+ }
286
+
287
+ function detectDockerfile(
288
+ rootDirectory: string
289
+ ): string | null {
290
+ for (
291
+ const file
292
+ of [
293
+ "Dockerfile",
294
+ "dockerfile",
295
+ ]
296
+ ) {
297
+ if (
298
+ fs.existsSync(
299
+ path.join(
300
+ rootDirectory,
301
+ file
302
+ )
303
+ )
304
+ ) {
305
+ return file;
306
+ }
307
+ }
308
+
309
+ return null;
310
+ }
311
+
312
+ function collectFrameworkDeclarations(
313
+ projectPackage:
314
+ ProjectPackageJson | null
315
+ ): ProjectDiagnostics[
316
+ "frameworkDeclarations"
317
+ ] {
318
+ if (!projectPackage) {
319
+ return [];
320
+ }
321
+
322
+ const result:
323
+ ProjectDiagnostics[
324
+ "frameworkDeclarations"
325
+ ] = [];
326
+
327
+ for (
328
+ const [
329
+ section,
330
+ dependencies,
331
+ ]
332
+ of [
333
+ [
334
+ "dependencies",
335
+ projectPackage.dependencies,
336
+ ],
337
+ [
338
+ "devDependencies",
339
+ projectPackage.devDependencies,
340
+ ],
341
+ ] as const
342
+ ) {
343
+ if (!dependencies) {
344
+ continue;
345
+ }
346
+
347
+ for (
348
+ const name
349
+ of [
350
+ "bcp",
351
+ "@chidchanun/bcp",
352
+ ] as const
353
+ ) {
354
+ const range =
355
+ dependencies[name];
356
+
357
+ if (range) {
358
+ result.push({
359
+ name,
360
+ range,
361
+ section,
362
+ });
363
+ }
364
+ }
365
+ }
366
+
367
+ return result;
368
+ }
369
+
370
+ function detectStorageProvider(
371
+ rootDirectory: string,
372
+ projectMetadata:
373
+ BcpProjectManifest | null
374
+ ): ProjectDiagnostics[
375
+ "storage"
376
+ ] {
377
+ const metadataStorage =
378
+ projectMetadata
379
+ ?.presets
380
+ ?.storage;
381
+
382
+ if (
383
+ metadataStorage === "none"
384
+ ) {
385
+ return {
386
+ provider:
387
+ "none",
388
+ file:
389
+ null,
390
+ };
391
+ }
392
+
393
+ if (
394
+ metadataStorage === "local" ||
395
+ metadataStorage === "amazon-s3" ||
396
+ metadataStorage === "cloudflare-r2"
397
+ ) {
398
+ return {
399
+ provider:
400
+ metadataStorage,
401
+ file:
402
+ "lib/storage.ts",
403
+ };
404
+ }
405
+
406
+ const relativeFile =
407
+ path.join(
408
+ "lib",
409
+ "storage.ts"
410
+ );
411
+ const file =
412
+ path.join(
413
+ rootDirectory,
414
+ relativeFile
415
+ );
416
+
417
+ if (
418
+ !fs.existsSync(
419
+ file
420
+ )
421
+ ) {
422
+ return {
423
+ provider:
424
+ "none",
425
+ file:
426
+ null,
427
+ };
428
+ }
429
+
430
+ const source =
431
+ fs.readFileSync(
432
+ file,
433
+ "utf8"
434
+ );
435
+
436
+ if (
437
+ /createLocalStorage\s*\(/.test(
438
+ source
439
+ )
440
+ ) {
441
+ return {
442
+ provider:
443
+ "local",
444
+ file:
445
+ "lib/storage.ts",
446
+ };
447
+ }
448
+
449
+ if (
450
+ /r2\.cloudflarestorage\.com/.test(
451
+ source
452
+ )
453
+ ) {
454
+ return {
455
+ provider:
456
+ "cloudflare-r2",
457
+ file:
458
+ "lib/storage.ts",
459
+ };
460
+ }
461
+
462
+ if (
463
+ /createS3Storage\s*\(/.test(
464
+ source
465
+ )
466
+ ) {
467
+ if (
468
+ /AWS_S3_BUCKET|AWS_REGION/.test(
469
+ source
470
+ )
471
+ ) {
472
+ return {
473
+ provider:
474
+ "amazon-s3",
475
+ file:
476
+ "lib/storage.ts",
477
+ };
478
+ }
479
+
480
+ return {
481
+ provider:
482
+ "s3-compatible",
483
+ file:
484
+ "lib/storage.ts",
485
+ };
486
+ }
487
+
488
+ return {
489
+ provider:
490
+ "custom",
491
+ file:
492
+ "lib/storage.ts",
493
+ };
494
+ }
495
+
496
+ function readProjectMetadata(
497
+ rootDirectory: string
498
+ ): {
499
+ file: string | null;
500
+ manifest: BcpProjectManifest | null;
501
+ } {
502
+ const fileName =
503
+ "bcp.project.json";
504
+ const file =
505
+ path.join(
506
+ rootDirectory,
507
+ fileName
508
+ );
509
+
510
+ if (
511
+ !fs.existsSync(
512
+ file
513
+ )
514
+ ) {
515
+ return {
516
+ file:
517
+ null,
518
+ manifest:
519
+ null,
520
+ };
521
+ }
522
+
523
+ try {
524
+ const value =
525
+ JSON.parse(
526
+ fs.readFileSync(
527
+ file,
528
+ "utf8"
529
+ )
530
+ ) as BcpProjectManifest;
531
+
532
+ return {
533
+ file:
534
+ fileName,
535
+ manifest:
536
+ value &&
537
+ typeof value === "object"
538
+ ? value
539
+ : null,
540
+ };
541
+ } catch {
542
+ return {
543
+ file:
544
+ fileName,
545
+ manifest:
546
+ null,
547
+ };
548
+ }
549
+ }
550
+
551
+ function normalizePresetMetadata(
552
+ manifest:
553
+ BcpProjectManifest | null
554
+ ): ProjectPresetMetadata | null {
555
+ if (
556
+ manifest?.schemaVersion !== 1 ||
557
+ !manifest.presets ||
558
+ typeof manifest.presets !==
559
+ "object"
560
+ ) {
561
+ return null;
562
+ }
563
+
564
+ return {
565
+ tailwind:
566
+ typeof manifest.presets
567
+ .tailwind ===
568
+ "boolean"
569
+ ? manifest.presets
570
+ .tailwind
571
+ : null,
572
+ database:
573
+ typeof manifest.presets
574
+ .database ===
575
+ "string"
576
+ ? manifest.presets
577
+ .database
578
+ : null,
579
+ auth:
580
+ typeof manifest.presets
581
+ .auth ===
582
+ "string"
583
+ ? manifest.presets
584
+ .auth
585
+ : null,
586
+ storage:
587
+ typeof manifest.presets
588
+ .storage ===
589
+ "string"
590
+ ? manifest.presets
591
+ .storage
592
+ : null,
593
+ };
594
+ }
595
+
596
+ function readProjectPackage(
597
+ packageFile: string
598
+ ): ProjectPackageJson | null {
599
+ if (
600
+ !fs.existsSync(
601
+ packageFile
602
+ )
603
+ ) {
604
+ return null;
605
+ }
606
+
607
+ try {
608
+ const value =
609
+ JSON.parse(
610
+ fs.readFileSync(
611
+ packageFile,
612
+ "utf8"
613
+ )
614
+ );
615
+
616
+ return (
617
+ value &&
618
+ typeof value === "object" &&
619
+ !Array.isArray(
620
+ value
621
+ )
622
+ )
623
+ ? value as
624
+ ProjectPackageJson
625
+ : null;
626
+ } catch {
627
+ return null;
628
+ }
629
+ }