@chidchanun/bcp 0.1.22 → 0.1.24

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,12 +2,64 @@
2
2
 
3
3
  BCP Framework 0.1.22 adds project diagnostics and runtime inspection commands for debugging a BCP application without starting the development server.
4
4
 
5
+ ## CLI executable names
6
+
7
+ The published BCP package exposes two executable names that point to the same CLI:
8
+
9
+ ```text
10
+ bcp
11
+ bcp-framework
12
+ ```
13
+
14
+ `bcp` remains the primary and backward-compatible executable name inside npm scripts.
15
+
16
+ On Windows, Microsoft SQL Server also installs a native utility named `bcp.exe`. When that executable appears earlier on `PATH`, typing `bcp` directly in PowerShell can launch the SQL Server utility instead of BCP Framework.
17
+
18
+ PowerShell also does not add an application's `node_modules/.bin` directory to its normal interactive `PATH`. Therefore the recommended interactive Windows invocation is the project-local executable through npm:
19
+
20
+ ```powershell
21
+ npm exec -- bcp-framework doctor
22
+ npm exec -- bcp-framework inspect
23
+ npm exec -- bcp-framework dev
24
+ npm exec -- bcp-framework build
25
+ npm exec -- bcp-framework update
26
+ ```
27
+
28
+ Or call the generated Windows shim explicitly:
29
+
30
+ ```powershell
31
+ .\node_modules\.bin\bcp-framework.cmd doctor
32
+ ```
33
+
34
+ BCP application npm scripts can continue using `bcp dev`, `bcp build`, and other existing commands because npm places the project's `node_modules/.bin` directory on the script `PATH` before system executables.
35
+
36
+ ### Framework source checkout
37
+
38
+ The framework source repository itself is a private development workspace and is not an installed copy of the published package. Merely opening PowerShell in `D:\bcp-framework` does not make `bcp-framework` available on `PATH`.
39
+
40
+ `bcp doctor` and `bcp inspect` are application diagnostics: they expect an application root with `app/` plus an installed BCP dependency. Do not use them as the release-health command for the framework source checkout.
41
+
42
+ For framework development and release validation, use:
43
+
44
+ ```powershell
45
+ npm run typecheck
46
+ npm run test:unit
47
+ npm run rc:check
48
+ ```
49
+
50
+ After a packed or published package is installed into an application such as `bcp-docs`, run:
51
+
52
+ ```powershell
53
+ npm exec -- bcp-framework doctor
54
+ npm exec -- bcp-framework inspect
55
+ ```
56
+
5
57
  ## `bcp doctor`
6
58
 
7
- Run a health check from the application root:
59
+ Run a health check from an installed application root. On Windows PowerShell, prefer:
8
60
 
9
- ```bash
10
- bcp doctor
61
+ ```powershell
62
+ npm exec -- bcp-framework doctor
11
63
  ```
12
64
 
13
65
  The command checks:
@@ -54,12 +106,29 @@ Invalid hook call. Hooks can only be called inside of the body of a function com
54
106
 
55
107
  For local release testing, install the packed `.tgz` artifact instead of linking `.package/bcp` directly into another project. A packed package lets the BCP peer dependencies resolve from the application's normal `node_modules` tree.
56
108
 
109
+ ### Duplicate BCP package protection
110
+
111
+ A BCP application must not load `node_modules/bcp` and `node_modules/@chidchanun/bcp` as two separate framework copies.
112
+
113
+ The CLI performs a startup preflight. If both package roots exist, runtime commands such as `dev`, `build`, and `start` stop before SSR starts. Two framework copies create separate React contexts, which can make APIs such as `useLoaderData()` incorrectly report missing loader data even when the route loader executed successfully.
114
+
115
+ For local release verification, install the tarball under the existing `bcp` dependency key instead of adding the scoped package beside it:
116
+
117
+ ```powershell
118
+ npm install `
119
+ --no-save `
120
+ --package-lock=false `
121
+ "bcp@file:D:\bcp-framework\.package\artifacts\chidchanun-bcp-0.1.22.tgz"
122
+ ```
123
+
124
+ The installed folder remains `node_modules/bcp` even when the package inside the tarball is published as `@chidchanun/bcp`.
125
+
57
126
  ## `bcp inspect`
58
127
 
59
128
  Use `inspect` when you want a deterministic snapshot of the project inputs BCP sees:
60
129
 
61
- ```bash
62
- bcp inspect
130
+ ```powershell
131
+ npm exec -- bcp-framework inspect
63
132
  ```
64
133
 
65
134
  It prints:
@@ -78,16 +147,16 @@ It prints:
78
147
 
79
148
  Both commands support machine-readable output:
80
149
 
81
- ```bash
82
- bcp doctor --json
83
- bcp inspect --json
150
+ ```powershell
151
+ npm exec -- bcp-framework doctor --json
152
+ npm exec -- bcp-framework inspect --json
84
153
  ```
85
154
 
86
- You can also target another project directory:
155
+ You can also target another installed BCP application directory:
87
156
 
88
- ```bash
89
- bcp doctor --root ../my-app --json
90
- bcp inspect --root ../my-app --json
157
+ ```powershell
158
+ npm exec -- bcp-framework doctor --root ..\my-app --json
159
+ npm exec -- bcp-framework inspect --root ..\my-app --json
91
160
  ```
92
161
 
93
162
  `--json` is intentionally limited to `doctor` and `inspect` so other CLI commands keep their existing human-oriented output contracts.
@@ -115,4 +184,6 @@ A simple project-health gate can use:
115
184
  bcp doctor --json > bcp-doctor.json
116
185
  ```
117
186
 
187
+ Inside npm scripts or CI package-script execution, npm already places `node_modules/.bin` on `PATH`.
188
+
118
189
  The command exits non-zero when blocking checks fail, while the JSON report remains available for CI logs or artifacts.
@@ -1,13 +1,241 @@
1
- # Development request logging
1
+ # Logging and observability
2
2
 
3
- BCP keeps development request output focused on application API traffic.
3
+ BCP Framework 0.1.23 adds a structured server logger and upgrades development request timing output while keeping framework-internal browser/bootstrap traffic quiet by default.
4
4
 
5
- By default, request-style console lines are shown for application API routes such as:
5
+ ## Server logger
6
+
7
+ Import logging APIs from the server-only `bcp/server` entry:
8
+
9
+ ```ts
10
+ import {
11
+ logger,
12
+ } from "bcp/server";
13
+
14
+ logger.info(
15
+ "Application started",
16
+ {
17
+ service: "catalog",
18
+ }
19
+ );
20
+ ```
21
+
22
+ Available levels are:
23
+
24
+ ```text
25
+ debug
26
+ info
27
+ warn
28
+ error
29
+ silent
30
+ ```
31
+
32
+ The default level is `info`.
33
+
34
+ Structured fields can contain strings, numbers, booleans, objects, arrays, `Error` values and `bigint` values. Circular references are replaced with `[Circular]` rather than crashing the logger.
35
+
36
+ ## Request-scoped logging
37
+
38
+ Use `requestLogger()` while handling an API route, loader, guard or form action:
39
+
40
+ ```ts
41
+ import {
42
+ requestLogger,
43
+ } from "bcp/server";
44
+
45
+ export async function loader() {
46
+ const log =
47
+ await requestLogger({
48
+ feature: "categories",
49
+ });
50
+
51
+ log.info(
52
+ "Loading categories"
53
+ );
54
+
55
+ return {
56
+ items: [],
57
+ };
58
+ }
59
+ ```
60
+
61
+ The returned child logger automatically binds the current request identity:
62
+
63
+ ```text
64
+ requestId
65
+ method
66
+ path
67
+ ```
68
+
69
+ `requestId` uses the same request-scoped value exposed by `requestId()`: a valid incoming `X-Request-Id` is reused, otherwise BCP creates a UUID for the request.
70
+
71
+ ## Response request ID
72
+
73
+ When an application wants to return the request identity to a caller, wrap a response with `attachRequestId()`:
74
+
75
+ ```ts
76
+ import {
77
+ attachRequestId,
78
+ json,
79
+ } from "bcp/server";
80
+
81
+ export async function GET() {
82
+ return attachRequestId(
83
+ json({
84
+ ok: true,
85
+ })
86
+ );
87
+ }
88
+ ```
89
+
90
+ The returned response includes:
6
91
 
7
92
  ```text
8
- GET /api/users 200 1.24ms
93
+ X-Request-Id: <current request id>
94
+ ```
95
+
96
+ This helper preserves the original response status, status text, body and headers.
97
+
98
+ ## Child loggers
99
+
100
+ Bind shared fields once with `child()`:
101
+
102
+ ```ts
103
+ import {
104
+ logger,
105
+ } from "bcp/server";
106
+
107
+ const dbLog =
108
+ logger.child({
109
+ component: "database",
110
+ });
111
+
112
+ dbLog.debug(
113
+ "Executing query",
114
+ {
115
+ operation: "users.list",
116
+ }
117
+ );
9
118
  ```
10
119
 
11
- BCP suppresses framework and browser bootstrap traffic such as `/_bcp/*`, page/document requests, public static assets, and the generated Tailwind stylesheet endpoint `/api/bcp-styles`.
120
+ Child bindings are inherited by further child loggers.
121
+
122
+ ## Log level
123
+
124
+ Set the minimum log level with:
125
+
126
+ ```bash
127
+ BCP_LOG_LEVEL=debug
128
+ ```
129
+
130
+ Supported values:
131
+
132
+ ```text
133
+ debug
134
+ info
135
+ warn
136
+ error
137
+ silent
138
+ ```
139
+
140
+ Invalid or missing values fall back to `info`.
141
+
142
+ Use `debug` during framework/application diagnostics to see development SSR and module-import timing events.
143
+
144
+ ## Log format
12
145
 
13
- Build, SSR, Fast Refresh, watcher and error diagnostics are not affected by this filter.
146
+ Human-readable output is the default:
147
+
148
+ ```bash
149
+ BCP_LOG_FORMAT=pretty
150
+ ```
151
+
152
+ Example:
153
+
154
+ ```text
155
+ [2026-08-27T10:00:00.000Z] [INFO] [bcp] User loaded userId=42
156
+ ```
157
+
158
+ For containers, log collectors and CI systems use JSON lines:
159
+
160
+ ```bash
161
+ BCP_LOG_FORMAT=json
162
+ ```
163
+
164
+ Example:
165
+
166
+ ```json
167
+ {"timestamp":"2026-08-27T10:00:00.000Z","level":"info","logger":"bcp","message":"User loaded","userId":42}
168
+ ```
169
+
170
+ Each log call emits exactly one line.
171
+
172
+ ## Development HTTP logging
173
+
174
+ Development request output remains focused on application API traffic. BCP suppresses request-style logs for framework and browser bootstrap traffic such as:
175
+
176
+ ```text
177
+ /_bcp/*
178
+ /api/bcp-styles
179
+ page/document requests
180
+ public static assets
181
+ ```
182
+
183
+ An application API request is converted into the structured logger format. With pretty output it resembles:
184
+
185
+ ```text
186
+ [2026-08-27T10:00:00.000Z] [INFO] [bcp-dev] HTTP request event=http.request method=GET path=/api/users status=200 durationMs=1.24
187
+ ```
188
+
189
+ With `BCP_LOG_FORMAT=json` the same event is emitted as one JSON object containing:
190
+
191
+ ```text
192
+ event=http.request
193
+ method
194
+ path
195
+ status
196
+ durationMs
197
+ ```
198
+
199
+ Existing development `Import: ...ms` and `SSR: ...ms` measurements are converted into `debug` events:
200
+
201
+ ```text
202
+ module.import
203
+ ssr.render
204
+ ```
205
+
206
+ They are therefore visible when `BCP_LOG_LEVEL=debug` and stay out of normal `info` output.
207
+
208
+ ## Errors
209
+
210
+ Pass an `Error` as a structured field:
211
+
212
+ ```ts
213
+ try {
214
+ // ...
215
+ } catch (error) {
216
+ logger.error(
217
+ "Operation failed",
218
+ {
219
+ error,
220
+ }
221
+ );
222
+ }
223
+ ```
224
+
225
+ `Error` values are serialized with their name, message, stack and cause when available.
226
+
227
+ ## Environment example
228
+
229
+ A development `.env.local` can use:
230
+
231
+ ```text
232
+ BCP_LOG_LEVEL=debug
233
+ BCP_LOG_FORMAT=pretty
234
+ ```
235
+
236
+ A production/container environment can use:
237
+
238
+ ```text
239
+ BCP_LOG_LEVEL=info
240
+ BCP_LOG_FORMAT=json
241
+ ```
@@ -0,0 +1,280 @@
1
+ # File Upload
2
+
3
+ BCP Framework 0.1.24 adds server-only multipart and file-storage helpers through `bcp/server`.
4
+
5
+ ## Basic API route
6
+
7
+ ```ts
8
+ import {
9
+ parseMultipartFormData,
10
+ requireUploadedFile,
11
+ saveUploadedFile,
12
+ } from "bcp/server";
13
+
14
+ export async function POST(
15
+ request: Request
16
+ ) {
17
+ const formData =
18
+ await parseMultipartFormData(
19
+ request,
20
+ {
21
+ maxBytes:
22
+ 8 * 1024 * 1024,
23
+ }
24
+ );
25
+
26
+ const file =
27
+ requireUploadedFile(
28
+ formData,
29
+ "file",
30
+ {
31
+ maxBytes:
32
+ 5 * 1024 * 1024,
33
+ allowedTypes: [
34
+ "image/png",
35
+ "image/jpeg",
36
+ "image/webp",
37
+ ],
38
+ allowedExtensions: [
39
+ ".png",
40
+ ".jpg",
41
+ ".jpeg",
42
+ ".webp",
43
+ ],
44
+ }
45
+ );
46
+
47
+ const saved =
48
+ await saveUploadedFile(
49
+ file,
50
+ {
51
+ directory:
52
+ "./uploads",
53
+ }
54
+ );
55
+
56
+ return Response.json({
57
+ fileName:
58
+ saved.fileName,
59
+ size:
60
+ saved.size,
61
+ checksumSha256:
62
+ saved.checksumSha256,
63
+ });
64
+ }
65
+ ```
66
+
67
+ ## Request body limit
68
+
69
+ BCP's security gateway enforces the framework-wide request body limit before the request reaches the application server.
70
+
71
+ The default is 1 MiB. Applications that accept larger uploads must explicitly raise it:
72
+
73
+ ```ts
74
+ // bcp.config.ts
75
+ import {
76
+ defineConfig,
77
+ } from "bcp/config";
78
+
79
+ export default defineConfig({
80
+ server: {
81
+ bodyLimit:
82
+ 10 * 1024 * 1024,
83
+ },
84
+ });
85
+ ```
86
+
87
+ The same setting can be supplied with `BCP_BODY_LIMIT`.
88
+
89
+ Keep `server.bodyLimit` greater than or equal to the multipart limit used by `parseMultipartFormData()`.
90
+
91
+ ## Multipart parsing
92
+
93
+ ```ts
94
+ const formData =
95
+ await parseMultipartFormData(
96
+ request,
97
+ {
98
+ maxBytes:
99
+ 10 * 1024 * 1024,
100
+ }
101
+ );
102
+ ```
103
+
104
+ The helper:
105
+
106
+ - requires `multipart/form-data`,
107
+ - rejects a declared `Content-Length` above `maxBytes`,
108
+ - validates the parsed form payload size,
109
+ - throws `UploadError` with HTTP-oriented status information on validation failures.
110
+
111
+ `maxBytes` is optional. The framework-wide `server.bodyLimit` remains the outer request limit.
112
+
113
+ ## Reading files
114
+
115
+ Use `getUploadedFile()` for optional files:
116
+
117
+ ```ts
118
+ const avatar =
119
+ getUploadedFile(
120
+ formData,
121
+ "avatar"
122
+ );
123
+
124
+ if (!avatar) {
125
+ // no file supplied
126
+ }
127
+ ```
128
+
129
+ Use `requireUploadedFile()` when a file is mandatory:
130
+
131
+ ```ts
132
+ const avatar =
133
+ requireUploadedFile(
134
+ formData,
135
+ "avatar"
136
+ );
137
+ ```
138
+
139
+ A missing required file throws `UploadError` with code `FILE_REQUIRED` and status `400`.
140
+
141
+ ## File constraints
142
+
143
+ `getUploadedFile()`, `requireUploadedFile()` and `saveUploadedFile()` can validate:
144
+
145
+ ```ts
146
+ {
147
+ maxBytes: 5 * 1024 * 1024,
148
+ allowedTypes: [
149
+ "application/pdf",
150
+ ],
151
+ allowedExtensions: [
152
+ ".pdf",
153
+ ],
154
+ }
155
+ ```
156
+
157
+ Supported checks are:
158
+
159
+ - maximum file size,
160
+ - MIME type allowlist,
161
+ - extension allowlist.
162
+
163
+ MIME type and file extension are metadata supplied with the upload. They are useful validation signals but are not content-signature verification. Applications accepting security-sensitive formats should inspect the file content or magic bytes before trusting the file type.
164
+
165
+ ## Saving files
166
+
167
+ ```ts
168
+ const saved =
169
+ await saveUploadedFile(
170
+ file,
171
+ {
172
+ directory:
173
+ "./uploads/documents",
174
+ }
175
+ );
176
+ ```
177
+
178
+ When `fileName` is omitted, BCP generates a UUID-based storage name while preserving a short sanitized extension.
179
+
180
+ The result contains:
181
+
182
+ ```ts
183
+ {
184
+ originalName: string;
185
+ fileName: string;
186
+ path: string;
187
+ type: string;
188
+ size: number;
189
+ checksumSha256: string;
190
+ }
191
+ ```
192
+
193
+ Files are created with exclusive-write behavior by default. An existing destination produces `FILE_EXISTS` instead of silently overwriting data.
194
+
195
+ Use `overwrite: true` only when replacement is intentional:
196
+
197
+ ```ts
198
+ await saveUploadedFile(
199
+ file,
200
+ {
201
+ directory:
202
+ "./uploads",
203
+ fileName:
204
+ "avatar.webp",
205
+ overwrite:
206
+ true,
207
+ }
208
+ );
209
+ ```
210
+
211
+ ## File-name safety
212
+
213
+ `saveUploadedFile()` sanitizes the final file name and verifies that the resolved destination remains inside the configured directory.
214
+
215
+ You can use the same sanitizer independently:
216
+
217
+ ```ts
218
+ import {
219
+ sanitizeUploadFileName,
220
+ } from "bcp/server";
221
+
222
+ const safeName =
223
+ sanitizeUploadFileName(
224
+ userFileName
225
+ );
226
+ ```
227
+
228
+ Path separators, control characters and operating-system-invalid filename characters are removed or replaced.
229
+
230
+ ## Upload errors
231
+
232
+ ```ts
233
+ import {
234
+ UploadError,
235
+ } from "bcp/server";
236
+
237
+ try {
238
+ // upload work
239
+ } catch (error) {
240
+ if (
241
+ error instanceof UploadError
242
+ ) {
243
+ return Response.json(
244
+ {
245
+ error: {
246
+ code:
247
+ error.code,
248
+ message:
249
+ error.message,
250
+ },
251
+ },
252
+ {
253
+ status:
254
+ error.status,
255
+ }
256
+ );
257
+ }
258
+
259
+ throw error;
260
+ }
261
+ ```
262
+
263
+ Current error codes include:
264
+
265
+ ```text
266
+ INVALID_MULTIPART
267
+ UPLOAD_TOO_LARGE
268
+ FILE_REQUIRED
269
+ FILE_TOO_LARGE
270
+ FILE_TYPE_NOT_ALLOWED
271
+ FILE_EXTENSION_NOT_ALLOWED
272
+ INVALID_FILE_NAME
273
+ FILE_EXISTS
274
+ ```
275
+
276
+ ## Storage model
277
+
278
+ The 0.1.24 foundation writes to the local filesystem. It does not provide S3/object-storage adapters, multipart streaming directly to object storage, virus scanning or image transcoding yet.
279
+
280
+ For horizontally scaled applications, use shared/object storage rather than relying on instance-local disk.