@chidchanun/bcp 0.2.3 → 0.2.5

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,1162 @@
1
+ import {
2
+ createHash,
3
+ } from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ export const APPLICATION_PACKAGE_SCHEMA_VERSION =
8
+ 1;
9
+ export const APPLICATION_PACKAGE_TARGET =
10
+ "standalone-node";
11
+ export const APPLICATION_PACKAGE_NODE_RANGE =
12
+ ">=24.11.0";
13
+
14
+ interface JsonObject {
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ interface ApplicationPackageJson
19
+ extends JsonObject {
20
+ name?: string;
21
+ version?: string;
22
+ type?: string;
23
+ private?: boolean;
24
+ scripts?: Record<string, string>;
25
+ engines?: Record<string, string>;
26
+ dependencies?: Record<string, string>;
27
+ optionalDependencies?: Record<string, string>;
28
+ peerDependencies?: Record<string, string>;
29
+ peerDependenciesMeta?: JsonObject;
30
+ overrides?: JsonObject;
31
+ packageManager?: string;
32
+ }
33
+
34
+ interface PackageLockPackage
35
+ extends JsonObject {
36
+ name?: string;
37
+ version?: string;
38
+ link?: boolean;
39
+ dev?: boolean;
40
+ dependencies?: Record<string, string>;
41
+ optionalDependencies?: Record<string, string>;
42
+ peerDependencies?: Record<string, string>;
43
+ }
44
+
45
+ interface PackageLockJson
46
+ extends JsonObject {
47
+ name?: string;
48
+ version?: string;
49
+ lockfileVersion?: number;
50
+ packages?: Record<
51
+ string,
52
+ PackageLockPackage
53
+ >;
54
+ }
55
+
56
+ export interface ApplicationPackageFile {
57
+ path: string;
58
+ bytes: number;
59
+ sha256: string;
60
+ }
61
+
62
+ export interface ApplicationPackageManifest {
63
+ schemaVersion: 1;
64
+ target: "standalone-node";
65
+ frameworkVersion: string;
66
+ application: {
67
+ name: string;
68
+ version: string;
69
+ };
70
+ runtime: {
71
+ node: string;
72
+ entrypoint: string;
73
+ startCommand: string;
74
+ };
75
+ install: {
76
+ command: string;
77
+ lockfile: boolean;
78
+ devDependenciesIncluded: false;
79
+ };
80
+ directories: {
81
+ client: string;
82
+ server: string;
83
+ public: string | null;
84
+ };
85
+ buildManifest: string;
86
+ environmentManifest: string;
87
+ deploymentManifest: string;
88
+ files: ApplicationPackageFile[];
89
+ }
90
+
91
+ export interface ApplicationPackagingResult {
92
+ outputDirectory: string;
93
+ packageManifest: ApplicationPackageManifest;
94
+ lockfileIncluded: boolean;
95
+ warnings: string[];
96
+ }
97
+
98
+ export interface PackageApplicationOptions {
99
+ rootDirectory: string;
100
+ frameworkVersion: string;
101
+ }
102
+
103
+ export function packageApplication(
104
+ options: PackageApplicationOptions
105
+ ): ApplicationPackagingResult {
106
+ const rootDirectory =
107
+ path.resolve(
108
+ options.rootDirectory
109
+ );
110
+ const buildDirectory =
111
+ path.join(
112
+ rootDirectory,
113
+ ".bcp-framework",
114
+ "build"
115
+ );
116
+ const serverFile =
117
+ path.join(
118
+ buildDirectory,
119
+ "server",
120
+ "server.mjs"
121
+ );
122
+ const buildManifestFile =
123
+ path.join(
124
+ buildDirectory,
125
+ "manifest.json"
126
+ );
127
+
128
+ if (
129
+ !fs.existsSync(serverFile) ||
130
+ !fs.existsSync(
131
+ buildManifestFile
132
+ )
133
+ ) {
134
+ throw new Error(
135
+ "BCP Framework: production build was not found. Run `bcp build` before packaging."
136
+ );
137
+ }
138
+
139
+ const sourcePackageFile =
140
+ path.join(
141
+ rootDirectory,
142
+ "package.json"
143
+ );
144
+
145
+ if (
146
+ !fs.existsSync(
147
+ sourcePackageFile
148
+ )
149
+ ) {
150
+ throw new Error(
151
+ "BCP Framework: package.json was not found in the project root."
152
+ );
153
+ }
154
+
155
+ const sourcePackage =
156
+ readJson<ApplicationPackageJson>(
157
+ sourcePackageFile
158
+ );
159
+ const productionPackage =
160
+ createProductionPackageJson(
161
+ sourcePackage
162
+ );
163
+ const outputDirectory =
164
+ path.join(
165
+ rootDirectory,
166
+ ".bcp-framework",
167
+ "package"
168
+ );
169
+
170
+ fs.rmSync(
171
+ outputDirectory,
172
+ {
173
+ recursive: true,
174
+ force: true,
175
+ }
176
+ );
177
+ fs.mkdirSync(
178
+ outputDirectory,
179
+ {
180
+ recursive: true,
181
+ }
182
+ );
183
+
184
+ copyBuildDirectory(
185
+ buildDirectory,
186
+ outputDirectory,
187
+ "client",
188
+ true
189
+ );
190
+ copyBuildDirectory(
191
+ buildDirectory,
192
+ outputDirectory,
193
+ "server",
194
+ true
195
+ );
196
+ copyBuildDirectory(
197
+ buildDirectory,
198
+ outputDirectory,
199
+ "public",
200
+ false
201
+ );
202
+
203
+ fs.copyFileSync(
204
+ buildManifestFile,
205
+ path.join(
206
+ outputDirectory,
207
+ "manifest.json"
208
+ )
209
+ );
210
+
211
+ writeJson(
212
+ path.join(
213
+ outputDirectory,
214
+ "package.json"
215
+ ),
216
+ productionPackage
217
+ );
218
+
219
+ const warnings:
220
+ string[] = [];
221
+ const lockfileIncluded =
222
+ writeProductionLockfile(
223
+ rootDirectory,
224
+ outputDirectory,
225
+ productionPackage,
226
+ warnings
227
+ );
228
+ const installCommand =
229
+ lockfileIncluded
230
+ ? "npm ci --omit=dev"
231
+ : "npm install --omit=dev";
232
+
233
+ const environmentManifest =
234
+ createEnvironmentManifest(
235
+ rootDirectory
236
+ );
237
+ writeJson(
238
+ path.join(
239
+ outputDirectory,
240
+ "bcp.env.json"
241
+ ),
242
+ environmentManifest
243
+ );
244
+
245
+ const deploymentManifest = {
246
+ schemaVersion:
247
+ APPLICATION_PACKAGE_SCHEMA_VERSION,
248
+ target:
249
+ APPLICATION_PACKAGE_TARGET,
250
+ runtime: {
251
+ name:
252
+ "node",
253
+ version:
254
+ APPLICATION_PACKAGE_NODE_RANGE,
255
+ },
256
+ install: {
257
+ command:
258
+ installCommand,
259
+ },
260
+ start: {
261
+ command:
262
+ "npm start",
263
+ entrypoint:
264
+ "server/server.mjs",
265
+ },
266
+ docker: {
267
+ file:
268
+ "Dockerfile",
269
+ },
270
+ };
271
+
272
+ writeJson(
273
+ path.join(
274
+ outputDirectory,
275
+ "bcp.deployment.json"
276
+ ),
277
+ deploymentManifest
278
+ );
279
+ fs.writeFileSync(
280
+ path.join(
281
+ outputDirectory,
282
+ "Dockerfile"
283
+ ),
284
+ createDockerfile(),
285
+ "utf8"
286
+ );
287
+ fs.writeFileSync(
288
+ path.join(
289
+ outputDirectory,
290
+ ".dockerignore"
291
+ ),
292
+ createDockerIgnore(),
293
+ "utf8"
294
+ );
295
+ fs.writeFileSync(
296
+ path.join(
297
+ outputDirectory,
298
+ "README.md"
299
+ ),
300
+ createPackageReadme(
301
+ installCommand
302
+ ),
303
+ "utf8"
304
+ );
305
+
306
+ const files =
307
+ collectPackageFiles(
308
+ outputDirectory,
309
+ new Set([
310
+ "bcp.package.json",
311
+ ])
312
+ );
313
+ const packageManifest:
314
+ ApplicationPackageManifest = {
315
+ schemaVersion:
316
+ APPLICATION_PACKAGE_SCHEMA_VERSION,
317
+ target:
318
+ APPLICATION_PACKAGE_TARGET,
319
+ frameworkVersion:
320
+ options.frameworkVersion,
321
+ application: {
322
+ name:
323
+ normalizeApplicationName(
324
+ productionPackage.name
325
+ ),
326
+ version:
327
+ normalizeApplicationVersion(
328
+ productionPackage.version
329
+ ),
330
+ },
331
+ runtime: {
332
+ node:
333
+ APPLICATION_PACKAGE_NODE_RANGE,
334
+ entrypoint:
335
+ "server/server.mjs",
336
+ startCommand:
337
+ "npm start",
338
+ },
339
+ install: {
340
+ command:
341
+ installCommand,
342
+ lockfile:
343
+ lockfileIncluded,
344
+ devDependenciesIncluded:
345
+ false,
346
+ },
347
+ directories: {
348
+ client:
349
+ "client",
350
+ server:
351
+ "server",
352
+ public:
353
+ fs.existsSync(
354
+ path.join(
355
+ outputDirectory,
356
+ "public"
357
+ )
358
+ )
359
+ ? "public"
360
+ : null,
361
+ },
362
+ buildManifest:
363
+ "manifest.json",
364
+ environmentManifest:
365
+ "bcp.env.json",
366
+ deploymentManifest:
367
+ "bcp.deployment.json",
368
+ files,
369
+ };
370
+
371
+ writeJson(
372
+ path.join(
373
+ outputDirectory,
374
+ "bcp.package.json"
375
+ ),
376
+ packageManifest
377
+ );
378
+
379
+ return {
380
+ outputDirectory,
381
+ packageManifest,
382
+ lockfileIncluded,
383
+ warnings,
384
+ };
385
+ }
386
+
387
+ export function createProductionPackageJson(
388
+ source: ApplicationPackageJson
389
+ ): ApplicationPackageJson {
390
+ const result:
391
+ ApplicationPackageJson = {
392
+ name:
393
+ normalizeApplicationName(
394
+ source.name
395
+ ),
396
+ version:
397
+ normalizeApplicationVersion(
398
+ source.version
399
+ ),
400
+ private: true,
401
+ type:
402
+ source.type === "commonjs"
403
+ ? "commonjs"
404
+ : "module",
405
+ scripts: {
406
+ start:
407
+ "node server/server.mjs",
408
+ },
409
+ engines: {
410
+ node:
411
+ APPLICATION_PACKAGE_NODE_RANGE,
412
+ },
413
+ };
414
+
415
+ copySortedStringMap(
416
+ source,
417
+ result,
418
+ "dependencies"
419
+ );
420
+ copySortedStringMap(
421
+ source,
422
+ result,
423
+ "optionalDependencies"
424
+ );
425
+ copySortedStringMap(
426
+ source,
427
+ result,
428
+ "peerDependencies"
429
+ );
430
+
431
+ if (
432
+ source.peerDependenciesMeta &&
433
+ isPlainObject(
434
+ source.peerDependenciesMeta
435
+ )
436
+ ) {
437
+ result.peerDependenciesMeta =
438
+ sortObject(
439
+ source.peerDependenciesMeta
440
+ );
441
+ }
442
+
443
+ if (
444
+ source.overrides &&
445
+ isPlainObject(
446
+ source.overrides
447
+ )
448
+ ) {
449
+ result.overrides =
450
+ sortObject(
451
+ source.overrides
452
+ );
453
+ }
454
+
455
+ if (
456
+ typeof source.packageManager ===
457
+ "string" &&
458
+ source.packageManager.trim() !==
459
+ ""
460
+ ) {
461
+ result.packageManager =
462
+ source.packageManager;
463
+ }
464
+
465
+ return result;
466
+ }
467
+
468
+ export function createProductionLockfile(
469
+ sourceLock:
470
+ PackageLockJson,
471
+ productionPackage:
472
+ ApplicationPackageJson
473
+ ): PackageLockJson | null {
474
+ if (
475
+ sourceLock.lockfileVersion !== 3 ||
476
+ !sourceLock.packages ||
477
+ !sourceLock.packages[""]
478
+ ) {
479
+ return null;
480
+ }
481
+
482
+ const sourcePackages =
483
+ sourceLock.packages;
484
+ const selected =
485
+ new Set<string>([
486
+ "",
487
+ ]);
488
+ const queue:
489
+ string[] = [
490
+ "",
491
+ ];
492
+
493
+ while (
494
+ queue.length > 0
495
+ ) {
496
+ const packagePath =
497
+ queue.shift() as string;
498
+ const packageEntry =
499
+ packagePath === ""
500
+ ? createRootLockPackage(
501
+ sourcePackages[""],
502
+ productionPackage
503
+ )
504
+ : sourcePackages[
505
+ packagePath
506
+ ];
507
+
508
+ if (!packageEntry) {
509
+ continue;
510
+ }
511
+
512
+ if (
513
+ packagePath !== "" &&
514
+ packageEntry.link === true
515
+ ) {
516
+ return null;
517
+ }
518
+
519
+ const dependencyNames =
520
+ collectDependencyNames(
521
+ packageEntry
522
+ );
523
+
524
+ for (
525
+ const dependencyName
526
+ of dependencyNames
527
+ ) {
528
+ const resolvedPath =
529
+ resolveLockPackagePath(
530
+ sourcePackages,
531
+ packagePath,
532
+ dependencyName
533
+ );
534
+
535
+ if (
536
+ !resolvedPath ||
537
+ selected.has(
538
+ resolvedPath
539
+ )
540
+ ) {
541
+ continue;
542
+ }
543
+
544
+ const resolvedEntry =
545
+ sourcePackages[
546
+ resolvedPath
547
+ ];
548
+
549
+ if (
550
+ !resolvedEntry ||
551
+ resolvedEntry.dev === true
552
+ ) {
553
+ continue;
554
+ }
555
+
556
+ selected.add(
557
+ resolvedPath
558
+ );
559
+ queue.push(
560
+ resolvedPath
561
+ );
562
+ }
563
+ }
564
+
565
+ const packages:
566
+ Record<
567
+ string,
568
+ PackageLockPackage
569
+ > = {};
570
+
571
+ for (
572
+ const packagePath
573
+ of Array.from(
574
+ selected
575
+ ).sort()
576
+ ) {
577
+ if (
578
+ packagePath === ""
579
+ ) {
580
+ packages[""] =
581
+ createRootLockPackage(
582
+ sourcePackages[""],
583
+ productionPackage
584
+ );
585
+ continue;
586
+ }
587
+
588
+ const entry =
589
+ sourcePackages[
590
+ packagePath
591
+ ];
592
+
593
+ if (entry) {
594
+ packages[
595
+ packagePath
596
+ ] =
597
+ entry;
598
+ }
599
+ }
600
+
601
+ return {
602
+ name:
603
+ normalizeApplicationName(
604
+ productionPackage.name
605
+ ),
606
+ version:
607
+ normalizeApplicationVersion(
608
+ productionPackage.version
609
+ ),
610
+ lockfileVersion: 3,
611
+ requires:
612
+ sourceLock.requires ??
613
+ true,
614
+ packages,
615
+ };
616
+ }
617
+
618
+ export function parseEnvironmentExampleKeys(
619
+ content: string
620
+ ): string[] {
621
+ const keys =
622
+ new Set<string>();
623
+
624
+ for (
625
+ const line
626
+ of content.split(/\r?\n/)
627
+ ) {
628
+ const trimmed =
629
+ line.trim();
630
+
631
+ if (
632
+ trimmed === "" ||
633
+ trimmed.startsWith("#")
634
+ ) {
635
+ continue;
636
+ }
637
+
638
+ const match =
639
+ /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(
640
+ trimmed
641
+ );
642
+
643
+ if (match) {
644
+ keys.add(
645
+ match[1]
646
+ );
647
+ }
648
+ }
649
+
650
+ return Array.from(
651
+ keys
652
+ ).sort();
653
+ }
654
+
655
+ function writeProductionLockfile(
656
+ rootDirectory: string,
657
+ outputDirectory: string,
658
+ productionPackage: ApplicationPackageJson,
659
+ warnings: string[]
660
+ ): boolean {
661
+ const sourceFile =
662
+ path.join(
663
+ rootDirectory,
664
+ "package-lock.json"
665
+ );
666
+
667
+ if (!fs.existsSync(sourceFile)) {
668
+ warnings.push(
669
+ "package-lock.json was not found; deployment will use npm install --omit=dev."
670
+ );
671
+ return false;
672
+ }
673
+
674
+ const sourceLock =
675
+ readJson<PackageLockJson>(
676
+ sourceFile
677
+ );
678
+ const productionLock =
679
+ createProductionLockfile(
680
+ sourceLock,
681
+ productionPackage
682
+ );
683
+
684
+ if (!productionLock) {
685
+ warnings.push(
686
+ "package-lock.json could not be safely pruned (npm lockfile v3 without workspace links is required); deployment will use npm install --omit=dev."
687
+ );
688
+ return false;
689
+ }
690
+
691
+ writeJson(
692
+ path.join(
693
+ outputDirectory,
694
+ "package-lock.json"
695
+ ),
696
+ productionLock
697
+ );
698
+
699
+ return true;
700
+ }
701
+
702
+ function createEnvironmentManifest(
703
+ rootDirectory: string
704
+ ): JsonObject {
705
+ const exampleFiles = [
706
+ ".env.example",
707
+ ".env.production.example",
708
+ ".env.production.local.example",
709
+ ];
710
+ const declared =
711
+ new Set<string>();
712
+ const discoveredFiles:
713
+ string[] = [];
714
+
715
+ for (
716
+ const fileName
717
+ of exampleFiles
718
+ ) {
719
+ const filePath =
720
+ path.join(
721
+ rootDirectory,
722
+ fileName
723
+ );
724
+
725
+ if (!fs.existsSync(filePath)) {
726
+ continue;
727
+ }
728
+
729
+ discoveredFiles.push(
730
+ fileName
731
+ );
732
+
733
+ const keys =
734
+ parseEnvironmentExampleKeys(
735
+ fs.readFileSync(
736
+ filePath,
737
+ "utf8"
738
+ )
739
+ );
740
+
741
+ for (const key of keys) {
742
+ declared.add(key);
743
+ }
744
+ }
745
+
746
+ return {
747
+ schemaVersion:
748
+ APPLICATION_PACKAGE_SCHEMA_VERSION,
749
+ valuesIncluded:
750
+ false,
751
+ environmentFilesIncluded:
752
+ false,
753
+ sourceExamples:
754
+ discoveredFiles.sort(),
755
+ declaredKeys:
756
+ Array.from(
757
+ declared
758
+ ).sort(),
759
+ runtimeOverrides: [
760
+ "BCP_HOSTNAME",
761
+ "BCP_PORT",
762
+ ],
763
+ note:
764
+ "Provide environment values at deployment/runtime. BCP application packages never copy .env files or secret values.",
765
+ };
766
+ }
767
+
768
+ function collectPackageFiles(
769
+ directory: string,
770
+ excluded: Set<string>
771
+ ): ApplicationPackageFile[] {
772
+ const files:
773
+ ApplicationPackageFile[] = [];
774
+
775
+ walkDirectory(
776
+ directory,
777
+ directory,
778
+ excluded,
779
+ files
780
+ );
781
+
782
+ return files.sort(
783
+ (left, right) =>
784
+ left.path.localeCompare(
785
+ right.path
786
+ )
787
+ );
788
+ }
789
+
790
+ function walkDirectory(
791
+ rootDirectory: string,
792
+ currentDirectory: string,
793
+ excluded: Set<string>,
794
+ files: ApplicationPackageFile[]
795
+ ): void {
796
+ const entries =
797
+ fs.readdirSync(
798
+ currentDirectory,
799
+ {
800
+ withFileTypes: true,
801
+ }
802
+ ).sort(
803
+ (left, right) =>
804
+ left.name.localeCompare(
805
+ right.name
806
+ )
807
+ );
808
+
809
+ for (const entry of entries) {
810
+ const absolutePath =
811
+ path.join(
812
+ currentDirectory,
813
+ entry.name
814
+ );
815
+ const relativePath =
816
+ normalizePath(
817
+ path.relative(
818
+ rootDirectory,
819
+ absolutePath
820
+ )
821
+ );
822
+
823
+ if (
824
+ excluded.has(
825
+ relativePath
826
+ )
827
+ ) {
828
+ continue;
829
+ }
830
+
831
+ if (entry.isDirectory()) {
832
+ walkDirectory(
833
+ rootDirectory,
834
+ absolutePath,
835
+ excluded,
836
+ files
837
+ );
838
+ continue;
839
+ }
840
+
841
+ if (!entry.isFile()) {
842
+ continue;
843
+ }
844
+
845
+ const content =
846
+ fs.readFileSync(
847
+ absolutePath
848
+ );
849
+
850
+ files.push({
851
+ path:
852
+ relativePath,
853
+ bytes:
854
+ content.length,
855
+ sha256:
856
+ createHash("sha256")
857
+ .update(content)
858
+ .digest("hex"),
859
+ });
860
+ }
861
+ }
862
+
863
+ function createRootLockPackage(
864
+ source: PackageLockPackage,
865
+ productionPackage: ApplicationPackageJson
866
+ ): PackageLockPackage {
867
+ const root:
868
+ PackageLockPackage = {
869
+ ...source,
870
+ name:
871
+ normalizeApplicationName(
872
+ productionPackage.name
873
+ ),
874
+ version:
875
+ normalizeApplicationVersion(
876
+ productionPackage.version
877
+ ),
878
+ };
879
+
880
+ delete root.devDependencies;
881
+ delete root.workspaces;
882
+
883
+ syncLockDependencyMap(
884
+ root,
885
+ productionPackage,
886
+ "dependencies"
887
+ );
888
+ syncLockDependencyMap(
889
+ root,
890
+ productionPackage,
891
+ "optionalDependencies"
892
+ );
893
+ syncLockDependencyMap(
894
+ root,
895
+ productionPackage,
896
+ "peerDependencies"
897
+ );
898
+
899
+ return root;
900
+ }
901
+
902
+ function syncLockDependencyMap(
903
+ target: PackageLockPackage,
904
+ source: ApplicationPackageJson,
905
+ key:
906
+ "dependencies" |
907
+ "optionalDependencies" |
908
+ "peerDependencies"
909
+ ): void {
910
+ const value =
911
+ source[key];
912
+
913
+ if (
914
+ value &&
915
+ Object.keys(value).length > 0
916
+ ) {
917
+ target[key] =
918
+ value;
919
+ } else {
920
+ delete target[key];
921
+ }
922
+ }
923
+
924
+ function collectDependencyNames(
925
+ entry: PackageLockPackage
926
+ ): string[] {
927
+ return Array.from(
928
+ new Set([
929
+ ...Object.keys(
930
+ entry.dependencies ?? {}
931
+ ),
932
+ ...Object.keys(
933
+ entry.optionalDependencies ?? {}
934
+ ),
935
+ ...Object.keys(
936
+ entry.peerDependencies ?? {}
937
+ ),
938
+ ])
939
+ ).sort();
940
+ }
941
+
942
+ function resolveLockPackagePath(
943
+ packages:
944
+ Record<
945
+ string,
946
+ PackageLockPackage
947
+ >,
948
+ requesterPath: string,
949
+ dependencyName: string
950
+ ): string | null {
951
+ const requesterSegments =
952
+ requesterPath === ""
953
+ ? []
954
+ : normalizePath(
955
+ requesterPath
956
+ ).split("/");
957
+ const candidates:
958
+ string[] = [];
959
+
960
+ for (
961
+ let length =
962
+ requesterSegments.length;
963
+ length >= 0;
964
+ length--
965
+ ) {
966
+ const prefix =
967
+ requesterSegments
968
+ .slice(
969
+ 0,
970
+ length
971
+ )
972
+ .join("/");
973
+ const candidate =
974
+ prefix === ""
975
+ ? `node_modules/${dependencyName}`
976
+ : `${prefix}/node_modules/${dependencyName}`;
977
+
978
+ candidates.push(
979
+ candidate
980
+ );
981
+ }
982
+
983
+ for (const candidate of candidates) {
984
+ if (
985
+ packages[
986
+ candidate
987
+ ]
988
+ ) {
989
+ return candidate;
990
+ }
991
+ }
992
+
993
+ return null;
994
+ }
995
+
996
+ function copyBuildDirectory(
997
+ buildDirectory: string,
998
+ outputDirectory: string,
999
+ name: string,
1000
+ required: boolean
1001
+ ): void {
1002
+ const source =
1003
+ path.join(
1004
+ buildDirectory,
1005
+ name
1006
+ );
1007
+
1008
+ if (!fs.existsSync(source)) {
1009
+ if (required) {
1010
+ throw new Error(
1011
+ `BCP Framework: production build directory "${name}" was not found.`
1012
+ );
1013
+ }
1014
+ return;
1015
+ }
1016
+
1017
+ fs.cpSync(
1018
+ source,
1019
+ path.join(
1020
+ outputDirectory,
1021
+ name
1022
+ ),
1023
+ {
1024
+ recursive: true,
1025
+ }
1026
+ );
1027
+ }
1028
+
1029
+ function copySortedStringMap(
1030
+ source: ApplicationPackageJson,
1031
+ target: ApplicationPackageJson,
1032
+ key:
1033
+ "dependencies" |
1034
+ "optionalDependencies" |
1035
+ "peerDependencies"
1036
+ ): void {
1037
+ const value =
1038
+ source[key];
1039
+
1040
+ if (
1041
+ value &&
1042
+ Object.keys(value).length > 0
1043
+ ) {
1044
+ target[key] =
1045
+ sortStringMap(
1046
+ value
1047
+ );
1048
+ }
1049
+ }
1050
+
1051
+ function sortStringMap(
1052
+ value: Record<string, string>
1053
+ ): Record<string, string> {
1054
+ return Object.fromEntries(
1055
+ Object.entries(value)
1056
+ .sort(
1057
+ ([left], [right]) =>
1058
+ left.localeCompare(
1059
+ right
1060
+ )
1061
+ )
1062
+ );
1063
+ }
1064
+
1065
+ function sortObject(
1066
+ value: JsonObject
1067
+ ): JsonObject {
1068
+ return Object.fromEntries(
1069
+ Object.entries(value)
1070
+ .sort(
1071
+ ([left], [right]) =>
1072
+ left.localeCompare(
1073
+ right
1074
+ )
1075
+ )
1076
+ );
1077
+ }
1078
+
1079
+ function createDockerfile(): string {
1080
+ return `FROM node:24-alpine\n\nWORKDIR /app\nENV NODE_ENV=production\n\nCOPY package*.json ./\nRUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi\n\nCOPY . .\n\nEXPOSE 3000\nCMD ["node", "server/server.mjs"]\n`;
1081
+ }
1082
+
1083
+ function createDockerIgnore(): string {
1084
+ return `node_modules\nnpm-debug.log*\n.env\n.env.*\n!.env.example\n.git\n.gitignore\n`;
1085
+ }
1086
+
1087
+ function createPackageReadme(
1088
+ installCommand: string
1089
+ ): string {
1090
+ return `# BCP Application Package\n\nTarget: \`${APPLICATION_PACKAGE_TARGET}\`\n\nInstall production dependencies:\n\n\`\`\`bash\n${installCommand}\n\`\`\`\n\nStart the application:\n\n\`\`\`bash\nnpm start\n\`\`\`\n\nRuntime configuration and secrets must be supplied by the deployment environment. This package intentionally excludes project \`.env\` files and devDependencies.\n\nFor containers:\n\n\`\`\`bash\ndocker build -t bcp-app .\ndocker run --rm -p 3000:3000 bcp-app\n\`\`\`\n`;
1091
+ }
1092
+
1093
+ function normalizeApplicationName(
1094
+ value: unknown
1095
+ ): string {
1096
+ if (
1097
+ typeof value === "string" &&
1098
+ value.trim() !== ""
1099
+ ) {
1100
+ return value.trim();
1101
+ }
1102
+
1103
+ return "bcp-application";
1104
+ }
1105
+
1106
+ function normalizeApplicationVersion(
1107
+ value: unknown
1108
+ ): string {
1109
+ if (
1110
+ typeof value === "string" &&
1111
+ value.trim() !== ""
1112
+ ) {
1113
+ return value.trim();
1114
+ }
1115
+
1116
+ return "0.0.0";
1117
+ }
1118
+
1119
+ function normalizePath(
1120
+ value: string
1121
+ ): string {
1122
+ return value.replace(
1123
+ /\\/g,
1124
+ "/"
1125
+ );
1126
+ }
1127
+
1128
+ function readJson<T>(
1129
+ filePath: string
1130
+ ): T {
1131
+ return JSON.parse(
1132
+ fs.readFileSync(
1133
+ filePath,
1134
+ "utf8"
1135
+ )
1136
+ ) as T;
1137
+ }
1138
+
1139
+ function writeJson(
1140
+ filePath: string,
1141
+ value: unknown
1142
+ ): void {
1143
+ fs.writeFileSync(
1144
+ filePath,
1145
+ `${JSON.stringify(
1146
+ value,
1147
+ null,
1148
+ 2
1149
+ )}\n`,
1150
+ "utf8"
1151
+ );
1152
+ }
1153
+
1154
+ function isPlainObject(
1155
+ value: unknown
1156
+ ): value is JsonObject {
1157
+ return Boolean(
1158
+ value &&
1159
+ typeof value === "object" &&
1160
+ !Array.isArray(value)
1161
+ );
1162
+ }