@chidchanun/bcp 0.1.0

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 (66) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +241 -0
  4. package/docs/caching.md +76 -0
  5. package/docs/configuration.md +97 -0
  6. package/docs/deployment.md +74 -0
  7. package/docs/getting-started.md +82 -0
  8. package/docs/middleware.md +58 -0
  9. package/docs/releasing.md +299 -0
  10. package/docs/routing.md +103 -0
  11. package/docs/security.md +57 -0
  12. package/package.json +68 -0
  13. package/packages/bundler/src/client-islands.ts +1457 -0
  14. package/packages/bundler/src/incremental-context.ts +206 -0
  15. package/packages/bundler/src/index.ts +1991 -0
  16. package/packages/bundler/src/module-graph.ts +317 -0
  17. package/packages/bundler/src/partial-hydration.ts +414 -0
  18. package/packages/bundler/src/production.ts +974 -0
  19. package/packages/bundler/src/server-production-middleware.ts +447 -0
  20. package/packages/bundler/src/server-production.ts +1193 -0
  21. package/packages/bundler/src/special-files.ts +131 -0
  22. package/packages/cache/src/index.ts +761 -0
  23. package/packages/cli/bin/bcp.mjs +93 -0
  24. package/packages/cli/src/args.ts +305 -0
  25. package/packages/cli/src/bootstrap.ts +514 -0
  26. package/packages/cli/src/index.ts +504 -0
  27. package/packages/cli/src/version.ts +45 -0
  28. package/packages/client/src/cache.ts +11 -0
  29. package/packages/client/src/config.ts +18 -0
  30. package/packages/client/src/error-boundary.tsx +149 -0
  31. package/packages/client/src/hydration.ts +3 -0
  32. package/packages/client/src/index.tsx +57 -0
  33. package/packages/client/src/islands.tsx +315 -0
  34. package/packages/client/src/metadata.ts +281 -0
  35. package/packages/client/src/navigation-loading.ts +52 -0
  36. package/packages/client/src/navigation-state.ts +80 -0
  37. package/packages/client/src/not-found.ts +34 -0
  38. package/packages/client/src/persistent-layout-runtime.ts +273 -0
  39. package/packages/client/src/router-v2.tsx +969 -0
  40. package/packages/client/src/router.tsx +1 -0
  41. package/packages/config/src/index.ts +1038 -0
  42. package/packages/env/src/index.ts +593 -0
  43. package/packages/router/src/advanced-router.ts +1032 -0
  44. package/packages/router/src/index.ts +1 -0
  45. package/packages/server/src/compression.ts +249 -0
  46. package/packages/server/src/dev-document-metadata.ts +154 -0
  47. package/packages/server/src/dev-hmr.ts +225 -0
  48. package/packages/server/src/index.ts +2265 -0
  49. package/packages/server/src/metadata.ts +478 -0
  50. package/packages/server/src/middleware-dev-server.ts +260 -0
  51. package/packages/server/src/middleware-loader.ts +140 -0
  52. package/packages/server/src/middleware-proxy.ts +516 -0
  53. package/packages/server/src/middleware.ts +704 -0
  54. package/packages/server/src/navigation-payload.ts +471 -0
  55. package/packages/server/src/production-server.ts +1746 -0
  56. package/packages/server/src/response-cache-proxy.ts +828 -0
  57. package/packages/server/src/security-proxy.ts +406 -0
  58. package/packages/server/src/security.ts +451 -0
  59. package/packages/server/src/standalone-production-runtime-v2.ts +2047 -0
  60. package/packages/server/src/standalone-production-runtime-v3.ts +250 -0
  61. package/packages/server/src/standalone-production-runtime-v4.ts +289 -0
  62. package/packages/server/src/standalone-production-runtime-v5.ts +289 -0
  63. package/packages/server/src/standalone-production-runtime.ts +1951 -0
  64. package/packages/server/src/standalone-production-server.ts +6 -0
  65. package/packages/server/src/static-assets.ts +262 -0
  66. package/packages/server/src/static-dev-server.ts +854 -0
@@ -0,0 +1,761 @@
1
+ import {
2
+ AsyncLocalStorage,
3
+ } from "node:async_hooks";
4
+ import {
5
+ createHash,
6
+ } from "node:crypto";
7
+
8
+ export type RevalidateValue =
9
+ | number
10
+ | false;
11
+
12
+ export interface CacheOptions {
13
+ key?: string;
14
+ revalidate?: RevalidateValue;
15
+ tags?: string[];
16
+ paths?: string[];
17
+ }
18
+
19
+ export interface CacheEntryOptions {
20
+ revalidate?: RevalidateValue;
21
+ tags?: string[];
22
+ paths?: string[];
23
+ }
24
+
25
+ export interface CacheStats {
26
+ entries: number;
27
+ inFlight: number;
28
+ }
29
+
30
+ interface CacheEntry {
31
+ value: unknown;
32
+ expiresAt: number | null;
33
+ tags: Set<string>;
34
+ paths: Set<string>;
35
+ }
36
+
37
+ interface RequestCacheContext {
38
+ pathname: string | null;
39
+ memo: Map<string, Promise<unknown>>;
40
+ }
41
+
42
+ interface CacheRuntimeState {
43
+ persistentCache: Map<string, CacheEntry>;
44
+ inFlight: Map<string, Promise<unknown>>;
45
+ requestStorage: AsyncLocalStorage<RequestCacheContext>;
46
+ }
47
+
48
+ const CACHE_RUNTIME_SYMBOL =
49
+ Symbol.for(
50
+ "bcp.framework.cache.runtime"
51
+ );
52
+
53
+ const globalRecord =
54
+ globalThis as typeof globalThis &
55
+ Record<PropertyKey, unknown>;
56
+
57
+ let runtimeState =
58
+ globalRecord[
59
+ CACHE_RUNTIME_SYMBOL
60
+ ] as CacheRuntimeState |
61
+ undefined;
62
+
63
+ if (!runtimeState) {
64
+ runtimeState = {
65
+ persistentCache:
66
+ new Map(),
67
+ inFlight:
68
+ new Map(),
69
+ requestStorage:
70
+ new AsyncLocalStorage<RequestCacheContext>(),
71
+ };
72
+
73
+ globalRecord[
74
+ CACHE_RUNTIME_SYMBOL
75
+ ] =
76
+ runtimeState;
77
+ }
78
+
79
+ const persistentCache =
80
+ runtimeState.persistentCache;
81
+
82
+ const inFlight =
83
+ runtimeState.inFlight;
84
+
85
+ const requestStorage =
86
+ runtimeState.requestStorage;
87
+
88
+ export function enterRequestCacheContext(
89
+ pathname: string
90
+ ): void {
91
+ requestStorage.enterWith({
92
+ pathname:
93
+ normalizePath(pathname),
94
+ memo:
95
+ new Map(),
96
+ });
97
+ }
98
+
99
+ export function runWithRequestCacheContext<T>(
100
+ pathname: string,
101
+ callback: () => T
102
+ ): T {
103
+ return requestStorage.run(
104
+ {
105
+ pathname:
106
+ normalizePath(pathname),
107
+ memo:
108
+ new Map(),
109
+ },
110
+ callback
111
+ );
112
+ }
113
+
114
+ export function dedupe<
115
+ Args extends unknown[],
116
+ Result
117
+ >(
118
+ fn: (...args: Args) =>
119
+ Result | Promise<Result>,
120
+ key?: string
121
+ ): (...args: Args) => Promise<Result> {
122
+ const namespace =
123
+ key ??
124
+ fn.name ??
125
+ "anonymous";
126
+
127
+ return async (
128
+ ...args: Args
129
+ ): Promise<Result> => {
130
+ const context =
131
+ ensureRequestContext();
132
+
133
+ const cacheKey =
134
+ `request:${namespace}:${hashArguments(args)}`;
135
+
136
+ const existing =
137
+ context.memo.get(
138
+ cacheKey
139
+ );
140
+
141
+ if (existing) {
142
+ return await existing as Result;
143
+ }
144
+
145
+ const promise =
146
+ Promise.resolve(
147
+ fn(...args)
148
+ );
149
+
150
+ context.memo.set(
151
+ cacheKey,
152
+ promise
153
+ );
154
+
155
+ try {
156
+ return await promise;
157
+ } catch (error) {
158
+ context.memo.delete(
159
+ cacheKey
160
+ );
161
+ throw error;
162
+ }
163
+ };
164
+ }
165
+
166
+ export function cache<
167
+ Args extends unknown[],
168
+ Result
169
+ >(
170
+ fn: (...args: Args) =>
171
+ Result | Promise<Result>,
172
+ options: CacheOptions = {}
173
+ ): (...args: Args) => Promise<Result> {
174
+ const namespace =
175
+ options.key ??
176
+ fn.name;
177
+
178
+ if (!namespace) {
179
+ throw new Error(
180
+ "BCP Framework: cache() requires options.key when the wrapped function has no name."
181
+ );
182
+ }
183
+
184
+ validateRevalidate(
185
+ options.revalidate
186
+ );
187
+ validateTags(
188
+ options.tags
189
+ );
190
+
191
+ return async (
192
+ ...args: Args
193
+ ): Promise<Result> => {
194
+ if (
195
+ options.revalidate === 0
196
+ ) {
197
+ return await fn(
198
+ ...args
199
+ );
200
+ }
201
+
202
+ const cacheKey =
203
+ `data:${namespace}:${hashArguments(args)}`;
204
+
205
+ const existingEntry =
206
+ readCacheEntry<Result>(
207
+ cacheKey
208
+ );
209
+
210
+ if (existingEntry.hit) {
211
+ return existingEntry.value as Result;
212
+ }
213
+
214
+ const pending =
215
+ inFlight.get(
216
+ cacheKey
217
+ );
218
+
219
+ if (pending) {
220
+ return await pending as Result;
221
+ }
222
+
223
+ const promise =
224
+ Promise.resolve(
225
+ fn(...args)
226
+ );
227
+
228
+ inFlight.set(
229
+ cacheKey,
230
+ promise
231
+ );
232
+
233
+ try {
234
+ const value =
235
+ await promise;
236
+
237
+ const currentPath =
238
+ requestStorage
239
+ .getStore()
240
+ ?.pathname ??
241
+ null;
242
+
243
+ const paths =
244
+ new Set(
245
+ (
246
+ options.paths ?? []
247
+ ).map(
248
+ normalizePath
249
+ )
250
+ );
251
+
252
+ if (currentPath) {
253
+ paths.add(
254
+ currentPath
255
+ );
256
+ }
257
+
258
+ setCacheEntry(
259
+ cacheKey,
260
+ value,
261
+ {
262
+ revalidate:
263
+ options.revalidate,
264
+ tags:
265
+ options.tags,
266
+ paths:
267
+ Array.from(
268
+ paths
269
+ ),
270
+ }
271
+ );
272
+
273
+ return value;
274
+ } finally {
275
+ inFlight.delete(
276
+ cacheKey
277
+ );
278
+ }
279
+ };
280
+ }
281
+
282
+ export function getCacheEntry<T>(
283
+ key: string
284
+ ): T | undefined {
285
+ const result =
286
+ readCacheEntry<T>(
287
+ key
288
+ );
289
+
290
+ return result.hit
291
+ ? result.value
292
+ : undefined;
293
+ }
294
+
295
+ export function setCacheEntry<T>(
296
+ key: string,
297
+ value: T,
298
+ options: CacheEntryOptions = {}
299
+ ): void {
300
+ validateRevalidate(
301
+ options.revalidate
302
+ );
303
+ validateTags(
304
+ options.tags
305
+ );
306
+
307
+ if (
308
+ options.revalidate === 0
309
+ ) {
310
+ persistentCache.delete(
311
+ key
312
+ );
313
+ return;
314
+ }
315
+
316
+ const expiresAt =
317
+ options.revalidate === undefined ||
318
+ options.revalidate === false
319
+ ? null
320
+ : Date.now() +
321
+ options.revalidate * 1000;
322
+
323
+ persistentCache.set(
324
+ key,
325
+ {
326
+ value,
327
+ expiresAt,
328
+ tags:
329
+ new Set(
330
+ options.tags ?? []
331
+ ),
332
+ paths:
333
+ new Set(
334
+ (
335
+ options.paths ?? []
336
+ ).map(
337
+ normalizePath
338
+ )
339
+ ),
340
+ }
341
+ );
342
+ }
343
+
344
+ export function deleteCacheEntry(
345
+ key: string
346
+ ): boolean {
347
+ return persistentCache.delete(
348
+ key
349
+ );
350
+ }
351
+
352
+ export function revalidateTag(
353
+ tag: string
354
+ ): number {
355
+ const normalized =
356
+ normalizeTag(tag);
357
+
358
+ let removed = 0;
359
+
360
+ for (
361
+ const [
362
+ key,
363
+ entry,
364
+ ]
365
+ of persistentCache
366
+ ) {
367
+ if (
368
+ entry.tags.has(
369
+ normalized
370
+ )
371
+ ) {
372
+ persistentCache.delete(
373
+ key
374
+ );
375
+ removed++;
376
+ }
377
+ }
378
+
379
+ return removed;
380
+ }
381
+
382
+ export function revalidatePath(
383
+ pathname: string
384
+ ): number {
385
+ const normalized =
386
+ normalizePath(pathname);
387
+
388
+ let removed = 0;
389
+
390
+ for (
391
+ const [
392
+ key,
393
+ entry,
394
+ ]
395
+ of persistentCache
396
+ ) {
397
+ const matches =
398
+ Array.from(
399
+ entry.paths
400
+ ).some(
401
+ (cachedPath) =>
402
+ pathMatches(
403
+ cachedPath,
404
+ normalized
405
+ )
406
+ );
407
+
408
+ if (matches) {
409
+ persistentCache.delete(
410
+ key
411
+ );
412
+ removed++;
413
+ }
414
+ }
415
+
416
+ return removed;
417
+ }
418
+
419
+ export function clearCache(): void {
420
+ persistentCache.clear();
421
+ inFlight.clear();
422
+ }
423
+
424
+ export function getCacheStats(): CacheStats {
425
+ pruneExpiredEntries();
426
+
427
+ return {
428
+ entries:
429
+ persistentCache.size,
430
+ inFlight:
431
+ inFlight.size,
432
+ };
433
+ }
434
+
435
+ export function createRouteCacheKey(
436
+ kind: "page" | "api" | "navigation",
437
+ url: string
438
+ ): string {
439
+ return `route:${kind}:${url}`;
440
+ }
441
+
442
+ export function normalizeRevalidate(
443
+ value: unknown
444
+ ): RevalidateValue | undefined {
445
+ if (
446
+ value === undefined
447
+ ) {
448
+ return undefined;
449
+ }
450
+
451
+ if (
452
+ value === false
453
+ ) {
454
+ return false;
455
+ }
456
+
457
+ if (
458
+ typeof value === "number" &&
459
+ Number.isFinite(value) &&
460
+ value >= 0
461
+ ) {
462
+ return value;
463
+ }
464
+
465
+ throw new Error(
466
+ "BCP Framework: revalidate must be false or a non-negative number of seconds."
467
+ );
468
+ }
469
+
470
+ function readCacheEntry<T>(
471
+ key: string
472
+ ): {
473
+ hit: boolean;
474
+ value?: T;
475
+ } {
476
+ const entry =
477
+ persistentCache.get(
478
+ key
479
+ );
480
+
481
+ if (!entry) {
482
+ return {
483
+ hit: false,
484
+ };
485
+ }
486
+
487
+ if (
488
+ entry.expiresAt !== null &&
489
+ entry.expiresAt <= Date.now()
490
+ ) {
491
+ persistentCache.delete(
492
+ key
493
+ );
494
+ return {
495
+ hit: false,
496
+ };
497
+ }
498
+
499
+ return {
500
+ hit: true,
501
+ value:
502
+ entry.value as T,
503
+ };
504
+ }
505
+
506
+ function ensureRequestContext(): RequestCacheContext {
507
+ const existing =
508
+ requestStorage.getStore();
509
+
510
+ if (existing) {
511
+ return existing;
512
+ }
513
+
514
+ const context:
515
+ RequestCacheContext = {
516
+ pathname: null,
517
+ memo:
518
+ new Map(),
519
+ };
520
+
521
+ requestStorage.enterWith(
522
+ context
523
+ );
524
+
525
+ return context;
526
+ }
527
+
528
+ function pruneExpiredEntries(): void {
529
+ const now =
530
+ Date.now();
531
+
532
+ for (
533
+ const [
534
+ key,
535
+ entry,
536
+ ]
537
+ of persistentCache
538
+ ) {
539
+ if (
540
+ entry.expiresAt !== null &&
541
+ entry.expiresAt <= now
542
+ ) {
543
+ persistentCache.delete(
544
+ key
545
+ );
546
+ }
547
+ }
548
+ }
549
+
550
+ function validateRevalidate(
551
+ value: RevalidateValue | undefined
552
+ ): void {
553
+ normalizeRevalidate(
554
+ value
555
+ );
556
+ }
557
+
558
+ function validateTags(
559
+ tags: string[] | undefined
560
+ ): void {
561
+ for (
562
+ const tag
563
+ of tags ?? []
564
+ ) {
565
+ normalizeTag(
566
+ tag
567
+ );
568
+ }
569
+ }
570
+
571
+ function normalizeTag(
572
+ value: string
573
+ ): string {
574
+ const normalized =
575
+ value.trim();
576
+
577
+ if (!normalized) {
578
+ throw new Error(
579
+ "BCP Framework: cache tags cannot be empty."
580
+ );
581
+ }
582
+
583
+ return normalized;
584
+ }
585
+
586
+ function normalizePath(
587
+ value: string
588
+ ): string {
589
+ let pathname =
590
+ value.trim();
591
+
592
+ if (!pathname) {
593
+ return "/";
594
+ }
595
+
596
+ try {
597
+ if (
598
+ pathname.startsWith(
599
+ "http://"
600
+ ) ||
601
+ pathname.startsWith(
602
+ "https://"
603
+ )
604
+ ) {
605
+ pathname =
606
+ new URL(
607
+ pathname
608
+ ).pathname;
609
+ }
610
+ } catch {
611
+ // Fall back to treating the input as a pathname.
612
+ }
613
+
614
+ pathname =
615
+ pathname.split("?")[0]
616
+ .split("#")[0];
617
+
618
+ if (
619
+ !pathname.startsWith("/")
620
+ ) {
621
+ pathname =
622
+ `/${pathname}`;
623
+ }
624
+
625
+ if (
626
+ pathname.length > 1 &&
627
+ pathname.endsWith("/")
628
+ ) {
629
+ pathname =
630
+ pathname.slice(
631
+ 0,
632
+ -1
633
+ );
634
+ }
635
+
636
+ return pathname;
637
+ }
638
+
639
+ function pathMatches(
640
+ cachedPath: string,
641
+ invalidatedPath: string
642
+ ): boolean {
643
+ if (
644
+ invalidatedPath === "/"
645
+ ) {
646
+ return true;
647
+ }
648
+
649
+ return (
650
+ cachedPath ===
651
+ invalidatedPath ||
652
+ cachedPath.startsWith(
653
+ `${invalidatedPath}/`
654
+ )
655
+ );
656
+ }
657
+
658
+ function hashArguments(
659
+ args: unknown[]
660
+ ): string {
661
+ return createHash(
662
+ "sha256"
663
+ )
664
+ .update(
665
+ stableSerialize(
666
+ args
667
+ )
668
+ )
669
+ .digest(
670
+ "hex"
671
+ )
672
+ .slice(
673
+ 0,
674
+ 24
675
+ );
676
+ }
677
+
678
+ function stableSerialize(
679
+ value: unknown,
680
+ seen = new WeakSet<object>()
681
+ ): string {
682
+ if (
683
+ value === null
684
+ ) {
685
+ return "null";
686
+ }
687
+
688
+ switch (
689
+ typeof value
690
+ ) {
691
+ case "undefined":
692
+ return "undefined";
693
+ case "string":
694
+ return JSON.stringify(
695
+ value
696
+ );
697
+ case "number":
698
+ case "boolean":
699
+ return String(value);
700
+ case "bigint":
701
+ return `${value.toString()}n`;
702
+ case "symbol":
703
+ case "function":
704
+ throw new Error(
705
+ `BCP Framework: cache keys cannot include ${typeof value} values.`
706
+ );
707
+ case "object":
708
+ break;
709
+ default:
710
+ return String(value);
711
+ }
712
+
713
+ const object =
714
+ value as object;
715
+
716
+ if (
717
+ seen.has(object)
718
+ ) {
719
+ throw new Error(
720
+ "BCP Framework: cache keys cannot include circular values."
721
+ );
722
+ }
723
+
724
+ seen.add(object);
725
+
726
+ try {
727
+ if (
728
+ value instanceof Date
729
+ ) {
730
+ return `Date(${value.toISOString()})`;
731
+ }
732
+
733
+ if (
734
+ Array.isArray(value)
735
+ ) {
736
+ return `[${value.map(
737
+ (item) =>
738
+ stableSerialize(
739
+ item,
740
+ seen
741
+ )
742
+ ).join(",")}]`;
743
+ }
744
+
745
+ const record =
746
+ value as Record<string, unknown>;
747
+
748
+ return `{${Object.keys(record)
749
+ .sort()
750
+ .map(
751
+ (key) =>
752
+ `${JSON.stringify(key)}:${stableSerialize(
753
+ record[key],
754
+ seen
755
+ )}`
756
+ )
757
+ .join(",")}}`;
758
+ } finally {
759
+ seen.delete(object);
760
+ }
761
+ }