@chidchanun/bcp 0.2.1 → 0.2.2

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,235 @@
1
+ import {
2
+ loadEnvironment,
3
+ } from "../../env/src/index.js";
4
+ import {
5
+ type ResolveBcpConfigOverrides,
6
+ } from "../../config/src/index.js";
7
+ import {
8
+ loadBcpEnvironmentSchema,
9
+ } from "../../config/src/environment-loader.js";
10
+ import {
11
+ applyEnvironmentDefaults,
12
+ } from "../../config/src/environment-schema.js";
13
+ import {
14
+ assertConfigurationDiagnostics,
15
+ diagnoseBcpConfiguration,
16
+ type BcpConfigurationDiagnosticsReport,
17
+ } from "../../config/src/diagnostics.js";
18
+
19
+ export interface RunConfigurationCheckOptions {
20
+ rootDirectory: string;
21
+ mode?: "development" | "production" | "test";
22
+ json?: boolean;
23
+ }
24
+
25
+ export async function runConfigurationCheck(
26
+ options: RunConfigurationCheckOptions
27
+ ): Promise<BcpConfigurationDiagnosticsReport> {
28
+ const mode =
29
+ options.mode ??
30
+ resolveCheckMode();
31
+ const loadedEnvironment =
32
+ loadEnvironment(
33
+ options.rootDirectory,
34
+ mode
35
+ );
36
+ const schema =
37
+ await loadBcpEnvironmentSchema(
38
+ options.rootDirectory
39
+ );
40
+ const appliedDefaults =
41
+ applyEnvironmentDefaults(
42
+ schema.schema,
43
+ process.env
44
+ );
45
+ const report =
46
+ await diagnoseBcpConfiguration({
47
+ rootDirectory:
48
+ options.rootDirectory,
49
+ mode,
50
+ environment:
51
+ process.env,
52
+ });
53
+
54
+ if (
55
+ options.json
56
+ ) {
57
+ console.log(
58
+ JSON.stringify(
59
+ {
60
+ ...report,
61
+ environmentFiles:
62
+ loadedEnvironment.files,
63
+ appliedDefaults,
64
+ },
65
+ null,
66
+ 2
67
+ )
68
+ );
69
+
70
+ if (
71
+ !report.ok
72
+ ) {
73
+ process.exitCode =
74
+ 1;
75
+
76
+ return report;
77
+ }
78
+ } else {
79
+ printConfigurationReport(
80
+ report,
81
+ loadedEnvironment.files,
82
+ appliedDefaults
83
+ );
84
+ }
85
+
86
+ assertConfigurationDiagnostics(
87
+ report
88
+ );
89
+
90
+ return report;
91
+ }
92
+
93
+ export async function runStartupConfigurationDiagnostics(
94
+ rootDirectory: string,
95
+ mode: "development" | "production",
96
+ overrides: ResolveBcpConfigOverrides = {}
97
+ ): Promise<BcpConfigurationDiagnosticsReport> {
98
+ const schema =
99
+ await loadBcpEnvironmentSchema(
100
+ rootDirectory
101
+ );
102
+ const appliedDefaults =
103
+ applyEnvironmentDefaults(
104
+ schema.schema,
105
+ process.env
106
+ );
107
+ const report =
108
+ await diagnoseBcpConfiguration({
109
+ rootDirectory,
110
+ mode,
111
+ environment:
112
+ process.env,
113
+ overrides,
114
+ });
115
+
116
+ if (
117
+ report.environmentSchemaFile
118
+ ) {
119
+ console.log(
120
+ `[BCP Config] Environment schema: ${report.environmentSchemaFile} (${report.environment.checked} variable(s), ${appliedDefaults} default(s) applied)`
121
+ );
122
+ }
123
+
124
+ for (
125
+ const diagnostic
126
+ of report.diagnostics
127
+ ) {
128
+ if (
129
+ diagnostic.severity ===
130
+ "warning"
131
+ ) {
132
+ console.warn(
133
+ `[BCP Config] Warning: ${diagnostic.message}`
134
+ );
135
+ }
136
+ }
137
+
138
+ assertConfigurationDiagnostics(
139
+ report
140
+ );
141
+
142
+ return report;
143
+ }
144
+
145
+ function printConfigurationReport(
146
+ report: BcpConfigurationDiagnosticsReport,
147
+ environmentFiles: string[],
148
+ appliedDefaults: number
149
+ ): void {
150
+ const errors =
151
+ report.diagnostics.filter(
152
+ (diagnostic) =>
153
+ diagnostic.severity ===
154
+ "error"
155
+ );
156
+ const warnings =
157
+ report.diagnostics.filter(
158
+ (diagnostic) =>
159
+ diagnostic.severity ===
160
+ "warning"
161
+ );
162
+
163
+ console.log("");
164
+ console.log(
165
+ `BCP Configuration Check (${report.mode})`
166
+ );
167
+ console.log("");
168
+ console.log(
169
+ ` Config: ${report.configFile ?? "defaults"}`
170
+ );
171
+ console.log(
172
+ ` Environment files: ${environmentFiles.length > 0 ? environmentFiles.join(", ") : "(none)"}`
173
+ );
174
+ console.log(
175
+ ` Environment schema: ${report.environmentSchemaFile ?? "(none)"}`
176
+ );
177
+ console.log(
178
+ ` Variables: ${report.environment.present}/${report.environment.checked} provided | ${appliedDefaults} default(s) applied`
179
+ );
180
+ console.log(
181
+ ` Server: ${report.resolvedConfig.server.hostname}:${report.resolvedConfig.server.port}`
182
+ );
183
+ console.log(
184
+ ` Build: minify=${report.resolvedConfig.build.minify} sourceMaps=${report.resolvedConfig.build.sourceMaps}`
185
+ );
186
+ console.log(
187
+ ` Diagnostics: ${errors.length} error(s), ${warnings.length} warning(s)`
188
+ );
189
+
190
+ if (
191
+ report.diagnostics.length >
192
+ 0
193
+ ) {
194
+ console.log("");
195
+
196
+ for (
197
+ const diagnostic
198
+ of report.diagnostics
199
+ ) {
200
+ console.log(
201
+ ` ${diagnostic.severity === "error" ? "✖" : "⚠"} ${diagnostic.message}`
202
+ );
203
+ }
204
+ }
205
+
206
+ if (
207
+ report.ok
208
+ ) {
209
+ console.log("");
210
+ console.log(
211
+ "[BCP Config] Configuration check passed."
212
+ );
213
+ }
214
+
215
+ console.log("");
216
+ }
217
+
218
+ function resolveCheckMode():
219
+ "development" | "production" | "test" {
220
+ if (
221
+ process.env.NODE_ENV ===
222
+ "production"
223
+ ) {
224
+ return "production";
225
+ }
226
+
227
+ if (
228
+ process.env.NODE_ENV ===
229
+ "test"
230
+ ) {
231
+ return "test";
232
+ }
233
+
234
+ return "development";
235
+ }
@@ -76,6 +76,11 @@ switch (cliOptions.command) {
76
76
  break;
77
77
  }
78
78
 
79
+ case "config": {
80
+ await runConfigCommand();
81
+ break;
82
+ }
83
+
79
84
  case "doctor": {
80
85
  await runDeveloperCommand(
81
86
  "doctor"
@@ -112,6 +117,24 @@ async function runDev() {
112
117
  cliOptions.rootDirectory
113
118
  );
114
119
 
120
+ const {
121
+ runStartupConfigurationDiagnostics,
122
+ } =
123
+ await import(
124
+ "./configuration.js"
125
+ );
126
+
127
+ await runStartupConfigurationDiagnostics(
128
+ rootDirectory,
129
+ "development",
130
+ {
131
+ port:
132
+ cliOptions.port,
133
+ hostname:
134
+ cliOptions.hostname,
135
+ }
136
+ );
137
+
115
138
  installApplicationModuleAlias(
116
139
  rootDirectory
117
140
  );
@@ -185,6 +208,24 @@ async function runBuild() {
185
208
  cliOptions.rootDirectory
186
209
  );
187
210
 
211
+ const {
212
+ runStartupConfigurationDiagnostics,
213
+ } =
214
+ await import(
215
+ "./configuration.js"
216
+ );
217
+
218
+ await runStartupConfigurationDiagnostics(
219
+ rootDirectory,
220
+ "production",
221
+ {
222
+ port:
223
+ cliOptions.port,
224
+ hostname:
225
+ cliOptions.hostname,
226
+ }
227
+ );
228
+
188
229
  const appDirectory =
189
230
  path.join(
190
231
  rootDirectory,
@@ -518,6 +559,36 @@ async function runGenerateCommand(): Promise<void> {
518
559
  });
519
560
  }
520
561
 
562
+ async function runConfigCommand(): Promise<void> {
563
+ const action =
564
+ cliOptions.configAction;
565
+
566
+ if (
567
+ action === undefined ||
568
+ action === "help"
569
+ ) {
570
+ printConfigHelp();
571
+ return;
572
+ }
573
+
574
+ const rootDirectory =
575
+ resolveProjectRoot(
576
+ cliOptions.rootDirectory
577
+ );
578
+ const {
579
+ runConfigurationCheck,
580
+ } =
581
+ await import(
582
+ "./configuration.js"
583
+ );
584
+
585
+ await runConfigurationCheck({
586
+ rootDirectory,
587
+ json:
588
+ cliOptions.json,
589
+ });
590
+ }
591
+
521
592
  async function runDeveloperCommand(
522
593
  command:
523
594
  "doctor" |
@@ -759,6 +830,32 @@ Examples:
759
830
  `);
760
831
  }
761
832
 
833
+ function printConfigHelp() {
834
+ console.log(`
835
+ BCP Configuration & Environment
836
+
837
+ Usage:
838
+ bcp config <command> [options]
839
+
840
+ Commands:
841
+ check Validate bcp.config.*, bcp.environment.* and loaded environment values
842
+ help Show configuration command help
843
+
844
+ Options:
845
+ --root <path> Project root directory
846
+ --json Emit the configuration diagnostics report as JSON
847
+
848
+ Environment mode:
849
+ NODE_ENV=production bcp config check
850
+ NODE_ENV=test bcp config check
851
+ bcp config check (development by default)
852
+
853
+ Examples:
854
+ bcp config check
855
+ bcp config check --json
856
+ `);
857
+ }
858
+
762
859
  function printHelp() {
763
860
  console.log(`
764
861
  BCP Framework v${FRAMEWORK_VERSION}
@@ -774,6 +871,7 @@ Commands:
774
871
  update [target] Update BCP Framework (default target: latest)
775
872
  db <command> Manage database migrations
776
873
  generate <kind> Generate pages, API routes, middleware or migrations
874
+ config <command> Validate project configuration and environment schema
777
875
  doctor Run project/runtime health diagnostics
778
876
  inspect Print resolved env, config, dependencies and routes
779
877
  help Show this help message
@@ -786,7 +884,7 @@ Options:
786
884
  --host <host> Alias for --hostname
787
885
  --check Check for a BCP update without changing files
788
886
  --dry-run Preview a BCP update without changing files
789
- --json JSON output for doctor/inspect
887
+ --json JSON output for doctor/inspect/config
790
888
  --force Replace existing generated scaffold files
791
889
  -h, --help Show help
792
890
  -v, --version Show version
@@ -803,6 +901,8 @@ Examples:
803
901
  bcp generate page dashboard/users
804
902
  bcp generate api users
805
903
  bcp generate middleware
904
+ bcp config check
905
+ bcp config check --json
806
906
  bcp doctor
807
907
  bcp doctor --json
808
908
  bcp inspect
@@ -16,3 +16,29 @@ export {
16
16
  type ResolveBcpConfigOverrides,
17
17
  type ResolvedBcpConfig,
18
18
  } from "../../config/src/index.js";
19
+
20
+ export {
21
+ applyEnvironmentDefaults,
22
+ defineEnvironment,
23
+ validateEnvironment,
24
+ type BcpEnvironmentSchema,
25
+ type BcpEnvironmentValidationIssue,
26
+ type BcpEnvironmentValidationResult,
27
+ type BcpEnvironmentValueType,
28
+ type BcpEnvironmentVariableRule,
29
+ } from "../../config/src/environment-schema.js";
30
+
31
+ export {
32
+ getEnvironmentSchemaFileNames,
33
+ loadBcpEnvironmentSchema,
34
+ type LoadedBcpEnvironmentSchema,
35
+ } from "../../config/src/environment-loader.js";
36
+
37
+ export {
38
+ assertConfigurationDiagnostics,
39
+ diagnoseBcpConfiguration,
40
+ type BcpConfigurationDiagnostic,
41
+ type BcpConfigurationDiagnosticSeverity,
42
+ type BcpConfigurationDiagnosticsReport,
43
+ type DiagnoseBcpConfigurationOptions,
44
+ } from "../../config/src/diagnostics.js";
@@ -0,0 +1,226 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ resolveBcpConfig,
5
+ type ResolveBcpConfigOverrides,
6
+ type ResolvedBcpConfig,
7
+ } from "./index.js";
8
+ import {
9
+ loadBcpEnvironmentSchema,
10
+ } from "./environment-loader.js";
11
+ import {
12
+ validateEnvironment,
13
+ type BcpEnvironmentValidationResult,
14
+ } from "./environment-schema.js";
15
+
16
+ export type BcpConfigurationDiagnosticSeverity =
17
+ | "error"
18
+ | "warning";
19
+
20
+ export interface BcpConfigurationDiagnostic {
21
+ severity: BcpConfigurationDiagnosticSeverity;
22
+ code: string;
23
+ message: string;
24
+ }
25
+
26
+ export interface BcpConfigurationDiagnosticsReport {
27
+ ok: boolean;
28
+ mode: string;
29
+ configFile: string | null;
30
+ environmentSchemaFile: string | null;
31
+ resolvedConfig: ResolvedBcpConfig;
32
+ environment: {
33
+ checked: number;
34
+ present: number;
35
+ defaults: number;
36
+ };
37
+ diagnostics: BcpConfigurationDiagnostic[];
38
+ }
39
+
40
+ export interface DiagnoseBcpConfigurationOptions {
41
+ rootDirectory: string;
42
+ mode: string;
43
+ environment?: NodeJS.ProcessEnv;
44
+ overrides?: ResolveBcpConfigOverrides;
45
+ }
46
+
47
+ export async function diagnoseBcpConfiguration(
48
+ options: DiagnoseBcpConfigurationOptions
49
+ ): Promise<BcpConfigurationDiagnosticsReport> {
50
+ const environment =
51
+ options.environment ??
52
+ process.env;
53
+ const resolved =
54
+ await resolveBcpConfig(
55
+ options.rootDirectory,
56
+ options.overrides ?? {},
57
+ environment
58
+ );
59
+ const loadedSchema =
60
+ await loadBcpEnvironmentSchema(
61
+ options.rootDirectory
62
+ );
63
+ const environmentValidation =
64
+ validateEnvironment(
65
+ loadedSchema.schema,
66
+ environment
67
+ );
68
+ const diagnostics:
69
+ BcpConfigurationDiagnostic[] = [];
70
+
71
+ appendEnvironmentDiagnostics(
72
+ diagnostics,
73
+ environmentValidation
74
+ );
75
+ appendRuntimeDiagnostics(
76
+ diagnostics,
77
+ options.mode,
78
+ resolved.config,
79
+ environment
80
+ );
81
+
82
+ return {
83
+ ok:
84
+ !diagnostics.some(
85
+ (diagnostic) =>
86
+ diagnostic.severity ===
87
+ "error"
88
+ ),
89
+ mode:
90
+ options.mode,
91
+ configFile:
92
+ resolved.file
93
+ ? path.basename(
94
+ resolved.file
95
+ )
96
+ : null,
97
+ environmentSchemaFile:
98
+ loadedSchema.file
99
+ ? path.basename(
100
+ loadedSchema.file
101
+ )
102
+ : null,
103
+ resolvedConfig:
104
+ resolved.config,
105
+ environment: {
106
+ checked:
107
+ environmentValidation.checked,
108
+ present:
109
+ environmentValidation.present,
110
+ defaults:
111
+ environmentValidation.defaults,
112
+ },
113
+ diagnostics,
114
+ };
115
+ }
116
+
117
+ export function assertConfigurationDiagnostics(
118
+ report: BcpConfigurationDiagnosticsReport
119
+ ): void {
120
+ if (
121
+ report.ok
122
+ ) {
123
+ return;
124
+ }
125
+
126
+ const errors =
127
+ report.diagnostics
128
+ .filter(
129
+ (diagnostic) =>
130
+ diagnostic.severity ===
131
+ "error"
132
+ )
133
+ .map(
134
+ (diagnostic) =>
135
+ diagnostic.message
136
+ );
137
+
138
+ throw new Error(
139
+ `BCP Configuration Error:\n${errors.map((message) => `- ${message}`).join("\n")}`
140
+ );
141
+ }
142
+
143
+ function appendEnvironmentDiagnostics(
144
+ diagnostics: BcpConfigurationDiagnostic[],
145
+ validation: BcpEnvironmentValidationResult
146
+ ): void {
147
+ for (
148
+ const issue
149
+ of validation.issues
150
+ ) {
151
+ diagnostics.push({
152
+ severity:
153
+ issue.severity,
154
+ code:
155
+ `environment.${issue.code}`,
156
+ message:
157
+ issue.message,
158
+ });
159
+ }
160
+ }
161
+
162
+ function appendRuntimeDiagnostics(
163
+ diagnostics: BcpConfigurationDiagnostic[],
164
+ mode: string,
165
+ config: ResolvedBcpConfig,
166
+ environment: NodeJS.ProcessEnv
167
+ ): void {
168
+ if (
169
+ mode !== "production"
170
+ ) {
171
+ return;
172
+ }
173
+
174
+ if (
175
+ config.build.sourceMaps
176
+ ) {
177
+ diagnostics.push({
178
+ severity: "warning",
179
+ code: "production.source_maps",
180
+ message:
181
+ "Production source maps are enabled. Confirm that exposing source information is intentional.",
182
+ });
183
+ }
184
+
185
+ if (
186
+ config.security.poweredByHeader
187
+ ) {
188
+ diagnostics.push({
189
+ severity: "warning",
190
+ code: "production.powered_by",
191
+ message:
192
+ "security.poweredByHeader is enabled in production.",
193
+ });
194
+ }
195
+
196
+ if (
197
+ config.security.contentSecurityPolicy ===
198
+ false
199
+ ) {
200
+ diagnostics.push({
201
+ severity: "warning",
202
+ code: "production.csp_disabled",
203
+ message:
204
+ "Content-Security-Policy is disabled. Configure a policy when the application can support one.",
205
+ });
206
+ }
207
+
208
+ const trustProxy =
209
+ environment.BCP_TRUST_PROXY
210
+ ?.trim()
211
+ .toLowerCase();
212
+
213
+ if (
214
+ trustProxy === "true" ||
215
+ trustProxy === "1" ||
216
+ trustProxy === "yes" ||
217
+ trustProxy === "on"
218
+ ) {
219
+ diagnostics.push({
220
+ severity: "warning",
221
+ code: "production.trust_proxy",
222
+ message:
223
+ "BCP_TRUST_PROXY is enabled. Ensure untrusted clients cannot bypass the trusted reverse proxy/load balancer.",
224
+ });
225
+ }
226
+ }