@chidchanun/bcp 0.1.9 → 0.1.11

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,130 @@
1
+ # Updating BCP Framework
2
+
3
+ BCP includes a project updater that can move an existing application to a newer published framework version and refresh the active package-manager lockfile.
4
+
5
+ ## First update from 0.1.9 or older
6
+
7
+ BCP versions published before the updater do not know the `update` command. Run the newest CLI explicitly once:
8
+
9
+ ```bash
10
+ npx @chidchanun/bcp@latest update
11
+ ```
12
+
13
+ The updater adds this project script when the application does not already define its own `update` script:
14
+
15
+ ```json
16
+ {
17
+ "scripts": {
18
+ "update": "bcp update"
19
+ }
20
+ }
21
+ ```
22
+
23
+ Normal future updates can therefore use either:
24
+
25
+ ```bash
26
+ bcp update
27
+ ```
28
+
29
+ or, without a global BCP installation:
30
+
31
+ ```bash
32
+ npm run update
33
+ ```
34
+
35
+ ## Update to latest
36
+
37
+ ```bash
38
+ bcp update
39
+ ```
40
+
41
+ This resolves `@chidchanun/bcp@latest` from npm, pins that exact resolved release in `package.json`, detects the project's package manager and runs its install command.
42
+
43
+ For example, if `latest` resolves to `0.1.10`, a generated application becomes:
44
+
45
+ ```json
46
+ {
47
+ "dependencies": {
48
+ "bcp": "npm:@chidchanun/bcp@0.1.10"
49
+ }
50
+ }
51
+ ```
52
+
53
+ The exact pin is intentional. A later plain `npm install` should not silently move the framework to a release that the BCP updater did not explicitly resolve.
54
+
55
+ Generated applications use the dependency key `bcp` with an npm alias to `@chidchanun/bcp`. The updater also recognizes a direct `@chidchanun/bcp` dependency.
56
+
57
+ ## Update to a specific version or dist-tag
58
+
59
+ ```bash
60
+ bcp update 0.1.10
61
+ bcp update next
62
+ ```
63
+
64
+ The target may be an exact published version or an npm dist-tag. The updater always records the exact version that the registry resolves.
65
+
66
+ ## Check without changing files
67
+
68
+ ```bash
69
+ bcp update --check
70
+ ```
71
+
72
+ The command resolves the current `latest` target and prints the installed/declaration state without modifying `package.json` or the lockfile.
73
+
74
+ ## Dry run
75
+
76
+ ```bash
77
+ bcp update 0.1.10 --dry-run
78
+ ```
79
+
80
+ A dry run resolves the target and shows the dependency/script changes without writing files or installing packages.
81
+
82
+ ## Project root
83
+
84
+ Use `--root` when updating a project from another directory:
85
+
86
+ ```bash
87
+ bcp update --root ./apps/admin
88
+ ```
89
+
90
+ ## Package-manager detection
91
+
92
+ BCP detects the package manager from its lockfile:
93
+
94
+ - `package-lock.json` -> npm
95
+ - `pnpm-lock.yaml` -> pnpm
96
+ - `yarn.lock` -> Yarn
97
+ - `bun.lock` / `bun.lockb` -> Bun
98
+
99
+ If no lockfile exists, BCP uses the invoking package-manager user agent when available and otherwise falls back to npm. If lockfiles from multiple package managers are present, the updater stops instead of guessing.
100
+
101
+ Registry version discovery currently uses npm's registry CLI (`npm view`) even when the project itself installs dependencies with pnpm, Yarn or Bun.
102
+
103
+ ## Failure safety
104
+
105
+ Before installation BCP keeps the original `package.json` and detected lockfile in memory. If the package-manager install fails, those files are restored before the command exits with an error.
106
+
107
+ A package manager may still have touched `node_modules` before failing, so run the normal install command again after resolving the underlying package-manager or network problem.
108
+
109
+ ## What the updater changes
110
+
111
+ The updater changes the framework dependency, adds `scripts.update = "bcp update"` when no update script exists, and refreshes the package-manager lockfile. An existing custom `update` script is never overwritten.
112
+
113
+ It does not overwrite application source code, environment files, authentication implementations, database schemas or custom configuration.
114
+
115
+ Features that were originally generator presets, such as JWT auth scaffolding, are not automatically injected into an existing application merely by updating the framework package.
116
+
117
+ ## Recommended verification
118
+
119
+ After an update, run the checks used by your project, for example:
120
+
121
+ ```bash
122
+ npm run typecheck
123
+ npm run build
124
+ ```
125
+
126
+ Commit `package.json` and the lockfile together after verification.
127
+
128
+ ## Release channels
129
+
130
+ Stable BCP releases use the npm `latest` dist-tag. A release maintainer can still publish an explicit pre-release channel with `BCP_DIST_TAG`, for example `BCP_DIST_TAG=next`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "BCP Framework - a React full-stack framework with file-based routing, SSR, APIs, middleware, islands, caching and standalone production builds.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,523 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ fileURLToPath,
5
+ } from "node:url";
6
+
7
+ import {
8
+ build,
9
+ } from "esbuild";
10
+
11
+ import {
12
+ createServerEnvironmentDefines,
13
+ } from "../../env/src/index.js";
14
+
15
+ import type {
16
+ Route,
17
+ } from "../../router/src/index.js";
18
+
19
+ import {
20
+ findPageGuardFiles,
21
+ } from "../../server/src/page-guard.js";
22
+
23
+ import {
24
+ findPageActionFile,
25
+ } from "../../server/src/form-action.js";
26
+
27
+ export async function buildProductionActions(
28
+ pageRoutes: Route[],
29
+ rootDirectory: string,
30
+ serverDirectory: string
31
+ ): Promise<void> {
32
+ const outputFile =
33
+ path.join(
34
+ serverDirectory,
35
+ "actions.mjs"
36
+ );
37
+
38
+ fs.rmSync(
39
+ outputFile,
40
+ {
41
+ force: true,
42
+ }
43
+ );
44
+
45
+ const appDirectory =
46
+ path.join(
47
+ rootDirectory,
48
+ "app"
49
+ );
50
+ const actionRoutes =
51
+ pageRoutes
52
+ .map(
53
+ (route) => ({
54
+ route,
55
+ actionFile:
56
+ findPageActionFile(
57
+ route.filePath
58
+ ),
59
+ guardFiles:
60
+ findPageGuardFiles(
61
+ appDirectory,
62
+ route.filePath
63
+ ),
64
+ })
65
+ )
66
+ .filter(
67
+ (entry) =>
68
+ entry.actionFile !==
69
+ null
70
+ ) as Array<{
71
+ route: Route;
72
+ actionFile: string;
73
+ guardFiles: string[];
74
+ }>;
75
+
76
+ if (
77
+ actionRoutes.length ===
78
+ 0
79
+ ) {
80
+ console.log(
81
+ "[BCP Build] Actions: none"
82
+ );
83
+ return;
84
+ }
85
+
86
+ const entryDirectory =
87
+ path.join(
88
+ rootDirectory,
89
+ ".bcp-framework",
90
+ "build",
91
+ ".action-entry"
92
+ );
93
+ const entryFile =
94
+ path.join(
95
+ entryDirectory,
96
+ "actions-entry.mjs"
97
+ );
98
+
99
+ fs.rmSync(
100
+ entryDirectory,
101
+ {
102
+ recursive: true,
103
+ force: true,
104
+ }
105
+ );
106
+ fs.mkdirSync(
107
+ entryDirectory,
108
+ {
109
+ recursive: true,
110
+ }
111
+ );
112
+
113
+ fs.writeFileSync(
114
+ entryFile,
115
+ createActionEntry(
116
+ actionRoutes,
117
+ rootDirectory,
118
+ entryDirectory
119
+ ),
120
+ "utf8"
121
+ );
122
+
123
+ const frameworkDirectory =
124
+ path.dirname(
125
+ fileURLToPath(
126
+ import.meta.url
127
+ )
128
+ );
129
+ const frameworkServerEntry =
130
+ path.resolve(
131
+ frameworkDirectory,
132
+ "../../client/src/server.ts"
133
+ );
134
+ const frameworkServerOnlyEntry =
135
+ path.resolve(
136
+ frameworkDirectory,
137
+ "../../client/src/server-only.mjs"
138
+ );
139
+ const frameworkCacheEntry =
140
+ path.resolve(
141
+ frameworkDirectory,
142
+ "../../cache/src/index.ts"
143
+ );
144
+
145
+ try {
146
+ await build({
147
+ absWorkingDir:
148
+ rootDirectory,
149
+ entryPoints: [
150
+ entryFile,
151
+ ],
152
+ outfile:
153
+ outputFile,
154
+ bundle:
155
+ true,
156
+ platform:
157
+ "node",
158
+ format:
159
+ "esm",
160
+ target: [
161
+ "node24",
162
+ ],
163
+ minify:
164
+ process.env
165
+ .BCP_BUILD_MINIFY !==
166
+ "0",
167
+ sourcemap:
168
+ process.env
169
+ .BCP_BUILD_SOURCE_MAPS ===
170
+ "1",
171
+ treeShaking:
172
+ true,
173
+ packages:
174
+ "external",
175
+ plugins: [
176
+ {
177
+ name:
178
+ "bcp-action-runtime",
179
+ setup(buildApi) {
180
+ buildApi.onResolve(
181
+ {
182
+ filter:
183
+ /^bcp\/server$/,
184
+ },
185
+ () => ({
186
+ path:
187
+ frameworkServerEntry,
188
+ })
189
+ );
190
+ buildApi.onResolve(
191
+ {
192
+ filter:
193
+ /^bcp\/server-only$/,
194
+ },
195
+ () => ({
196
+ path:
197
+ frameworkServerOnlyEntry,
198
+ })
199
+ );
200
+ buildApi.onResolve(
201
+ {
202
+ filter:
203
+ /^bcp\/cache$/,
204
+ },
205
+ () => ({
206
+ path:
207
+ frameworkCacheEntry,
208
+ })
209
+ );
210
+ },
211
+ },
212
+ ],
213
+ define:
214
+ createServerEnvironmentDefines(
215
+ "production"
216
+ ),
217
+ legalComments:
218
+ "none",
219
+ logLevel:
220
+ "silent",
221
+ });
222
+ } finally {
223
+ fs.rmSync(
224
+ entryDirectory,
225
+ {
226
+ recursive: true,
227
+ force: true,
228
+ }
229
+ );
230
+ }
231
+
232
+ console.log(
233
+ `[BCP Build] Actions: ${actionRoutes.length} route(s) -> ${path.relative(rootDirectory, outputFile).replace(/\\/g, "/")}`
234
+ );
235
+ }
236
+
237
+ function createActionEntry(
238
+ actionRoutes: Array<{
239
+ route: Route;
240
+ actionFile: string;
241
+ guardFiles: string[];
242
+ }>,
243
+ rootDirectory: string,
244
+ entryDirectory: string
245
+ ): string {
246
+ const frameworkDirectory =
247
+ path.dirname(
248
+ fileURLToPath(
249
+ import.meta.url
250
+ )
251
+ );
252
+ const actionRuntime =
253
+ path.resolve(
254
+ frameworkDirectory,
255
+ "../../server/src/form-action.ts"
256
+ );
257
+ const actionTransportRuntime =
258
+ path.resolve(
259
+ frameworkDirectory,
260
+ "../../server/src/form-action-transport.ts"
261
+ );
262
+ const guardRuntime =
263
+ path.resolve(
264
+ frameworkDirectory,
265
+ "../../server/src/page-guard.ts"
266
+ );
267
+ const requestContextRuntime =
268
+ path.resolve(
269
+ frameworkDirectory,
270
+ "../../server/src/request-context.ts"
271
+ );
272
+ const cacheRuntime =
273
+ path.resolve(
274
+ frameworkDirectory,
275
+ "../../cache/src/index.ts"
276
+ );
277
+ const routerRuntime =
278
+ path.resolve(
279
+ frameworkDirectory,
280
+ "../../router/src/index.ts"
281
+ );
282
+
283
+ const imports: string[] = [
284
+ `import { assertPageActionName, executePageActionFunction, normalizePageActionMethod } from ${JSON.stringify(toImportSpecifier(entryDirectory, actionRuntime))};`,
285
+ `import { completePageActionExecution } from ${JSON.stringify(toImportSpecifier(entryDirectory, actionTransportRuntime))};`,
286
+ `import { executePageGuardFunctions } from ${JSON.stringify(toImportSpecifier(entryDirectory, guardRuntime))};`,
287
+ `import { runWithRequestContext } from ${JSON.stringify(toImportSpecifier(entryDirectory, requestContextRuntime))};`,
288
+ `import { runWithRequestCacheContext } from ${JSON.stringify(toImportSpecifier(entryDirectory, cacheRuntime))};`,
289
+ `import { matchRoute } from ${JSON.stringify(toImportSpecifier(entryDirectory, routerRuntime))};`,
290
+ ];
291
+ const guardNameByFile =
292
+ new Map<string, string>();
293
+ let guardIndex =
294
+ 0;
295
+
296
+ actionRoutes.forEach(
297
+ (entry, index) => {
298
+ imports.push(
299
+ `import * as Actions${index} from ${JSON.stringify(toImportSpecifier(entryDirectory, entry.actionFile))};`
300
+ );
301
+
302
+ for (const guardFile of entry.guardFiles) {
303
+ if (
304
+ guardNameByFile.has(
305
+ guardFile
306
+ )
307
+ ) {
308
+ continue;
309
+ }
310
+
311
+ const guardName =
312
+ `Guard${guardIndex++}`;
313
+ guardNameByFile.set(
314
+ guardFile,
315
+ guardName
316
+ );
317
+ imports.push(
318
+ `import { guard as ${guardName} } from ${JSON.stringify(toImportSpecifier(entryDirectory, guardFile))};`
319
+ );
320
+ }
321
+ }
322
+ );
323
+
324
+ const definitions =
325
+ actionRoutes.map(
326
+ (entry, index) => {
327
+ const guards =
328
+ entry.guardFiles.map(
329
+ (guardFile) => {
330
+ const guardName =
331
+ guardNameByFile.get(
332
+ guardFile
333
+ );
334
+
335
+ if (!guardName) {
336
+ throw new Error(
337
+ `BCP Framework: action guard import was not generated for "${guardFile}".`
338
+ );
339
+ }
340
+
341
+ return `{
342
+ filePath: ${JSON.stringify(path.relative(rootDirectory, guardFile).replace(/\\/g, "/"))},
343
+ guard: ${guardName}
344
+ }`;
345
+ }
346
+ );
347
+
348
+ return `{
349
+ pathname: ${JSON.stringify(entry.route.pathname)},
350
+ segments: ${JSON.stringify(entry.route.segments)},
351
+ actions: Actions${index},
352
+ actionFile: ${JSON.stringify(path.relative(rootDirectory, entry.actionFile).replace(/\\/g, "/"))},
353
+ guards: [${guards.join(", ")}]
354
+ }`;
355
+ }
356
+ );
357
+
358
+ return `${imports.join("\n")}
359
+
360
+ const definitions = [
361
+ ${definitions.join(",\n ")}
362
+ ];
363
+
364
+ const routePatterns =
365
+ definitions.map(
366
+ (definition) => ({
367
+ pathname: definition.pathname,
368
+ segments: definition.segments,
369
+ filePath: definition.pathname,
370
+ layouts: []
371
+ })
372
+ );
373
+
374
+ const definitionByPathname =
375
+ new Map(
376
+ definitions.map(
377
+ (definition) => [
378
+ definition.pathname,
379
+ definition
380
+ ]
381
+ )
382
+ );
383
+
384
+ export const actionPathnames =
385
+ definitions.map(
386
+ (definition) =>
387
+ definition.pathname
388
+ );
389
+
390
+ export async function evaluateRouteAction(input) {
391
+ assertPageActionName(
392
+ input.name
393
+ );
394
+
395
+ const target =
396
+ new URL(
397
+ input.url
398
+ );
399
+ const match =
400
+ matchRoute(
401
+ routePatterns,
402
+ target.pathname
403
+ );
404
+
405
+ if (!match) {
406
+ return null;
407
+ }
408
+
409
+ const definition =
410
+ definitionByPathname.get(
411
+ match.route.pathname
412
+ );
413
+
414
+ if (!definition) {
415
+ return null;
416
+ }
417
+
418
+ const candidate =
419
+ definition.actions[
420
+ input.name
421
+ ];
422
+
423
+ if (
424
+ typeof candidate !==
425
+ "function"
426
+ ) {
427
+ return null;
428
+ }
429
+
430
+ const method =
431
+ normalizePageActionMethod(
432
+ input.method
433
+ );
434
+ const request =
435
+ new Request(
436
+ target,
437
+ {
438
+ method,
439
+ headers:
440
+ new Headers(
441
+ input.headers ||
442
+ []
443
+ )
444
+ }
445
+ );
446
+
447
+ return runWithRequestContext(
448
+ request,
449
+ () =>
450
+ runWithRequestCacheContext(
451
+ target.pathname,
452
+ async () => {
453
+ const guardExecution =
454
+ await executePageGuardFunctions(
455
+ definition.guards,
456
+ match.params,
457
+ target
458
+ );
459
+
460
+ if (guardExecution.response) {
461
+ return completePageActionExecution({
462
+ hasGuard:
463
+ guardExecution.hasGuard,
464
+ guardData:
465
+ guardExecution.data,
466
+ data:
467
+ null,
468
+ response:
469
+ guardExecution.response
470
+ });
471
+ }
472
+
473
+ const execution =
474
+ await executePageActionFunction(
475
+ candidate,
476
+ input.name,
477
+ method,
478
+ input.formData,
479
+ match.params,
480
+ target,
481
+ guardExecution.data,
482
+ guardExecution.hasGuard,
483
+ definition.actionFile
484
+ );
485
+
486
+ return completePageActionExecution(
487
+ execution
488
+ );
489
+ }
490
+ ),
491
+ {
492
+ remoteAddress:
493
+ input.remoteAddress ||
494
+ null
495
+ }
496
+ );
497
+ }
498
+ `;
499
+ }
500
+
501
+ function toImportSpecifier(
502
+ fromDirectory: string,
503
+ filePath: string
504
+ ): string {
505
+ let relative =
506
+ path.relative(
507
+ fromDirectory,
508
+ filePath
509
+ )
510
+ .replace(
511
+ /\\/g,
512
+ "/"
513
+ );
514
+
515
+ if (
516
+ !relative.startsWith(".")
517
+ ) {
518
+ relative =
519
+ `./${relative}`;
520
+ }
521
+
522
+ return relative;
523
+ }