@chidchanun/bcp 0.1.9 → 0.1.10

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.
@@ -4,12 +4,29 @@ import {
4
4
  pathToFileURL,
5
5
  } from "node:url";
6
6
 
7
+ import {
8
+ executePageGuards,
9
+ type PageGuardData,
10
+ } from "./page-guard.js";
11
+
12
+ import {
13
+ headers,
14
+ } from "./request-context.js";
15
+
7
16
  export const LOADER_DATA_PARAM =
8
17
  "__bcp_loader_data";
18
+ export const GUARD_DATA_PARAM =
19
+ "__bcp_guard_data";
20
+ export const GUARD_DATA_HEADER =
21
+ "x-bcp-guard-data";
22
+
23
+ const SERVER_DATA_MARKER =
24
+ "__bcp_server_data";
9
25
 
10
26
  export interface PageLoaderContext {
11
27
  params: Record<string, any>;
12
28
  searchParams: URLSearchParams;
29
+ guardData: Readonly<PageGuardData>;
13
30
  }
14
31
 
15
32
  export type PageLoader = (
@@ -18,9 +35,26 @@ export type PageLoader = (
18
35
  | unknown
19
36
  | Promise<unknown>;
20
37
 
38
+ export interface PageServerDataEnvelope {
39
+ __bcp_server_data: true;
40
+ hasPageLoader: boolean;
41
+ hasGuard: boolean;
42
+ loaderData: unknown;
43
+ guardData: PageGuardData;
44
+ }
45
+
21
46
  export interface PageLoaderExecution {
47
+ /*
48
+ * hasLoader remains the SSR-provider switch for backward compatibility.
49
+ * It is true when either loader or guard data must be made available to
50
+ * React during server rendering.
51
+ */
22
52
  hasLoader: boolean;
53
+ hasPageLoader?: boolean;
54
+ hasGuard?: boolean;
23
55
  data: unknown;
56
+ loaderData?: unknown;
57
+ guardData?: PageGuardData;
24
58
  response: Response | null;
25
59
  }
26
60
 
@@ -81,20 +115,54 @@ export async function executePageLoader(
81
115
  requestUrl: URL,
82
116
  moduleVersion = 0
83
117
  ): Promise<PageLoaderExecution> {
118
+ const appDirectory =
119
+ findAppDirectory(
120
+ pageFilePath
121
+ );
122
+ const guardExecution =
123
+ await executePageGuards(
124
+ appDirectory,
125
+ pageFilePath,
126
+ params,
127
+ requestUrl,
128
+ moduleVersion
129
+ );
130
+
131
+ if (
132
+ guardExecution.response
133
+ ) {
134
+ return createExecution({
135
+ hasPageLoader:
136
+ false,
137
+ hasGuard:
138
+ guardExecution.hasGuard,
139
+ loaderData:
140
+ undefined,
141
+ guardData:
142
+ guardExecution.data,
143
+ response:
144
+ guardExecution.response,
145
+ });
146
+ }
147
+
84
148
  const loaderFile =
85
149
  findPageLoaderFile(
86
150
  pageFilePath
87
151
  );
88
152
 
89
153
  if (!loaderFile) {
90
- return {
91
- hasLoader:
154
+ return createExecution({
155
+ hasPageLoader:
92
156
  false,
93
- data:
157
+ hasGuard:
158
+ guardExecution.hasGuard,
159
+ loaderData:
94
160
  undefined,
161
+ guardData:
162
+ guardExecution.data,
95
163
  response:
96
164
  null,
97
- };
165
+ });
98
166
  }
99
167
 
100
168
  const moduleUrl =
@@ -130,7 +198,9 @@ export async function executePageLoader(
130
198
  PageLoader,
131
199
  params,
132
200
  requestUrl,
133
- loaderFile
201
+ loaderFile,
202
+ guardExecution.data,
203
+ guardExecution.hasGuard
134
204
  );
135
205
  }
136
206
 
@@ -138,8 +208,24 @@ export async function executePageLoaderFunction(
138
208
  loader: PageLoader,
139
209
  params: Record<string, any>,
140
210
  requestUrl: URL,
141
- label = "loader"
211
+ label = "loader",
212
+ explicitGuardData?: PageGuardData,
213
+ explicitHasGuard?: boolean
142
214
  ): Promise<PageLoaderExecution> {
215
+ const headerGuardData =
216
+ explicitGuardData ===
217
+ undefined
218
+ ? await readGuardDataHeader()
219
+ : null;
220
+ const guardData =
221
+ explicitGuardData ??
222
+ headerGuardData?.data ??
223
+ {};
224
+ const hasGuard =
225
+ explicitHasGuard ??
226
+ headerGuardData?.hasGuard ??
227
+ false;
228
+
143
229
  const result =
144
230
  await loader({
145
231
  params: {
@@ -149,20 +235,25 @@ export async function executePageLoaderFunction(
149
235
  new URLSearchParams(
150
236
  requestUrl.searchParams
151
237
  ),
238
+ guardData: {
239
+ ...guardData,
240
+ },
152
241
  });
153
242
 
154
243
  if (
155
244
  result instanceof
156
245
  Response
157
246
  ) {
158
- return {
159
- hasLoader:
247
+ return createExecution({
248
+ hasPageLoader:
160
249
  true,
161
- data:
250
+ hasGuard,
251
+ loaderData:
162
252
  undefined,
253
+ guardData,
163
254
  response:
164
255
  result,
165
- };
256
+ });
166
257
  }
167
258
 
168
259
  assertLoaderData(
@@ -170,33 +261,239 @@ export async function executePageLoaderFunction(
170
261
  label
171
262
  );
172
263
 
173
- return {
174
- hasLoader:
264
+ return createExecution({
265
+ hasPageLoader:
175
266
  true,
176
- data:
267
+ hasGuard,
268
+ loaderData:
177
269
  result,
270
+ guardData,
178
271
  response:
179
272
  null,
180
- };
273
+ });
181
274
  }
182
275
 
183
276
  export function attachLoaderDataToParams(
184
277
  params: Record<string, any>,
185
278
  execution: PageLoaderExecution
186
279
  ): Record<string, any> {
187
- if (!execution.hasLoader) {
188
- return {
189
- ...params,
280
+ let result = {
281
+ ...params,
282
+ };
283
+ const hasGuard =
284
+ execution.hasGuard ===
285
+ true;
286
+ const guardData =
287
+ execution.guardData ??
288
+ {};
289
+ const hasPageLoader =
290
+ execution.hasPageLoader ??
291
+ execution.hasLoader;
292
+ const loaderData =
293
+ execution.hasPageLoader ===
294
+ undefined
295
+ ? execution.data
296
+ : execution.loaderData;
297
+
298
+ if (hasGuard) {
299
+ result = {
300
+ ...result,
301
+ [GUARD_DATA_PARAM]:
302
+ guardData,
190
303
  };
191
304
  }
192
305
 
306
+ if (hasPageLoader) {
307
+ result = {
308
+ ...result,
309
+ [LOADER_DATA_PARAM]:
310
+ loaderData,
311
+ };
312
+ }
313
+
314
+ return result;
315
+ }
316
+
317
+ export function isPageServerDataEnvelope(
318
+ value: unknown
319
+ ): value is PageServerDataEnvelope {
320
+ return (
321
+ value !== null &&
322
+ typeof value ===
323
+ "object" &&
324
+ (
325
+ value as
326
+ Record<string, unknown>
327
+ )[SERVER_DATA_MARKER] ===
328
+ true
329
+ );
330
+ }
331
+
332
+ function createExecution(
333
+ input: {
334
+ hasPageLoader: boolean;
335
+ hasGuard: boolean;
336
+ loaderData: unknown;
337
+ guardData: PageGuardData;
338
+ response: Response | null;
339
+ }
340
+ ): PageLoaderExecution {
341
+ const data:
342
+ PageServerDataEnvelope = {
343
+ __bcp_server_data:
344
+ true,
345
+ hasPageLoader:
346
+ input.hasPageLoader,
347
+ hasGuard:
348
+ input.hasGuard,
349
+ loaderData:
350
+ input.loaderData,
351
+ guardData: {
352
+ ...input.guardData,
353
+ },
354
+ };
355
+
193
356
  return {
194
- ...params,
195
- [LOADER_DATA_PARAM]:
196
- execution.data,
357
+ hasLoader:
358
+ input.hasPageLoader ||
359
+ input.hasGuard,
360
+ hasPageLoader:
361
+ input.hasPageLoader,
362
+ hasGuard:
363
+ input.hasGuard,
364
+ data,
365
+ loaderData:
366
+ input.loaderData,
367
+ guardData:
368
+ data.guardData,
369
+ response:
370
+ input.response,
197
371
  };
198
372
  }
199
373
 
374
+ async function readGuardDataHeader():
375
+ Promise<{
376
+ hasGuard: true;
377
+ data: PageGuardData;
378
+ } | null> {
379
+ let requestHeaders:
380
+ Headers;
381
+
382
+ try {
383
+ requestHeaders =
384
+ await headers();
385
+ } catch {
386
+ return null;
387
+ }
388
+
389
+ const encoded =
390
+ requestHeaders.get(
391
+ GUARD_DATA_HEADER
392
+ );
393
+
394
+ if (!encoded) {
395
+ return null;
396
+ }
397
+
398
+ let parsed:
399
+ unknown;
400
+
401
+ try {
402
+ parsed =
403
+ JSON.parse(
404
+ Buffer.from(
405
+ encoded,
406
+ "base64url"
407
+ ).toString(
408
+ "utf8"
409
+ )
410
+ );
411
+ } catch {
412
+ throw new Error(
413
+ "BCP Framework: invalid internal guard data header."
414
+ );
415
+ }
416
+
417
+ assertGuardHeaderData(
418
+ parsed
419
+ );
420
+
421
+ return {
422
+ hasGuard:
423
+ true,
424
+ data:
425
+ parsed,
426
+ };
427
+ }
428
+
429
+ function assertGuardHeaderData(
430
+ value: unknown
431
+ ): asserts value is PageGuardData {
432
+ if (
433
+ value === null ||
434
+ Array.isArray(
435
+ value
436
+ ) ||
437
+ typeof value !==
438
+ "object"
439
+ ) {
440
+ throw new Error(
441
+ "BCP Framework: invalid internal guard data header."
442
+ );
443
+ }
444
+
445
+ const prototype =
446
+ Object.getPrototypeOf(
447
+ value
448
+ );
449
+
450
+ if (
451
+ prototype !==
452
+ Object.prototype &&
453
+ prototype !==
454
+ null
455
+ ) {
456
+ throw new Error(
457
+ "BCP Framework: invalid internal guard data header."
458
+ );
459
+ }
460
+ }
461
+
462
+ function findAppDirectory(
463
+ pageFilePath: string
464
+ ): string {
465
+ let current =
466
+ path.dirname(
467
+ path.resolve(
468
+ pageFilePath
469
+ )
470
+ );
471
+
472
+ while (true) {
473
+ if (
474
+ path.basename(
475
+ current
476
+ ) === "app"
477
+ ) {
478
+ return current;
479
+ }
480
+
481
+ const parent =
482
+ path.dirname(
483
+ current
484
+ );
485
+
486
+ if (parent === current) {
487
+ throw new Error(
488
+ `BCP Framework: could not resolve app directory for page "${pageFilePath}".`
489
+ );
490
+ }
491
+
492
+ current =
493
+ parent;
494
+ }
495
+ }
496
+
200
497
  function assertLoaderData(
201
498
  value: unknown,
202
499
  loaderLabel: string