@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,497 @@
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
+ export async function buildProductionGuards(
24
+ pageRoutes: Route[],
25
+ rootDirectory: string,
26
+ serverDirectory: string
27
+ ): Promise<void> {
28
+ const outputFile =
29
+ path.join(
30
+ serverDirectory,
31
+ "guards.mjs"
32
+ );
33
+
34
+ fs.rmSync(
35
+ outputFile,
36
+ {
37
+ force: true,
38
+ }
39
+ );
40
+
41
+ const appDirectory =
42
+ path.join(
43
+ rootDirectory,
44
+ "app"
45
+ );
46
+ const guardedRoutes =
47
+ pageRoutes
48
+ .map(
49
+ (route) => ({
50
+ route,
51
+ guardFiles:
52
+ findPageGuardFiles(
53
+ appDirectory,
54
+ route.filePath
55
+ ),
56
+ })
57
+ )
58
+ .filter(
59
+ (entry) =>
60
+ entry.guardFiles.length > 0
61
+ );
62
+
63
+ if (
64
+ guardedRoutes.length ===
65
+ 0
66
+ ) {
67
+ console.log(
68
+ "[BCP Build] Guards: none"
69
+ );
70
+ return;
71
+ }
72
+
73
+ const entryDirectory =
74
+ path.join(
75
+ rootDirectory,
76
+ ".bcp-framework",
77
+ "build",
78
+ ".guard-entry"
79
+ );
80
+ const entryFile =
81
+ path.join(
82
+ entryDirectory,
83
+ "guards-entry.mjs"
84
+ );
85
+
86
+ fs.rmSync(
87
+ entryDirectory,
88
+ {
89
+ recursive: true,
90
+ force: true,
91
+ }
92
+ );
93
+ fs.mkdirSync(
94
+ entryDirectory,
95
+ {
96
+ recursive: true,
97
+ }
98
+ );
99
+
100
+ fs.writeFileSync(
101
+ entryFile,
102
+ createGuardEntry(
103
+ guardedRoutes,
104
+ rootDirectory,
105
+ entryDirectory
106
+ ),
107
+ "utf8"
108
+ );
109
+
110
+ const frameworkDirectory =
111
+ path.dirname(
112
+ fileURLToPath(
113
+ import.meta.url
114
+ )
115
+ );
116
+ const frameworkServerEntry =
117
+ path.resolve(
118
+ frameworkDirectory,
119
+ "../../client/src/server.ts"
120
+ );
121
+ const frameworkServerOnlyEntry =
122
+ path.resolve(
123
+ frameworkDirectory,
124
+ "../../client/src/server-only.mjs"
125
+ );
126
+ const frameworkCacheEntry =
127
+ path.resolve(
128
+ frameworkDirectory,
129
+ "../../cache/src/index.ts"
130
+ );
131
+
132
+ try {
133
+ await build({
134
+ absWorkingDir:
135
+ rootDirectory,
136
+ entryPoints: [
137
+ entryFile,
138
+ ],
139
+ outfile:
140
+ outputFile,
141
+ bundle:
142
+ true,
143
+ platform:
144
+ "node",
145
+ format:
146
+ "esm",
147
+ target: [
148
+ "node24",
149
+ ],
150
+ minify:
151
+ process.env
152
+ .BCP_BUILD_MINIFY !==
153
+ "0",
154
+ sourcemap:
155
+ process.env
156
+ .BCP_BUILD_SOURCE_MAPS ===
157
+ "1",
158
+ treeShaking:
159
+ true,
160
+ packages:
161
+ "external",
162
+ plugins: [
163
+ {
164
+ name:
165
+ "bcp-guard-runtime",
166
+ setup(buildApi) {
167
+ buildApi.onResolve(
168
+ {
169
+ filter:
170
+ /^bcp\/server$/,
171
+ },
172
+ () => ({
173
+ path:
174
+ frameworkServerEntry,
175
+ })
176
+ );
177
+ buildApi.onResolve(
178
+ {
179
+ filter:
180
+ /^bcp\/server-only$/,
181
+ },
182
+ () => ({
183
+ path:
184
+ frameworkServerOnlyEntry,
185
+ })
186
+ );
187
+ buildApi.onResolve(
188
+ {
189
+ filter:
190
+ /^bcp\/cache$/,
191
+ },
192
+ () => ({
193
+ path:
194
+ frameworkCacheEntry,
195
+ })
196
+ );
197
+ },
198
+ },
199
+ ],
200
+ define:
201
+ createServerEnvironmentDefines(
202
+ "production"
203
+ ),
204
+ legalComments:
205
+ "none",
206
+ logLevel:
207
+ "silent",
208
+ });
209
+ } finally {
210
+ fs.rmSync(
211
+ entryDirectory,
212
+ {
213
+ recursive: true,
214
+ force: true,
215
+ }
216
+ );
217
+ }
218
+
219
+ console.log(
220
+ `[BCP Build] Guards: ${guardedRoutes.length} protected route(s) -> ${path.relative(rootDirectory, outputFile).replace(/\\/g, "/")}`
221
+ );
222
+ }
223
+
224
+ function createGuardEntry(
225
+ guardedRoutes: Array<{
226
+ route: Route;
227
+ guardFiles: string[];
228
+ }>,
229
+ rootDirectory: string,
230
+ entryDirectory: string
231
+ ): string {
232
+ const frameworkDirectory =
233
+ path.dirname(
234
+ fileURLToPath(
235
+ import.meta.url
236
+ )
237
+ );
238
+ const pageGuardRuntime =
239
+ path.resolve(
240
+ frameworkDirectory,
241
+ "../../server/src/page-guard.ts"
242
+ );
243
+ const requestContextRuntime =
244
+ path.resolve(
245
+ frameworkDirectory,
246
+ "../../server/src/request-context.ts"
247
+ );
248
+ const routerRuntime =
249
+ path.resolve(
250
+ frameworkDirectory,
251
+ "../../router/src/index.ts"
252
+ );
253
+
254
+ const imports: string[] = [
255
+ `import { executePageGuardFunctions } from ${JSON.stringify(
256
+ toImportSpecifier(
257
+ entryDirectory,
258
+ pageGuardRuntime
259
+ )
260
+ )};`,
261
+ `import { applyResponseCookies, runWithRequestContext } from ${JSON.stringify(
262
+ toImportSpecifier(
263
+ entryDirectory,
264
+ requestContextRuntime
265
+ )
266
+ )};`,
267
+ `import { matchRoute } from ${JSON.stringify(
268
+ toImportSpecifier(
269
+ entryDirectory,
270
+ routerRuntime
271
+ )
272
+ )};`,
273
+ ];
274
+ const guardNameByFile =
275
+ new Map<string, string>();
276
+ let guardIndex =
277
+ 0;
278
+
279
+ for (const entry of guardedRoutes) {
280
+ for (const guardFile of entry.guardFiles) {
281
+ if (
282
+ guardNameByFile.has(
283
+ guardFile
284
+ )
285
+ ) {
286
+ continue;
287
+ }
288
+
289
+ const guardName =
290
+ `Guard${guardIndex++}`;
291
+ guardNameByFile.set(
292
+ guardFile,
293
+ guardName
294
+ );
295
+ imports.push(
296
+ `import { guard as ${guardName} } from ${JSON.stringify(
297
+ toImportSpecifier(
298
+ entryDirectory,
299
+ guardFile
300
+ )
301
+ )};`
302
+ );
303
+ }
304
+ }
305
+
306
+ const definitions =
307
+ guardedRoutes.map(
308
+ ({
309
+ route,
310
+ guardFiles,
311
+ }) => {
312
+ const guards =
313
+ guardFiles.map(
314
+ (guardFile) => {
315
+ const guardName =
316
+ guardNameByFile.get(
317
+ guardFile
318
+ );
319
+
320
+ if (!guardName) {
321
+ throw new Error(
322
+ `BCP Framework: guard import was not generated for "${guardFile}".`
323
+ );
324
+ }
325
+
326
+ const label =
327
+ path.relative(
328
+ rootDirectory,
329
+ guardFile
330
+ )
331
+ .replace(
332
+ /\\/g,
333
+ "/"
334
+ );
335
+
336
+ return `{
337
+ filePath: ${JSON.stringify(label)},
338
+ guard: ${guardName}
339
+ }`;
340
+ }
341
+ );
342
+
343
+ return `{
344
+ pathname: ${JSON.stringify(route.pathname)},
345
+ segments: ${JSON.stringify(route.segments)},
346
+ guards: [${guards.join(", ")}]
347
+ }`;
348
+ }
349
+ );
350
+
351
+ return `${imports.join("\n")}
352
+
353
+ const definitions = [
354
+ ${definitions.join(",\n ")}
355
+ ];
356
+
357
+ const routePatterns =
358
+ definitions.map(
359
+ (definition) => ({
360
+ pathname:
361
+ definition.pathname,
362
+ segments:
363
+ definition.segments,
364
+ filePath:
365
+ definition.pathname,
366
+ layouts: []
367
+ })
368
+ );
369
+
370
+ const definitionByPathname =
371
+ new Map(
372
+ definitions.map(
373
+ (definition) => [
374
+ definition.pathname,
375
+ definition
376
+ ]
377
+ )
378
+ );
379
+
380
+ export const guardedPathnames =
381
+ definitions.map(
382
+ (definition) =>
383
+ definition.pathname
384
+ );
385
+
386
+ export async function evaluateRouteGuards(input) {
387
+ const target =
388
+ new URL(
389
+ input.url
390
+ );
391
+ const match =
392
+ matchRoute(
393
+ routePatterns,
394
+ target.pathname
395
+ );
396
+
397
+ if (!match) {
398
+ return null;
399
+ }
400
+
401
+ const definition =
402
+ definitionByPathname.get(
403
+ match.route.pathname
404
+ );
405
+
406
+ if (!definition) {
407
+ return null;
408
+ }
409
+
410
+ const request =
411
+ new Request(
412
+ target,
413
+ {
414
+ method:
415
+ input.method ||
416
+ "GET",
417
+ headers:
418
+ new Headers(
419
+ input.headers ||
420
+ []
421
+ )
422
+ }
423
+ );
424
+
425
+ return runWithRequestContext(
426
+ request,
427
+ async () => {
428
+ const execution =
429
+ await executePageGuardFunctions(
430
+ definition.guards,
431
+ match.params,
432
+ target
433
+ );
434
+
435
+ if (execution.response) {
436
+ return {
437
+ kind:
438
+ "response",
439
+ response:
440
+ applyResponseCookies(
441
+ execution.response
442
+ )
443
+ };
444
+ }
445
+
446
+ const cookieCarrier =
447
+ applyResponseCookies(
448
+ new Response(
449
+ null,
450
+ {
451
+ status: 204
452
+ }
453
+ )
454
+ );
455
+
456
+ return {
457
+ kind:
458
+ "allow",
459
+ data:
460
+ execution.data,
461
+ setCookies:
462
+ cookieCarrier.headers.getSetCookie()
463
+ };
464
+ },
465
+ {
466
+ remoteAddress:
467
+ input.remoteAddress ||
468
+ null
469
+ }
470
+ );
471
+ }
472
+ `;
473
+ }
474
+
475
+ function toImportSpecifier(
476
+ fromDirectory: string,
477
+ filePath: string
478
+ ): string {
479
+ let relative =
480
+ path.relative(
481
+ fromDirectory,
482
+ filePath
483
+ )
484
+ .replace(
485
+ /\\/g,
486
+ "/"
487
+ );
488
+
489
+ if (
490
+ !relative.startsWith(".")
491
+ ) {
492
+ relative =
493
+ `./${relative}`;
494
+ }
495
+
496
+ return relative;
497
+ }
@@ -12,10 +12,18 @@ import {
12
12
  createServerEnvironmentDefines,
13
13
  } from "../../env/src/index.js";
14
14
 
15
+ import {
16
+ routeHasPageActions,
17
+ } from "../../server/src/form-action.js";
18
+
15
19
  import {
16
20
  findProjectMiddlewareFile,
17
21
  } from "../../server/src/middleware-loader.js";
18
22
 
23
+ import {
24
+ routeHasPageGuard,
25
+ } from "../../server/src/page-guard.js";
26
+
19
27
  import type {
20
28
  ApiRoute,
21
29
  Route,
@@ -29,6 +37,14 @@ import type {
29
37
  PartialHydrationBuildManifest,
30
38
  } from "./partial-hydration.js";
31
39
 
40
+ import {
41
+ buildProductionActions,
42
+ } from "./server-production-actions.js";
43
+
44
+ import {
45
+ buildProductionGuards,
46
+ } from "./server-production-guards.js";
47
+
32
48
  import {
33
49
  buildProductionServer as buildBaseProductionServer,
34
50
  type ProductionServerBuildResult,
@@ -73,7 +89,8 @@ export async function buildProductionServer(
73
89
  writeCacheManifest(
74
90
  pageRoutes,
75
91
  apiRoutes,
76
- serverDirectory
92
+ serverDirectory,
93
+ rootDirectory
77
94
  );
78
95
 
79
96
  await buildProductionMiddleware(
@@ -81,6 +98,18 @@ export async function buildProductionServer(
81
98
  serverDirectory
82
99
  );
83
100
 
101
+ await buildProductionGuards(
102
+ pageRoutes,
103
+ rootDirectory,
104
+ serverDirectory
105
+ );
106
+
107
+ await buildProductionActions(
108
+ pageRoutes,
109
+ rootDirectory,
110
+ serverDirectory
111
+ );
112
+
84
113
  return {
85
114
  ...baseResult,
86
115
  size:
@@ -174,11 +203,6 @@ async function bundleServerCacheRuntime(
174
203
  serverFile
175
204
  );
176
205
 
177
- /*
178
- * The base server build may have emitted an external map. The final
179
- * cache-runtime rebundle uses an inline map so a stale server.mjs.map
180
- * must not be shipped next to the rewritten bundle.
181
- */
182
206
  fs.rmSync(
183
207
  `${serverFile}.map`,
184
208
  {
@@ -204,14 +228,32 @@ async function bundleServerCacheRuntime(
204
228
  function writeCacheManifest(
205
229
  pageRoutes: Route[],
206
230
  apiRoutes: ApiRoute[],
207
- serverDirectory: string
231
+ serverDirectory: string,
232
+ rootDirectory: string
208
233
  ): void {
234
+ const appDirectory =
235
+ path.join(
236
+ rootDirectory,
237
+ "app"
238
+ );
239
+ const cacheablePages =
240
+ pageRoutes.filter(
241
+ (route) =>
242
+ !routeHasPageGuard(
243
+ appDirectory,
244
+ route.filePath
245
+ ) &&
246
+ !routeHasPageActions(
247
+ route.filePath
248
+ )
249
+ );
250
+
209
251
  const manifest:
210
252
  CacheManifest = {
211
253
  version: 1,
212
254
  pages:
213
255
  collectCacheRoutes(
214
- pageRoutes
256
+ cacheablePages
215
257
  ),
216
258
  apiRoutes:
217
259
  collectCacheRoutes(
@@ -6,6 +6,7 @@ export type CliCommand =
6
6
  | "build"
7
7
  | "start"
8
8
  | "routes"
9
+ | "update"
9
10
  | "help"
10
11
  | "version";
11
12
 
@@ -17,6 +18,12 @@ export interface CliOptions {
17
18
  port?: number;
18
19
 
19
20
  hostname?: string;
21
+
22
+ updateTarget?: string;
23
+
24
+ updateCheck?: boolean;
25
+
26
+ updateDryRun?: boolean;
20
27
  }
21
28
 
22
29
  export function parseCliArgs(
@@ -33,6 +40,15 @@ export function parseCliArgs(
33
40
  let hostname:
34
41
  string | undefined;
35
42
 
43
+ let updateTarget:
44
+ string | undefined;
45
+
46
+ let updateCheck =
47
+ false;
48
+
49
+ let updateDryRun =
50
+ false;
51
+
36
52
  let commandSet = false;
37
53
 
38
54
  for (
@@ -152,6 +168,22 @@ export function parseCliArgs(
152
168
  continue;
153
169
  }
154
170
 
171
+ if (
172
+ argument === "--check"
173
+ ) {
174
+ updateCheck =
175
+ true;
176
+ continue;
177
+ }
178
+
179
+ if (
180
+ argument === "--dry-run"
181
+ ) {
182
+ updateDryRun =
183
+ true;
184
+ continue;
185
+ }
186
+
155
187
  if (
156
188
  argument.startsWith("-")
157
189
  ) {
@@ -179,6 +211,16 @@ export function parseCliArgs(
179
211
  continue;
180
212
  }
181
213
 
214
+ if (
215
+ commandSet &&
216
+ command === "update" &&
217
+ updateTarget === undefined
218
+ ) {
219
+ updateTarget =
220
+ argument;
221
+ continue;
222
+ }
223
+
182
224
  if (commandSet) {
183
225
  throw new Error(
184
226
  `Unexpected argument: ${argument}`
@@ -190,6 +232,7 @@ export function parseCliArgs(
190
232
  argument === "build" ||
191
233
  argument === "start" ||
192
234
  argument === "routes" ||
235
+ argument === "update" ||
193
236
  argument === "help" ||
194
237
  argument === "version"
195
238
  ) {
@@ -203,11 +246,26 @@ export function parseCliArgs(
203
246
  );
204
247
  }
205
248
 
249
+ if (
250
+ command !== "update" &&
251
+ (
252
+ updateCheck ||
253
+ updateDryRun
254
+ )
255
+ ) {
256
+ throw new Error(
257
+ "Options --check and --dry-run are only valid with `bcp update`."
258
+ );
259
+ }
260
+
206
261
  return {
207
262
  command,
208
263
  rootDirectory,
209
264
  port,
210
265
  hostname,
266
+ updateTarget,
267
+ updateCheck,
268
+ updateDryRun,
211
269
  };
212
270
  }
213
271