@chidchanun/bcp 0.1.8 → 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.
Files changed (29) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +218 -7
  3. package/docs/application-modules.md +63 -3
  4. package/docs/releasing.md +34 -19
  5. package/docs/route-guards.md +240 -0
  6. package/docs/server-data-loaders.md +240 -0
  7. package/docs/updating.md +130 -0
  8. package/package.json +1 -1
  9. package/packages/bundler/src/server-production-guards.ts +497 -0
  10. package/packages/bundler/src/server-production-middleware.ts +33 -8
  11. package/packages/bundler/src/server-production.ts +25 -0
  12. package/packages/cli/src/args.ts +58 -0
  13. package/packages/cli/src/index.ts +41 -13
  14. package/packages/cli/src/update.ts +860 -0
  15. package/packages/client/src/index.tsx +5 -0
  16. package/packages/client/src/loader-data.tsx +229 -0
  17. package/packages/client/src/router-v2.tsx +254 -29
  18. package/packages/server/src/dev-navigation-target.ts +188 -0
  19. package/packages/server/src/index.ts +209 -119
  20. package/packages/server/src/navigation-payload.ts +24 -1
  21. package/packages/server/src/navigation-response.ts +284 -0
  22. package/packages/server/src/page-guard.ts +529 -0
  23. package/packages/server/src/page-loader.ts +643 -0
  24. package/packages/server/src/standalone-production-runtime-v2-guard.ts +815 -0
  25. package/packages/server/src/standalone-production-runtime-v2-navigation.ts +785 -0
  26. package/packages/server/src/standalone-production-runtime-v2.ts +131 -19
  27. package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
  28. package/packages/server/src/standalone-production-runtime-v4.ts +42 -2
  29. package/packages/server/src/static-dev-server.ts +21 -17
@@ -0,0 +1,529 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ pathToFileURL,
5
+ } from "node:url";
6
+
7
+ export const GUARD_DATA_PARAM =
8
+ "__bcp_guard_data";
9
+
10
+ export type PageGuardData =
11
+ Record<string, unknown>;
12
+
13
+ export interface PageGuardContext {
14
+ params: Record<string, any>;
15
+ searchParams: URLSearchParams;
16
+ guardData: Readonly<PageGuardData>;
17
+ }
18
+
19
+ export type PageGuard = (
20
+ context: PageGuardContext
21
+ ) =>
22
+ | void
23
+ | PageGuardData
24
+ | Response
25
+ | Promise<
26
+ | void
27
+ | PageGuardData
28
+ | Response
29
+ >;
30
+
31
+ export interface PageGuardExecution {
32
+ hasGuard: boolean;
33
+ data: PageGuardData;
34
+ response: Response | null;
35
+ }
36
+
37
+ export interface PageGuardDefinition {
38
+ filePath: string;
39
+ guard: PageGuard;
40
+ }
41
+
42
+ export function findPageGuardFiles(
43
+ appDirectory: string,
44
+ pageFilePath: string
45
+ ): string[] {
46
+ const appRoot =
47
+ path.resolve(
48
+ appDirectory
49
+ );
50
+ const pageDirectory =
51
+ path.dirname(
52
+ path.resolve(
53
+ pageFilePath
54
+ )
55
+ );
56
+
57
+ if (
58
+ !isInsideDirectory(
59
+ appRoot,
60
+ pageDirectory
61
+ )
62
+ ) {
63
+ throw new Error(
64
+ `BCP Framework: page "${pageFilePath}" is outside app directory "${appDirectory}".`
65
+ );
66
+ }
67
+
68
+ const directories:
69
+ string[] = [];
70
+ let current =
71
+ pageDirectory;
72
+
73
+ while (true) {
74
+ directories.push(
75
+ current
76
+ );
77
+
78
+ if (current === appRoot) {
79
+ break;
80
+ }
81
+
82
+ const parent =
83
+ path.dirname(
84
+ current
85
+ );
86
+
87
+ if (
88
+ parent === current ||
89
+ !isInsideDirectory(
90
+ appRoot,
91
+ parent
92
+ )
93
+ ) {
94
+ break;
95
+ }
96
+
97
+ current =
98
+ parent;
99
+ }
100
+
101
+ directories.reverse();
102
+
103
+ const files:
104
+ string[] = [];
105
+
106
+ for (const directory of directories) {
107
+ const guardFile =
108
+ findGuardFileInDirectory(
109
+ directory
110
+ );
111
+
112
+ if (guardFile) {
113
+ files.push(
114
+ guardFile
115
+ );
116
+ }
117
+ }
118
+
119
+ return files;
120
+ }
121
+
122
+ export function routeHasPageGuard(
123
+ appDirectory: string,
124
+ pageFilePath: string
125
+ ): boolean {
126
+ return findPageGuardFiles(
127
+ appDirectory,
128
+ pageFilePath
129
+ ).length > 0;
130
+ }
131
+
132
+ export async function executePageGuards(
133
+ appDirectory: string,
134
+ pageFilePath: string,
135
+ params: Record<string, any>,
136
+ requestUrl: URL,
137
+ moduleVersion = 0
138
+ ): Promise<PageGuardExecution> {
139
+ const guardFiles =
140
+ findPageGuardFiles(
141
+ appDirectory,
142
+ pageFilePath
143
+ );
144
+
145
+ if (guardFiles.length === 0) {
146
+ return emptyGuardExecution();
147
+ }
148
+
149
+ const definitions:
150
+ PageGuardDefinition[] = [];
151
+
152
+ for (const guardFile of guardFiles) {
153
+ const moduleUrl =
154
+ pathToFileURL(
155
+ guardFile
156
+ );
157
+
158
+ moduleUrl.searchParams.set(
159
+ "bcp-guard",
160
+ String(
161
+ moduleVersion
162
+ )
163
+ );
164
+
165
+ const guardModule =
166
+ await import(
167
+ moduleUrl.href
168
+ ) as {
169
+ guard?: unknown;
170
+ };
171
+
172
+ if (
173
+ typeof guardModule.guard !==
174
+ "function"
175
+ ) {
176
+ throw new Error(
177
+ `BCP Framework: guard "${guardFile}" must export a named guard() function.`
178
+ );
179
+ }
180
+
181
+ definitions.push({
182
+ filePath:
183
+ guardFile,
184
+ guard:
185
+ guardModule.guard as PageGuard,
186
+ });
187
+ }
188
+
189
+ return executePageGuardFunctions(
190
+ definitions,
191
+ params,
192
+ requestUrl
193
+ );
194
+ }
195
+
196
+ export async function executePageGuardFunctions(
197
+ guards: PageGuardDefinition[],
198
+ params: Record<string, any>,
199
+ requestUrl: URL
200
+ ): Promise<PageGuardExecution> {
201
+ if (guards.length === 0) {
202
+ return emptyGuardExecution();
203
+ }
204
+
205
+ const guardData:
206
+ PageGuardData = {};
207
+
208
+ for (const definition of guards) {
209
+ const result =
210
+ await definition.guard({
211
+ params: {
212
+ ...params,
213
+ },
214
+ searchParams:
215
+ new URLSearchParams(
216
+ requestUrl.searchParams
217
+ ),
218
+ guardData: {
219
+ ...guardData,
220
+ },
221
+ });
222
+
223
+ if (
224
+ result instanceof
225
+ Response
226
+ ) {
227
+ return {
228
+ hasGuard:
229
+ true,
230
+ data:
231
+ guardData,
232
+ response:
233
+ result,
234
+ };
235
+ }
236
+
237
+ if (
238
+ result === undefined
239
+ ) {
240
+ continue;
241
+ }
242
+
243
+ assertGuardData(
244
+ result,
245
+ definition.filePath
246
+ );
247
+
248
+ Object.assign(
249
+ guardData,
250
+ result
251
+ );
252
+ }
253
+
254
+ return {
255
+ hasGuard:
256
+ true,
257
+ data:
258
+ guardData,
259
+ response:
260
+ null,
261
+ };
262
+ }
263
+
264
+ export function attachGuardDataToParams(
265
+ params: Record<string, any>,
266
+ execution: PageGuardExecution
267
+ ): Record<string, any> {
268
+ if (!execution.hasGuard) {
269
+ return {
270
+ ...params,
271
+ };
272
+ }
273
+
274
+ return {
275
+ ...params,
276
+ [GUARD_DATA_PARAM]:
277
+ execution.data,
278
+ };
279
+ }
280
+
281
+ function emptyGuardExecution(): PageGuardExecution {
282
+ return {
283
+ hasGuard:
284
+ false,
285
+ data: {},
286
+ response:
287
+ null,
288
+ };
289
+ }
290
+
291
+ function findGuardFileInDirectory(
292
+ directory: string
293
+ ): string | null {
294
+ const tsFile =
295
+ path.join(
296
+ directory,
297
+ "guard.ts"
298
+ );
299
+ const tsxFile =
300
+ path.join(
301
+ directory,
302
+ "guard.tsx"
303
+ );
304
+ const hasTs =
305
+ fs.existsSync(
306
+ tsFile
307
+ );
308
+ const hasTsx =
309
+ fs.existsSync(
310
+ tsxFile
311
+ );
312
+
313
+ if (hasTs && hasTsx) {
314
+ throw new Error(
315
+ `BCP Framework: both guard.ts and guard.tsx exist in "${directory}".`
316
+ );
317
+ }
318
+
319
+ if (hasTs) {
320
+ return tsFile;
321
+ }
322
+
323
+ if (hasTsx) {
324
+ return tsxFile;
325
+ }
326
+
327
+ return null;
328
+ }
329
+
330
+ function assertGuardData(
331
+ value: unknown,
332
+ label: string
333
+ ): asserts value is PageGuardData {
334
+ if (
335
+ value === null ||
336
+ Array.isArray(
337
+ value
338
+ ) ||
339
+ typeof value !==
340
+ "object"
341
+ ) {
342
+ throw new Error(
343
+ `BCP Framework: guard "${label}" must return a plain object, undefined, or a Response.`
344
+ );
345
+ }
346
+
347
+ const prototype =
348
+ Object.getPrototypeOf(
349
+ value
350
+ );
351
+
352
+ if (
353
+ prototype !==
354
+ Object.prototype &&
355
+ prototype !==
356
+ null
357
+ ) {
358
+ throw new Error(
359
+ `BCP Framework: guard "${label}" must return a plain object, undefined, or a Response.`
360
+ );
361
+ }
362
+
363
+ const seen =
364
+ new Set<object>();
365
+
366
+ validateJsonValue(
367
+ value,
368
+ "$",
369
+ seen,
370
+ label
371
+ );
372
+ }
373
+
374
+ function validateJsonValue(
375
+ value: unknown,
376
+ location: string,
377
+ seen: Set<object>,
378
+ guardLabel: string
379
+ ): void {
380
+ if (
381
+ value === null ||
382
+ typeof value ===
383
+ "string" ||
384
+ typeof value ===
385
+ "boolean"
386
+ ) {
387
+ return;
388
+ }
389
+
390
+ if (
391
+ typeof value ===
392
+ "number"
393
+ ) {
394
+ if (
395
+ Number.isFinite(
396
+ value
397
+ )
398
+ ) {
399
+ return;
400
+ }
401
+
402
+ throwInvalidGuardData(
403
+ guardLabel,
404
+ location,
405
+ "numbers must be finite"
406
+ );
407
+ }
408
+
409
+ if (
410
+ typeof value !==
411
+ "object"
412
+ ) {
413
+ throwInvalidGuardData(
414
+ guardLabel,
415
+ location,
416
+ `unsupported ${typeof value} value`
417
+ );
418
+ }
419
+
420
+ const objectValue =
421
+ value as object;
422
+
423
+ if (seen.has(objectValue)) {
424
+ throwInvalidGuardData(
425
+ guardLabel,
426
+ location,
427
+ "circular references are not supported"
428
+ );
429
+ }
430
+
431
+ seen.add(
432
+ objectValue
433
+ );
434
+
435
+ try {
436
+ if (Array.isArray(value)) {
437
+ value.forEach(
438
+ (
439
+ item,
440
+ index
441
+ ) => {
442
+ validateJsonValue(
443
+ item,
444
+ `${location}[${index}]`,
445
+ seen,
446
+ guardLabel
447
+ );
448
+ }
449
+ );
450
+ return;
451
+ }
452
+
453
+ const prototype =
454
+ Object.getPrototypeOf(
455
+ value
456
+ );
457
+
458
+ if (
459
+ prototype !==
460
+ Object.prototype &&
461
+ prototype !==
462
+ null
463
+ ) {
464
+ throwInvalidGuardData(
465
+ guardLabel,
466
+ location,
467
+ "only plain objects and arrays are supported"
468
+ );
469
+ }
470
+
471
+ for (
472
+ const [
473
+ key,
474
+ item,
475
+ ]
476
+ of Object.entries(
477
+ value as Record<
478
+ string,
479
+ unknown
480
+ >
481
+ )
482
+ ) {
483
+ validateJsonValue(
484
+ item,
485
+ `${location}.${key}`,
486
+ seen,
487
+ guardLabel
488
+ );
489
+ }
490
+ } finally {
491
+ seen.delete(
492
+ objectValue
493
+ );
494
+ }
495
+ }
496
+
497
+ function throwInvalidGuardData(
498
+ guardLabel: string,
499
+ location: string,
500
+ reason: string
501
+ ): never {
502
+ throw new Error(
503
+ `BCP Framework: guard "${guardLabel}" returned non-serializable data at ${location}: ${reason}.`
504
+ );
505
+ }
506
+
507
+ function isInsideDirectory(
508
+ rootDirectory: string,
509
+ candidate: string
510
+ ): boolean {
511
+ const relative =
512
+ path.relative(
513
+ rootDirectory,
514
+ candidate
515
+ );
516
+
517
+ return (
518
+ relative === "" ||
519
+ (
520
+ relative !== ".." &&
521
+ !relative.startsWith(
522
+ `..${path.sep}`
523
+ ) &&
524
+ !path.isAbsolute(
525
+ relative
526
+ )
527
+ )
528
+ );
529
+ }