@chidchanun/bcp 0.1.21 → 0.1.23

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,1249 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ createRequire,
5
+ } from "node:module";
6
+
7
+ import {
8
+ validateClientBoundaries,
9
+ } from "../../bundler/src/client-boundary.js";
10
+ import {
11
+ resolveBcpConfig,
12
+ type ResolvedBcpConfig,
13
+ } from "../../config/src/index.js";
14
+ import {
15
+ loadEnvironment,
16
+ } from "../../env/src/index.js";
17
+ import {
18
+ assertNoPageApiConflicts,
19
+ scanApiRoutes,
20
+ scanRoutes,
21
+ } from "../../router/src/index.js";
22
+
23
+ export type DoctorStatus =
24
+ | "pass"
25
+ | "warn"
26
+ | "fail";
27
+
28
+ export interface DoctorCheck {
29
+ id: string;
30
+ status: DoctorStatus;
31
+ label: string;
32
+ message: string;
33
+ }
34
+
35
+ export interface DoctorReport {
36
+ frameworkVersion: string;
37
+ rootDirectory: string;
38
+ runtime: {
39
+ node: string;
40
+ platform: NodeJS.Platform;
41
+ arch: string;
42
+ };
43
+ checks: DoctorCheck[];
44
+ summary: {
45
+ passed: number;
46
+ warnings: number;
47
+ failed: number;
48
+ };
49
+ }
50
+
51
+ export interface InspectReport {
52
+ frameworkVersion: string;
53
+ rootDirectory: string;
54
+ runtime: {
55
+ node: string;
56
+ platform: NodeJS.Platform;
57
+ arch: string;
58
+ };
59
+ environment: {
60
+ mode: "development";
61
+ files: string[];
62
+ publicVariables: string[];
63
+ };
64
+ config: {
65
+ file: string | null;
66
+ resolved: ResolvedBcpConfig;
67
+ };
68
+ dependencies: {
69
+ bcp: string | null;
70
+ react: string | null;
71
+ reactDom: string | null;
72
+ };
73
+ routes: {
74
+ pages: Array<{
75
+ pathname: string;
76
+ file: string;
77
+ }>;
78
+ api: Array<{
79
+ pathname: string;
80
+ file: string;
81
+ }>;
82
+ };
83
+ }
84
+
85
+ interface DeveloperToolOptions {
86
+ rootDirectory: string;
87
+ frameworkVersion: string;
88
+ json?: boolean;
89
+ }
90
+
91
+ interface PackageManifestInfo {
92
+ version: string;
93
+ manifestPath: string;
94
+ packageRoot: string;
95
+ }
96
+
97
+ interface ProjectPackageJson {
98
+ dependencies?: Record<string, string>;
99
+ devDependencies?: Record<string, string>;
100
+ }
101
+
102
+ const MINIMUM_NODE_VERSION =
103
+ "24.11.0";
104
+
105
+ const frameworkRequire =
106
+ createRequire(
107
+ import.meta.url
108
+ );
109
+
110
+ export async function runDoctor(
111
+ options: DeveloperToolOptions
112
+ ): Promise<DoctorReport> {
113
+ const report =
114
+ await collectDoctorReport(
115
+ options
116
+ );
117
+
118
+ if (options.json) {
119
+ console.log(
120
+ JSON.stringify(
121
+ report,
122
+ null,
123
+ 2
124
+ )
125
+ );
126
+ } else {
127
+ printDoctorReport(
128
+ report
129
+ );
130
+ }
131
+
132
+ if (
133
+ report.summary.failed > 0
134
+ ) {
135
+ process.exitCode = 1;
136
+ }
137
+
138
+ return report;
139
+ }
140
+
141
+ export async function runInspect(
142
+ options: DeveloperToolOptions
143
+ ): Promise<InspectReport> {
144
+ try {
145
+ const report =
146
+ await collectInspectReport(
147
+ options
148
+ );
149
+
150
+ if (options.json) {
151
+ console.log(
152
+ JSON.stringify(
153
+ report,
154
+ null,
155
+ 2
156
+ )
157
+ );
158
+ } else {
159
+ printInspectReport(
160
+ report
161
+ );
162
+ }
163
+
164
+ return report;
165
+ } catch (error) {
166
+ if (options.json) {
167
+ console.log(
168
+ JSON.stringify(
169
+ {
170
+ frameworkVersion:
171
+ options.frameworkVersion,
172
+ rootDirectory:
173
+ options.rootDirectory,
174
+ error:
175
+ errorMessage(
176
+ error
177
+ ),
178
+ },
179
+ null,
180
+ 2
181
+ )
182
+ );
183
+ process.exitCode = 1;
184
+ throw new DeveloperToolJsonError();
185
+ }
186
+
187
+ throw error;
188
+ }
189
+ }
190
+
191
+ export async function collectDoctorReport(
192
+ options: DeveloperToolOptions
193
+ ): Promise<DoctorReport> {
194
+ const rootDirectory =
195
+ path.resolve(
196
+ options.rootDirectory
197
+ );
198
+ const checks:
199
+ DoctorCheck[] = [];
200
+
201
+ addCheck(
202
+ checks,
203
+ "runtime.node",
204
+ compareVersions(
205
+ process.versions.node,
206
+ MINIMUM_NODE_VERSION
207
+ ) >= 0
208
+ ? "pass"
209
+ : "fail",
210
+ "Node.js runtime",
211
+ compareVersions(
212
+ process.versions.node,
213
+ MINIMUM_NODE_VERSION
214
+ ) >= 0
215
+ ? `Node.js ${process.versions.node} satisfies >=${MINIMUM_NODE_VERSION}.`
216
+ : `Node.js ${process.versions.node} is unsupported; BCP requires >=${MINIMUM_NODE_VERSION}.`
217
+ );
218
+
219
+ const packageFile =
220
+ path.join(
221
+ rootDirectory,
222
+ "package.json"
223
+ );
224
+
225
+ let projectPackage:
226
+ ProjectPackageJson | null = null;
227
+
228
+ if (
229
+ !fs.existsSync(
230
+ packageFile
231
+ )
232
+ ) {
233
+ addCheck(
234
+ checks,
235
+ "project.package",
236
+ "fail",
237
+ "Project package",
238
+ "package.json was not found in the project root."
239
+ );
240
+ } else {
241
+ try {
242
+ projectPackage =
243
+ readProjectPackage(
244
+ packageFile
245
+ );
246
+
247
+ addCheck(
248
+ checks,
249
+ "project.package",
250
+ "pass",
251
+ "Project package",
252
+ "package.json is readable."
253
+ );
254
+ } catch (error) {
255
+ addCheck(
256
+ checks,
257
+ "project.package",
258
+ "fail",
259
+ "Project package",
260
+ errorMessage(
261
+ error
262
+ )
263
+ );
264
+ }
265
+ }
266
+
267
+ const appDirectory =
268
+ path.join(
269
+ rootDirectory,
270
+ "app"
271
+ );
272
+ const hasAppDirectory =
273
+ isDirectory(
274
+ appDirectory
275
+ );
276
+
277
+ addCheck(
278
+ checks,
279
+ "project.app",
280
+ hasAppDirectory
281
+ ? "pass"
282
+ : "fail",
283
+ "App directory",
284
+ hasAppDirectory
285
+ ? "app/ is present."
286
+ : "app/ was not found. BCP page and API routes are discovered from this directory."
287
+ );
288
+
289
+ const publicDirectory =
290
+ path.join(
291
+ rootDirectory,
292
+ "public"
293
+ );
294
+
295
+ addCheck(
296
+ checks,
297
+ "project.public",
298
+ isDirectory(
299
+ publicDirectory
300
+ )
301
+ ? "pass"
302
+ : "warn",
303
+ "Public directory",
304
+ isDirectory(
305
+ publicDirectory
306
+ )
307
+ ? "public/ is present."
308
+ : "public/ is not present. This is valid when the application has no static assets."
309
+ );
310
+
311
+ if (projectPackage) {
312
+ const declared =
313
+ findDeclaredFrameworkDependency(
314
+ projectPackage
315
+ );
316
+
317
+ if (
318
+ declared?.name === "bcp"
319
+ ) {
320
+ addCheck(
321
+ checks,
322
+ "dependencies.bcp",
323
+ "pass",
324
+ "BCP dependency",
325
+ `bcp is declared as ${declared.range}.`
326
+ );
327
+ } else if (declared) {
328
+ addCheck(
329
+ checks,
330
+ "dependencies.bcp",
331
+ "warn",
332
+ "BCP dependency",
333
+ `${declared.name} is declared as ${declared.range}. The documented application import name is bcp.`
334
+ );
335
+ } else {
336
+ addCheck(
337
+ checks,
338
+ "dependencies.bcp",
339
+ "fail",
340
+ "BCP dependency",
341
+ "Neither bcp nor @chidchanun/bcp is declared in dependencies/devDependencies."
342
+ );
343
+ }
344
+ }
345
+
346
+ const appReact =
347
+ resolveInstalledPackage(
348
+ packageFile,
349
+ "react"
350
+ );
351
+ const appReactDom =
352
+ resolveInstalledPackage(
353
+ packageFile,
354
+ "react-dom"
355
+ );
356
+ const appBcp =
357
+ resolveInstalledPackage(
358
+ packageFile,
359
+ "bcp"
360
+ ) ??
361
+ resolveInstalledPackage(
362
+ packageFile,
363
+ "@chidchanun/bcp"
364
+ );
365
+
366
+ addInstalledPackageCheck(
367
+ checks,
368
+ "dependencies.installed-bcp",
369
+ "Installed BCP package",
370
+ appBcp
371
+ );
372
+ addInstalledPackageCheck(
373
+ checks,
374
+ "dependencies.react",
375
+ "React package",
376
+ appReact
377
+ );
378
+ addInstalledPackageCheck(
379
+ checks,
380
+ "dependencies.react-dom",
381
+ "React DOM package",
382
+ appReactDom
383
+ );
384
+
385
+ if (
386
+ appReact &&
387
+ appReactDom
388
+ ) {
389
+ addCheck(
390
+ checks,
391
+ "dependencies.react-version-parity",
392
+ appReact.version ===
393
+ appReactDom.version
394
+ ? "pass"
395
+ : "fail",
396
+ "React renderer version parity",
397
+ appReact.version ===
398
+ appReactDom.version
399
+ ? `react and react-dom both resolve to ${appReact.version}.`
400
+ : `react resolves to ${appReact.version} while react-dom resolves to ${appReactDom.version}. Install matching versions.`
401
+ );
402
+ }
403
+
404
+ const frameworkReact =
405
+ resolveFrameworkPackage(
406
+ "react"
407
+ );
408
+ const frameworkReactDom =
409
+ resolveFrameworkPackage(
410
+ "react-dom"
411
+ );
412
+
413
+ if (
414
+ appReact &&
415
+ frameworkReact
416
+ ) {
417
+ const sameReact =
418
+ samePackageRoot(
419
+ appReact,
420
+ frameworkReact
421
+ );
422
+
423
+ addCheck(
424
+ checks,
425
+ "dependencies.single-react",
426
+ sameReact
427
+ ? "pass"
428
+ : "fail",
429
+ "Single React instance",
430
+ sameReact
431
+ ? `BCP and the application resolve the same React package (${appReact.version}).`
432
+ : `BCP resolves React from ${frameworkReact.packageRoot}, but the application resolves React from ${appReact.packageRoot}. This can cause Invalid hook call errors.`
433
+ );
434
+ }
435
+
436
+ if (
437
+ appReactDom &&
438
+ frameworkReactDom
439
+ ) {
440
+ const sameRenderer =
441
+ samePackageRoot(
442
+ appReactDom,
443
+ frameworkReactDom
444
+ );
445
+
446
+ addCheck(
447
+ checks,
448
+ "dependencies.single-react-dom",
449
+ sameRenderer
450
+ ? "pass"
451
+ : "fail",
452
+ "Single React DOM renderer",
453
+ sameRenderer
454
+ ? `BCP and the application resolve the same React DOM package (${appReactDom.version}).`
455
+ : `BCP resolves react-dom from ${frameworkReactDom.packageRoot}, but the application resolves react-dom from ${appReactDom.packageRoot}. Avoid linked framework directories; test packed .tgz artifacts instead.`
456
+ );
457
+ }
458
+
459
+ try {
460
+ const environment =
461
+ loadEnvironment(
462
+ rootDirectory,
463
+ "development"
464
+ );
465
+
466
+ addCheck(
467
+ checks,
468
+ "environment.development",
469
+ "pass",
470
+ "Development environment",
471
+ environment.files.length > 0
472
+ ? `Loaded ${environment.files.join(", ")}. Public variables: ${Object.keys(environment.publicValues).length}.`
473
+ : "No development .env files were found; process environment and defaults remain available."
474
+ );
475
+ } catch (error) {
476
+ addCheck(
477
+ checks,
478
+ "environment.development",
479
+ "fail",
480
+ "Development environment",
481
+ errorMessage(
482
+ error
483
+ )
484
+ );
485
+ }
486
+
487
+ try {
488
+ const resolved =
489
+ await resolveBcpConfig(
490
+ rootDirectory
491
+ );
492
+
493
+ addCheck(
494
+ checks,
495
+ "config.resolved",
496
+ "pass",
497
+ "Framework config",
498
+ resolved.file
499
+ ? `${path.basename(resolved.file)} is valid; server resolves to ${resolved.config.server.hostname}:${resolved.config.server.port}.`
500
+ : `No bcp.config file found; defaults/environment resolve server to ${resolved.config.server.hostname}:${resolved.config.server.port}.`
501
+ );
502
+ } catch (error) {
503
+ addCheck(
504
+ checks,
505
+ "config.resolved",
506
+ "fail",
507
+ "Framework config",
508
+ errorMessage(
509
+ error
510
+ )
511
+ );
512
+ }
513
+
514
+ if (hasAppDirectory) {
515
+ try {
516
+ const pageRoutes =
517
+ scanRoutes(
518
+ appDirectory
519
+ );
520
+ const apiRoutes =
521
+ scanApiRoutes(
522
+ appDirectory
523
+ );
524
+
525
+ assertNoPageApiConflicts(
526
+ pageRoutes,
527
+ apiRoutes
528
+ );
529
+
530
+ addCheck(
531
+ checks,
532
+ "routes.discovery",
533
+ pageRoutes.length > 0 ||
534
+ apiRoutes.length > 0
535
+ ? "pass"
536
+ : "warn",
537
+ "Route discovery",
538
+ `Discovered ${pageRoutes.length} page route(s) and ${apiRoutes.length} API route(s).`
539
+ );
540
+
541
+ try {
542
+ validateClientBoundaries(
543
+ pageRoutes,
544
+ rootDirectory
545
+ );
546
+
547
+ addCheck(
548
+ checks,
549
+ "routes.client-boundaries",
550
+ "pass",
551
+ "Client boundaries",
552
+ "Hydrated routes do not expose detected server-only modules and client-hook directives are valid."
553
+ );
554
+ } catch (error) {
555
+ addCheck(
556
+ checks,
557
+ "routes.client-boundaries",
558
+ "fail",
559
+ "Client boundaries",
560
+ errorMessage(
561
+ error
562
+ )
563
+ );
564
+ }
565
+ } catch (error) {
566
+ addCheck(
567
+ checks,
568
+ "routes.discovery",
569
+ "fail",
570
+ "Route discovery",
571
+ errorMessage(
572
+ error
573
+ )
574
+ );
575
+ }
576
+ }
577
+
578
+ const summary = {
579
+ passed:
580
+ checks.filter(
581
+ (check) =>
582
+ check.status === "pass"
583
+ ).length,
584
+ warnings:
585
+ checks.filter(
586
+ (check) =>
587
+ check.status === "warn"
588
+ ).length,
589
+ failed:
590
+ checks.filter(
591
+ (check) =>
592
+ check.status === "fail"
593
+ ).length,
594
+ };
595
+
596
+ return {
597
+ frameworkVersion:
598
+ options.frameworkVersion,
599
+ rootDirectory,
600
+ runtime: {
601
+ node:
602
+ process.versions.node,
603
+ platform:
604
+ process.platform,
605
+ arch:
606
+ process.arch,
607
+ },
608
+ checks,
609
+ summary,
610
+ };
611
+ }
612
+
613
+ export async function collectInspectReport(
614
+ options: DeveloperToolOptions
615
+ ): Promise<InspectReport> {
616
+ const rootDirectory =
617
+ path.resolve(
618
+ options.rootDirectory
619
+ );
620
+ const packageFile =
621
+ path.join(
622
+ rootDirectory,
623
+ "package.json"
624
+ );
625
+ const appDirectory =
626
+ path.join(
627
+ rootDirectory,
628
+ "app"
629
+ );
630
+
631
+ const environment =
632
+ loadEnvironment(
633
+ rootDirectory,
634
+ "development"
635
+ );
636
+ const resolved =
637
+ await resolveBcpConfig(
638
+ rootDirectory
639
+ );
640
+ const pageRoutes =
641
+ scanRoutes(
642
+ appDirectory
643
+ );
644
+ const apiRoutes =
645
+ scanApiRoutes(
646
+ appDirectory
647
+ );
648
+
649
+ assertNoPageApiConflicts(
650
+ pageRoutes,
651
+ apiRoutes
652
+ );
653
+ validateClientBoundaries(
654
+ pageRoutes,
655
+ rootDirectory
656
+ );
657
+
658
+ const bcp =
659
+ resolveInstalledPackage(
660
+ packageFile,
661
+ "bcp"
662
+ ) ??
663
+ resolveInstalledPackage(
664
+ packageFile,
665
+ "@chidchanun/bcp"
666
+ );
667
+ const react =
668
+ resolveInstalledPackage(
669
+ packageFile,
670
+ "react"
671
+ );
672
+ const reactDom =
673
+ resolveInstalledPackage(
674
+ packageFile,
675
+ "react-dom"
676
+ );
677
+
678
+ return {
679
+ frameworkVersion:
680
+ options.frameworkVersion,
681
+ rootDirectory,
682
+ runtime: {
683
+ node:
684
+ process.versions.node,
685
+ platform:
686
+ process.platform,
687
+ arch:
688
+ process.arch,
689
+ },
690
+ environment: {
691
+ mode:
692
+ "development",
693
+ files:
694
+ environment.files,
695
+ publicVariables:
696
+ Object.keys(
697
+ environment.publicValues
698
+ ).sort(),
699
+ },
700
+ config: {
701
+ file:
702
+ resolved.file
703
+ ? path.basename(
704
+ resolved.file
705
+ )
706
+ : null,
707
+ resolved:
708
+ resolved.config,
709
+ },
710
+ dependencies: {
711
+ bcp:
712
+ bcp?.version ??
713
+ null,
714
+ react:
715
+ react?.version ??
716
+ null,
717
+ reactDom:
718
+ reactDom?.version ??
719
+ null,
720
+ },
721
+ routes: {
722
+ pages:
723
+ pageRoutes.map(
724
+ (route) => ({
725
+ pathname:
726
+ route.pathname,
727
+ file:
728
+ relativeProjectPath(
729
+ rootDirectory,
730
+ route.filePath
731
+ ),
732
+ })
733
+ ),
734
+ api:
735
+ apiRoutes.map(
736
+ (route) => ({
737
+ pathname:
738
+ route.pathname,
739
+ file:
740
+ relativeProjectPath(
741
+ rootDirectory,
742
+ route.filePath
743
+ ),
744
+ })
745
+ ),
746
+ },
747
+ };
748
+ }
749
+
750
+ export function compareVersions(
751
+ left: string,
752
+ right: string
753
+ ): number {
754
+ const leftParts =
755
+ parseVersionParts(
756
+ left
757
+ );
758
+ const rightParts =
759
+ parseVersionParts(
760
+ right
761
+ );
762
+
763
+ for (
764
+ let index = 0;
765
+ index < 3;
766
+ index++
767
+ ) {
768
+ if (
769
+ leftParts[index] >
770
+ rightParts[index]
771
+ ) {
772
+ return 1;
773
+ }
774
+
775
+ if (
776
+ leftParts[index] <
777
+ rightParts[index]
778
+ ) {
779
+ return -1;
780
+ }
781
+ }
782
+
783
+ return 0;
784
+ }
785
+
786
+ function printDoctorReport(
787
+ report: DoctorReport
788
+ ): void {
789
+ console.log("");
790
+ console.log(
791
+ `BCP Doctor v${report.frameworkVersion}`
792
+ );
793
+ console.log(
794
+ `Project: ${report.rootDirectory}`
795
+ );
796
+ console.log(
797
+ `Runtime: Node.js ${report.runtime.node} | ${report.runtime.platform} ${report.runtime.arch}`
798
+ );
799
+ console.log("");
800
+
801
+ for (const check of report.checks) {
802
+ const marker =
803
+ check.status === "pass"
804
+ ? "PASS"
805
+ : check.status === "warn"
806
+ ? "WARN"
807
+ : "FAIL";
808
+
809
+ console.log(
810
+ `[${marker}] ${check.label}: ${check.message}`
811
+ );
812
+ }
813
+
814
+ console.log("");
815
+ console.log(
816
+ `Summary: ${report.summary.passed} passed, ${report.summary.warnings} warning(s), ${report.summary.failed} failed.`
817
+ );
818
+ console.log("");
819
+ }
820
+
821
+ function printInspectReport(
822
+ report: InspectReport
823
+ ): void {
824
+ console.log("");
825
+ console.log(
826
+ `BCP Inspect v${report.frameworkVersion}`
827
+ );
828
+ console.log(
829
+ `Project: ${report.rootDirectory}`
830
+ );
831
+ console.log(
832
+ `Runtime: Node.js ${report.runtime.node} | ${report.runtime.platform} ${report.runtime.arch}`
833
+ );
834
+ console.log("");
835
+ console.log(
836
+ "Environment:"
837
+ );
838
+ console.log(
839
+ ` mode: ${report.environment.mode}`
840
+ );
841
+ console.log(
842
+ ` files: ${report.environment.files.length > 0 ? report.environment.files.join(", ") : "(none)"}`
843
+ );
844
+ console.log(
845
+ ` public variables: ${report.environment.publicVariables.length > 0 ? report.environment.publicVariables.join(", ") : "(none)"}`
846
+ );
847
+ console.log("");
848
+ console.log(
849
+ "Dependencies:"
850
+ );
851
+ console.log(
852
+ ` bcp: ${report.dependencies.bcp ?? "not resolved"}`
853
+ );
854
+ console.log(
855
+ ` react: ${report.dependencies.react ?? "not resolved"}`
856
+ );
857
+ console.log(
858
+ ` react-dom: ${report.dependencies.reactDom ?? "not resolved"}`
859
+ );
860
+ console.log("");
861
+ console.log(
862
+ `Config: ${report.config.file ?? "defaults/environment"}`
863
+ );
864
+ console.log(
865
+ JSON.stringify(
866
+ report.config.resolved,
867
+ null,
868
+ 2
869
+ )
870
+ );
871
+ console.log("");
872
+ console.log(
873
+ `Routes: ${report.routes.pages.length} page(s), ${report.routes.api.length} API route(s)`
874
+ );
875
+
876
+ for (const route of report.routes.pages) {
877
+ console.log(
878
+ ` PAGE ${route.pathname} -> ${route.file}`
879
+ );
880
+ }
881
+
882
+ for (const route of report.routes.api) {
883
+ console.log(
884
+ ` API ${route.pathname} -> ${route.file}`
885
+ );
886
+ }
887
+
888
+ console.log("");
889
+ }
890
+
891
+ function addInstalledPackageCheck(
892
+ checks: DoctorCheck[],
893
+ id: string,
894
+ label: string,
895
+ info: PackageManifestInfo | null
896
+ ): void {
897
+ addCheck(
898
+ checks,
899
+ id,
900
+ info
901
+ ? "pass"
902
+ : "fail",
903
+ label,
904
+ info
905
+ ? `${info.version} at ${info.packageRoot}`
906
+ : "Package could not be resolved from the project."
907
+ );
908
+ }
909
+
910
+ function addCheck(
911
+ checks: DoctorCheck[],
912
+ id: string,
913
+ status: DoctorStatus,
914
+ label: string,
915
+ message: string
916
+ ): void {
917
+ checks.push({
918
+ id,
919
+ status,
920
+ label,
921
+ message,
922
+ });
923
+ }
924
+
925
+ function readProjectPackage(
926
+ packageFile: string
927
+ ): ProjectPackageJson {
928
+ const value =
929
+ JSON.parse(
930
+ fs.readFileSync(
931
+ packageFile,
932
+ "utf8"
933
+ )
934
+ );
935
+
936
+ if (
937
+ !value ||
938
+ typeof value !== "object" ||
939
+ Array.isArray(
940
+ value
941
+ )
942
+ ) {
943
+ throw new Error(
944
+ "package.json must contain a JSON object."
945
+ );
946
+ }
947
+
948
+ return value as ProjectPackageJson;
949
+ }
950
+
951
+ function findDeclaredFrameworkDependency(
952
+ projectPackage: ProjectPackageJson
953
+ ): {
954
+ name: string;
955
+ range: string;
956
+ } | null {
957
+ for (
958
+ const dependencies
959
+ of [
960
+ projectPackage.dependencies,
961
+ projectPackage.devDependencies,
962
+ ]
963
+ ) {
964
+ if (!dependencies) {
965
+ continue;
966
+ }
967
+
968
+ if (
969
+ dependencies.bcp
970
+ ) {
971
+ return {
972
+ name:
973
+ "bcp",
974
+ range:
975
+ dependencies.bcp,
976
+ };
977
+ }
978
+
979
+ if (
980
+ dependencies[
981
+ "@chidchanun/bcp"
982
+ ]
983
+ ) {
984
+ return {
985
+ name:
986
+ "@chidchanun/bcp",
987
+ range:
988
+ dependencies[
989
+ "@chidchanun/bcp"
990
+ ],
991
+ };
992
+ }
993
+ }
994
+
995
+ return null;
996
+ }
997
+
998
+ function resolveInstalledPackage(
999
+ projectPackageFile: string,
1000
+ packageName: string
1001
+ ): PackageManifestInfo | null {
1002
+ if (
1003
+ !fs.existsSync(
1004
+ projectPackageFile
1005
+ )
1006
+ ) {
1007
+ return null;
1008
+ }
1009
+
1010
+ try {
1011
+ const projectRequire =
1012
+ createRequire(
1013
+ projectPackageFile
1014
+ );
1015
+
1016
+ return resolvePackageWithRequire(
1017
+ projectRequire,
1018
+ packageName
1019
+ );
1020
+ } catch {
1021
+ return null;
1022
+ }
1023
+ }
1024
+
1025
+ function resolveFrameworkPackage(
1026
+ packageName: string
1027
+ ): PackageManifestInfo | null {
1028
+ try {
1029
+ return resolvePackageWithRequire(
1030
+ frameworkRequire,
1031
+ packageName
1032
+ );
1033
+ } catch {
1034
+ return null;
1035
+ }
1036
+ }
1037
+
1038
+ function resolvePackageWithRequire(
1039
+ resolver: NodeJS.Require,
1040
+ packageName: string
1041
+ ): PackageManifestInfo {
1042
+ let manifestPath:
1043
+ string;
1044
+
1045
+ try {
1046
+ manifestPath =
1047
+ resolver.resolve(
1048
+ `${packageName}/package.json`
1049
+ );
1050
+ } catch {
1051
+ const entry =
1052
+ resolver.resolve(
1053
+ packageName
1054
+ );
1055
+ const discovered =
1056
+ findNearestPackageManifest(
1057
+ path.dirname(
1058
+ entry
1059
+ )
1060
+ );
1061
+
1062
+ if (!discovered) {
1063
+ throw new Error(
1064
+ `Could not locate package.json for ${packageName}.`
1065
+ );
1066
+ }
1067
+
1068
+ manifestPath =
1069
+ discovered;
1070
+ }
1071
+
1072
+ const manifest =
1073
+ JSON.parse(
1074
+ fs.readFileSync(
1075
+ manifestPath,
1076
+ "utf8"
1077
+ )
1078
+ ) as {
1079
+ version?: unknown;
1080
+ };
1081
+
1082
+ if (
1083
+ typeof manifest.version !==
1084
+ "string"
1085
+ ) {
1086
+ throw new Error(
1087
+ `${packageName} package.json does not contain a version.`
1088
+ );
1089
+ }
1090
+
1091
+ const packageRoot =
1092
+ realPath(
1093
+ path.dirname(
1094
+ manifestPath
1095
+ )
1096
+ );
1097
+
1098
+ return {
1099
+ version:
1100
+ manifest.version,
1101
+ manifestPath:
1102
+ realPath(
1103
+ manifestPath
1104
+ ),
1105
+ packageRoot,
1106
+ };
1107
+ }
1108
+
1109
+ function findNearestPackageManifest(
1110
+ startDirectory: string
1111
+ ): string | null {
1112
+ let current =
1113
+ path.resolve(
1114
+ startDirectory
1115
+ );
1116
+
1117
+ while (true) {
1118
+ const candidate =
1119
+ path.join(
1120
+ current,
1121
+ "package.json"
1122
+ );
1123
+
1124
+ if (
1125
+ fs.existsSync(
1126
+ candidate
1127
+ )
1128
+ ) {
1129
+ return candidate;
1130
+ }
1131
+
1132
+ const parent =
1133
+ path.dirname(
1134
+ current
1135
+ );
1136
+
1137
+ if (parent === current) {
1138
+ return null;
1139
+ }
1140
+
1141
+ current =
1142
+ parent;
1143
+ }
1144
+ }
1145
+
1146
+ function samePackageRoot(
1147
+ left: PackageManifestInfo,
1148
+ right: PackageManifestInfo
1149
+ ): boolean {
1150
+ const normalize =
1151
+ (value: string) =>
1152
+ process.platform === "win32"
1153
+ ? value.toLowerCase()
1154
+ : value;
1155
+
1156
+ return normalize(
1157
+ left.packageRoot
1158
+ ) === normalize(
1159
+ right.packageRoot
1160
+ );
1161
+ }
1162
+
1163
+ function realPath(
1164
+ value: string
1165
+ ): string {
1166
+ try {
1167
+ return fs.realpathSync(
1168
+ value
1169
+ );
1170
+ } catch {
1171
+ return path.resolve(
1172
+ value
1173
+ );
1174
+ }
1175
+ }
1176
+
1177
+ function isDirectory(
1178
+ directory: string
1179
+ ): boolean {
1180
+ try {
1181
+ return fs.statSync(
1182
+ directory
1183
+ ).isDirectory();
1184
+ } catch {
1185
+ return false;
1186
+ }
1187
+ }
1188
+
1189
+ function relativeProjectPath(
1190
+ rootDirectory: string,
1191
+ filePath: string
1192
+ ): string {
1193
+ return path.relative(
1194
+ rootDirectory,
1195
+ filePath
1196
+ ).replace(
1197
+ /\\/g,
1198
+ "/"
1199
+ );
1200
+ }
1201
+
1202
+ function parseVersionParts(
1203
+ version: string
1204
+ ): [number, number, number] {
1205
+ const match =
1206
+ /^(\d+)\.(\d+)\.(\d+)/.exec(
1207
+ version
1208
+ );
1209
+
1210
+ if (!match) {
1211
+ return [
1212
+ 0,
1213
+ 0,
1214
+ 0,
1215
+ ];
1216
+ }
1217
+
1218
+ return [
1219
+ Number(
1220
+ match[1]
1221
+ ),
1222
+ Number(
1223
+ match[2]
1224
+ ),
1225
+ Number(
1226
+ match[3]
1227
+ ),
1228
+ ];
1229
+ }
1230
+
1231
+ function errorMessage(
1232
+ error: unknown
1233
+ ): string {
1234
+ return error instanceof Error
1235
+ ? error.message
1236
+ : String(
1237
+ error
1238
+ );
1239
+ }
1240
+
1241
+ class DeveloperToolJsonError extends Error {
1242
+ constructor() {
1243
+ super(
1244
+ "BCP inspect failed."
1245
+ );
1246
+ this.name =
1247
+ "DeveloperToolJsonError";
1248
+ }
1249
+ }