@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,969 @@
1
+ import {
2
+ useEffect,
3
+ type AnchorHTMLAttributes,
4
+ type MouseEvent,
5
+ type ReactNode,
6
+ } from "react";
7
+
8
+ import {
9
+ applyResolvedMetadata,
10
+ type ResolvedMetadata,
11
+ } from "./metadata.js";
12
+
13
+ import {
14
+ hideNavigationLoading,
15
+ showNavigationLoading,
16
+ } from "./navigation-loading.js";
17
+
18
+ import {
19
+ getNavigationState,
20
+ resetNavigationState,
21
+ setNavigationState,
22
+ } from "./navigation-state.js";
23
+
24
+ export interface NavigationOptions {
25
+ replace?: boolean;
26
+ scroll?: boolean;
27
+ }
28
+
29
+ export interface Router {
30
+ readonly isNavigating: boolean;
31
+ push(
32
+ href: string,
33
+ options?: Omit<NavigationOptions, "replace">
34
+ ): Promise<void>;
35
+ replace(
36
+ href: string,
37
+ options?: Omit<NavigationOptions, "replace">
38
+ ): Promise<void>;
39
+ back(): void;
40
+ forward(): void;
41
+ refresh(): Promise<void>;
42
+ }
43
+
44
+ export interface LinkProps
45
+ extends Omit<
46
+ AnchorHTMLAttributes<HTMLAnchorElement>,
47
+ "href"
48
+ > {
49
+ href: string;
50
+ replace?: boolean;
51
+ scroll?: boolean;
52
+ }
53
+
54
+ interface NavigationPayload {
55
+ route?: string;
56
+ params?: Record<string, string>;
57
+ clientScript: string;
58
+ clientImports: string[];
59
+ title: string;
60
+ metadata: ResolvedMetadata | null;
61
+ version: number;
62
+ loadingHtml: string | null;
63
+ }
64
+
65
+ interface BcpDevConfig {
66
+ eventsPath: string;
67
+ clientScript: string;
68
+ route: string;
69
+ version: number;
70
+ }
71
+
72
+ interface BcpReactRoot {
73
+ render(
74
+ tree: ReactNode
75
+ ): void;
76
+ unmount(): void;
77
+ }
78
+
79
+ declare global {
80
+ var __BCP_DEV_CONFIG__:
81
+ | BcpDevConfig
82
+ | undefined;
83
+ var __BCP_NAVIGATION__:
84
+ | Router
85
+ | undefined;
86
+ var __BCP_HMR_SOURCE__:
87
+ | EventSource
88
+ | undefined;
89
+ var __BCP_REACT_ROOT__:
90
+ | BcpReactRoot
91
+ | undefined;
92
+
93
+ interface Window {
94
+ __BCP_DEV_CONFIG__?: BcpDevConfig;
95
+ __BCP_NAVIGATION__?: Router;
96
+ __BCP_HMR_SOURCE__?: EventSource;
97
+ __BCP_REACT_ROOT__?: BcpReactRoot;
98
+ __BCP_ROUTER_INSTALLED__?: boolean;
99
+ }
100
+ }
101
+
102
+ let navigationController:
103
+ AbortController | null = null;
104
+
105
+ export async function navigate(
106
+ href: string,
107
+ options: NavigationOptions = {}
108
+ ): Promise<void> {
109
+ if (typeof window === "undefined") {
110
+ return;
111
+ }
112
+
113
+ const target =
114
+ new URL(
115
+ href,
116
+ window.location.href
117
+ );
118
+
119
+ if (
120
+ target.origin !==
121
+ window.location.origin
122
+ ) {
123
+ window.location.assign(
124
+ target.href
125
+ );
126
+ return;
127
+ }
128
+
129
+ const currentWithoutHash =
130
+ `${window.location.pathname}${window.location.search}`;
131
+ const targetWithoutHash =
132
+ `${target.pathname}${target.search}`;
133
+
134
+ if (
135
+ currentWithoutHash ===
136
+ targetWithoutHash &&
137
+ target.hash !==
138
+ window.location.hash
139
+ ) {
140
+ updateHistory(
141
+ target,
142
+ Boolean(
143
+ options.replace
144
+ )
145
+ );
146
+ scrollToHash(
147
+ target.hash
148
+ );
149
+ return;
150
+ }
151
+
152
+ if (
153
+ target.href ===
154
+ window.location.href
155
+ ) {
156
+ return;
157
+ }
158
+
159
+ await loadNavigation(
160
+ target,
161
+ options
162
+ );
163
+ }
164
+
165
+ export function useRouter(): Router {
166
+ ensureNavigationRuntime();
167
+
168
+ if (
169
+ typeof window !== "undefined" &&
170
+ window.__BCP_NAVIGATION__
171
+ ) {
172
+ return window.__BCP_NAVIGATION__;
173
+ }
174
+
175
+ return createRouter();
176
+ }
177
+
178
+ export function Link({
179
+ href,
180
+ replace = false,
181
+ scroll = true,
182
+ onClick,
183
+ ...props
184
+ }: LinkProps) {
185
+ useEffect(
186
+ () => {
187
+ ensureNavigationRuntime();
188
+ },
189
+ []
190
+ );
191
+
192
+ function handleClick(
193
+ event: MouseEvent<HTMLAnchorElement>
194
+ ) {
195
+ onClick?.(event);
196
+
197
+ if (
198
+ event.defaultPrevented ||
199
+ !shouldInterceptClick(
200
+ event
201
+ )
202
+ ) {
203
+ return;
204
+ }
205
+
206
+ event.preventDefault();
207
+
208
+ void navigate(
209
+ href,
210
+ {
211
+ replace,
212
+ scroll,
213
+ }
214
+ ).catch(
215
+ (error) => {
216
+ if (isAbortError(error)) {
217
+ return;
218
+ }
219
+
220
+ console.error(
221
+ "[BCP Router] navigation failed:",
222
+ error
223
+ );
224
+ window.location.assign(
225
+ href
226
+ );
227
+ }
228
+ );
229
+ }
230
+
231
+ return (
232
+ <a
233
+ {...props}
234
+ href={href}
235
+ onClick={handleClick}
236
+ />
237
+ );
238
+ }
239
+
240
+ function createRouter(): Router {
241
+ return {
242
+ get isNavigating() {
243
+ return getNavigationState()
244
+ .isNavigating;
245
+ },
246
+ push(
247
+ href,
248
+ options = {}
249
+ ) {
250
+ return navigate(
251
+ href,
252
+ {
253
+ ...options,
254
+ replace: false,
255
+ }
256
+ );
257
+ },
258
+ replace(
259
+ href,
260
+ options = {}
261
+ ) {
262
+ return navigate(
263
+ href,
264
+ {
265
+ ...options,
266
+ replace: true,
267
+ }
268
+ );
269
+ },
270
+ back() {
271
+ if (
272
+ typeof window !==
273
+ "undefined"
274
+ ) {
275
+ window.history.back();
276
+ }
277
+ },
278
+ forward() {
279
+ if (
280
+ typeof window !==
281
+ "undefined"
282
+ ) {
283
+ window.history.forward();
284
+ }
285
+ },
286
+ refresh() {
287
+ if (
288
+ typeof window ===
289
+ "undefined"
290
+ ) {
291
+ return Promise.resolve();
292
+ }
293
+
294
+ return loadNavigation(
295
+ new URL(
296
+ window.location.href
297
+ ),
298
+ {
299
+ replace: true,
300
+ scroll: false,
301
+ }
302
+ );
303
+ },
304
+ };
305
+ }
306
+
307
+ function ensureNavigationRuntime(): void {
308
+ if (typeof window === "undefined") {
309
+ return;
310
+ }
311
+
312
+ if (!window.__BCP_NAVIGATION__) {
313
+ window.__BCP_NAVIGATION__ =
314
+ createRouter();
315
+ }
316
+
317
+ if (window.__BCP_ROUTER_INSTALLED__) {
318
+ return;
319
+ }
320
+
321
+ window.__BCP_ROUTER_INSTALLED__ =
322
+ true;
323
+
324
+ window.addEventListener(
325
+ "popstate",
326
+ () => {
327
+ void loadNavigation(
328
+ new URL(
329
+ window.location.href
330
+ ),
331
+ {
332
+ replace: true,
333
+ scroll: true,
334
+ fromHistory: true,
335
+ }
336
+ ).catch(
337
+ (error) => {
338
+ if (isAbortError(error)) {
339
+ return;
340
+ }
341
+
342
+ console.error(
343
+ "[BCP Router] history navigation failed:",
344
+ error
345
+ );
346
+ window.location.reload();
347
+ }
348
+ );
349
+ }
350
+ );
351
+ }
352
+
353
+ async function loadNavigation(
354
+ target: URL,
355
+ options: NavigationOptions & {
356
+ fromHistory?: boolean;
357
+ }
358
+ ): Promise<void> {
359
+ ensureNavigationRuntime();
360
+ navigationController?.abort();
361
+
362
+ const controller =
363
+ new AbortController();
364
+ navigationController =
365
+ controller;
366
+
367
+ hideNavigationLoading();
368
+ setNavigationState({
369
+ isNavigating: true,
370
+ targetHref:
371
+ target.href,
372
+ targetRoute: null,
373
+ });
374
+
375
+ try {
376
+ const targetPath =
377
+ `${target.pathname}${target.search}`;
378
+
379
+ const response =
380
+ await fetch(
381
+ `/_bcp/navigation?url=${encodeURIComponent(
382
+ targetPath
383
+ )}`,
384
+ {
385
+ method: "GET",
386
+ headers: {
387
+ Accept:
388
+ "application/json",
389
+ "X-BCP-Navigation":
390
+ "1",
391
+ },
392
+ signal:
393
+ controller.signal,
394
+ }
395
+ );
396
+
397
+ if (!response.ok) {
398
+ window.location.assign(
399
+ target.href
400
+ );
401
+ return;
402
+ }
403
+
404
+ const payload =
405
+ await parseNavigationPayload(
406
+ response
407
+ );
408
+
409
+ if (!payload) {
410
+ window.location.assign(
411
+ target.href
412
+ );
413
+ return;
414
+ }
415
+
416
+ setNavigationState({
417
+ targetRoute:
418
+ payload.route ??
419
+ target.pathname,
420
+ });
421
+
422
+ showNavigationLoading(
423
+ payload.loadingHtml
424
+ );
425
+ preloadClientImports(
426
+ payload.clientImports
427
+ );
428
+ applyNavigationPayload(
429
+ payload
430
+ );
431
+
432
+ if (!options.fromHistory) {
433
+ updateHistory(
434
+ target,
435
+ Boolean(
436
+ options.replace
437
+ )
438
+ );
439
+ }
440
+
441
+ updateDevConfig(
442
+ payload,
443
+ target
444
+ );
445
+ await restartDevRuntime();
446
+ await importClientBundle(
447
+ payload.clientScript
448
+ );
449
+ await waitForNextPaint();
450
+
451
+ if (options.scroll !== false) {
452
+ if (target.hash) {
453
+ scrollToHash(
454
+ target.hash
455
+ );
456
+ } else {
457
+ window.scrollTo(
458
+ 0,
459
+ 0
460
+ );
461
+ }
462
+ }
463
+ } finally {
464
+ if (
465
+ navigationController ===
466
+ controller
467
+ ) {
468
+ navigationController =
469
+ null;
470
+ hideNavigationLoading();
471
+ resetNavigationState();
472
+ }
473
+ }
474
+ }
475
+
476
+ async function parseNavigationPayload(
477
+ response: Response
478
+ ): Promise<NavigationPayload | null> {
479
+ try {
480
+ const payload =
481
+ await response.json() as
482
+ Partial<NavigationPayload>;
483
+
484
+ if (
485
+ typeof payload.clientScript !==
486
+ "string" ||
487
+ typeof payload.title !==
488
+ "string" ||
489
+ typeof payload.version !==
490
+ "number" ||
491
+ !(
492
+ payload.loadingHtml === null ||
493
+ typeof payload.loadingHtml ===
494
+ "string"
495
+ ) ||
496
+ !(
497
+ payload.clientImports ===
498
+ undefined ||
499
+ (
500
+ Array.isArray(
501
+ payload.clientImports
502
+ ) &&
503
+ payload.clientImports.every(
504
+ (item) =>
505
+ typeof item ===
506
+ "string"
507
+ )
508
+ )
509
+ ) ||
510
+ !(
511
+ payload.metadata === undefined ||
512
+ payload.metadata === null ||
513
+ isResolvedMetadata(
514
+ payload.metadata
515
+ )
516
+ )
517
+ ) {
518
+ return null;
519
+ }
520
+
521
+ return {
522
+ route:
523
+ payload.route,
524
+ params:
525
+ payload.params ?? {},
526
+ clientScript:
527
+ stripQuery(
528
+ payload.clientScript
529
+ ),
530
+ clientImports:
531
+ (
532
+ payload.clientImports ??
533
+ []
534
+ ).map(
535
+ stripQuery
536
+ ),
537
+ title:
538
+ payload.title,
539
+ metadata:
540
+ payload.metadata ??
541
+ null,
542
+ version:
543
+ payload.version,
544
+ loadingHtml:
545
+ payload.loadingHtml,
546
+ };
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+
552
+ function applyNavigationPayload(
553
+ payload: NavigationPayload
554
+ ): void {
555
+ if (
556
+ !document.getElementById(
557
+ "root"
558
+ )
559
+ ) {
560
+ throw new Error(
561
+ "BCP Framework: root element was not found during navigation."
562
+ );
563
+ }
564
+
565
+ const dataElement =
566
+ document.getElementById(
567
+ "__BCP_DATA__"
568
+ );
569
+
570
+ if (!dataElement) {
571
+ throw new Error(
572
+ "BCP Framework: framework data was not found during navigation."
573
+ );
574
+ }
575
+
576
+ dataElement.textContent =
577
+ serializeFrameworkData({
578
+ route:
579
+ payload.route ??
580
+ window.location.pathname,
581
+ params:
582
+ payload.params ?? {},
583
+ metadata:
584
+ payload.metadata,
585
+ });
586
+
587
+ if (payload.metadata) {
588
+ applyResolvedMetadata(
589
+ payload.metadata
590
+ );
591
+ } else {
592
+ document.title =
593
+ payload.title ||
594
+ document.title;
595
+ }
596
+ }
597
+
598
+ function preloadClientImports(
599
+ imports: string[]
600
+ ): void {
601
+ for (const importPath of imports) {
602
+ const target =
603
+ new URL(
604
+ importPath,
605
+ window.location.href
606
+ );
607
+
608
+ if (
609
+ target.origin !==
610
+ window.location.origin
611
+ ) {
612
+ continue;
613
+ }
614
+
615
+ const alreadyPreloaded =
616
+ Array.from(
617
+ document.querySelectorAll<HTMLLinkElement>(
618
+ 'link[rel="modulepreload"]'
619
+ )
620
+ ).some(
621
+ (link) =>
622
+ link.href ===
623
+ target.href
624
+ );
625
+
626
+ if (alreadyPreloaded) {
627
+ continue;
628
+ }
629
+
630
+ const link =
631
+ document.createElement(
632
+ "link"
633
+ );
634
+ link.rel =
635
+ "modulepreload";
636
+ link.href =
637
+ target.href;
638
+ link.dataset.bcpPreload =
639
+ "true";
640
+ document.head.appendChild(
641
+ link
642
+ );
643
+ }
644
+ }
645
+
646
+ function updateDevConfig(
647
+ payload: NavigationPayload,
648
+ target: URL
649
+ ): void {
650
+ const config =
651
+ window.__BCP_DEV_CONFIG__;
652
+
653
+ if (!config) {
654
+ return;
655
+ }
656
+
657
+ window.__BCP_DEV_CONFIG__ = {
658
+ ...config,
659
+ clientScript:
660
+ payload.clientScript,
661
+ route:
662
+ payload.route ??
663
+ target.pathname,
664
+ version:
665
+ payload.version,
666
+ };
667
+ }
668
+
669
+ function updateHistory(
670
+ target: URL,
671
+ replace: boolean
672
+ ): void {
673
+ const state = {
674
+ __bcp: true,
675
+ };
676
+
677
+ if (replace) {
678
+ window.history.replaceState(
679
+ state,
680
+ "",
681
+ target.href
682
+ );
683
+ } else {
684
+ window.history.pushState(
685
+ state,
686
+ "",
687
+ target.href
688
+ );
689
+ }
690
+ }
691
+
692
+ async function restartDevRuntime(): Promise<void> {
693
+ if (
694
+ typeof window === "undefined" ||
695
+ !window.__BCP_DEV_CONFIG__
696
+ ) {
697
+ return;
698
+ }
699
+
700
+ window.__BCP_HMR_SOURCE__?.close();
701
+ delete window.__BCP_HMR_SOURCE__;
702
+
703
+ document
704
+ .querySelector(
705
+ "script[data-bcp-runtime]"
706
+ )
707
+ ?.remove();
708
+
709
+ const script =
710
+ document.createElement(
711
+ "script"
712
+ );
713
+ script.dataset.bcpRuntime =
714
+ "true";
715
+ script.src =
716
+ `/_bcp/dev-runtime.js?bcp-nav-runtime=${Date.now()}`;
717
+ script.async = false;
718
+
719
+ await new Promise<void>(
720
+ (
721
+ resolve,
722
+ reject
723
+ ) => {
724
+ script.addEventListener(
725
+ "load",
726
+ () => resolve(),
727
+ {
728
+ once: true,
729
+ }
730
+ );
731
+ script.addEventListener(
732
+ "error",
733
+ () => reject(
734
+ new Error(
735
+ "BCP Framework: failed to reload the dev runtime."
736
+ )
737
+ ),
738
+ {
739
+ once: true,
740
+ }
741
+ );
742
+ document.head.appendChild(
743
+ script
744
+ );
745
+ }
746
+ );
747
+ }
748
+
749
+ async function importClientBundle(
750
+ clientScript: string
751
+ ): Promise<void> {
752
+ const separator =
753
+ clientScript.includes("?")
754
+ ? "&"
755
+ : "?";
756
+
757
+ await import(
758
+ clientScript +
759
+ separator +
760
+ "bcp-nav=" +
761
+ Date.now()
762
+ );
763
+ }
764
+
765
+ function shouldInterceptClick(
766
+ event: MouseEvent<HTMLAnchorElement>
767
+ ): boolean {
768
+ if (
769
+ event.defaultPrevented ||
770
+ event.button !== 0 ||
771
+ event.metaKey ||
772
+ event.ctrlKey ||
773
+ event.shiftKey ||
774
+ event.altKey
775
+ ) {
776
+ return false;
777
+ }
778
+
779
+ const anchor =
780
+ event.currentTarget;
781
+
782
+ if (
783
+ anchor.target &&
784
+ anchor.target !== "_self"
785
+ ) {
786
+ return false;
787
+ }
788
+
789
+ if (
790
+ anchor.hasAttribute(
791
+ "download"
792
+ )
793
+ ) {
794
+ return false;
795
+ }
796
+
797
+ return (
798
+ new URL(
799
+ anchor.href,
800
+ window.location.href
801
+ ).origin ===
802
+ window.location.origin
803
+ );
804
+ }
805
+
806
+ function scrollToHash(
807
+ hash: string
808
+ ): void {
809
+ const element =
810
+ document.getElementById(
811
+ decodeURIComponent(
812
+ hash.slice(1)
813
+ )
814
+ );
815
+
816
+ if (element) {
817
+ element.scrollIntoView();
818
+ } else {
819
+ window.scrollTo(
820
+ 0,
821
+ 0
822
+ );
823
+ }
824
+ }
825
+
826
+ function stripQuery(
827
+ value: string
828
+ ): string {
829
+ const url =
830
+ new URL(
831
+ value,
832
+ window.location.href
833
+ );
834
+ url.search = "";
835
+ return `${url.pathname}${url.hash}`;
836
+ }
837
+
838
+ function serializeFrameworkData(
839
+ value: unknown
840
+ ): string {
841
+ return JSON.stringify(
842
+ value
843
+ )
844
+ .replace(
845
+ /</g,
846
+ "\\u003c"
847
+ )
848
+ .replace(
849
+ /\u2028/g,
850
+ "\\u2028"
851
+ )
852
+ .replace(
853
+ /\u2029/g,
854
+ "\\u2029"
855
+ );
856
+ }
857
+
858
+ function waitForNextPaint(): Promise<void> {
859
+ return new Promise(
860
+ (resolve) => {
861
+ requestAnimationFrame(
862
+ () => resolve()
863
+ );
864
+ }
865
+ );
866
+ }
867
+
868
+ function isAbortError(
869
+ error: unknown
870
+ ): boolean {
871
+ return (
872
+ error instanceof DOMException &&
873
+ error.name === "AbortError"
874
+ );
875
+ }
876
+
877
+ function isResolvedMetadata(
878
+ value: unknown
879
+ ): value is ResolvedMetadata {
880
+ if (
881
+ typeof value !== "object" ||
882
+ value === null
883
+ ) {
884
+ return false;
885
+ }
886
+
887
+ const metadata =
888
+ value as Partial<ResolvedMetadata>;
889
+
890
+ if (
891
+ typeof metadata.title !==
892
+ "string" ||
893
+ !isNullableString(
894
+ metadata.description
895
+ ) ||
896
+ !isNullableString(
897
+ metadata.robots
898
+ ) ||
899
+ !isNullableString(
900
+ metadata.canonical
901
+ ) ||
902
+ typeof metadata.icons !==
903
+ "object" ||
904
+ metadata.icons === null
905
+ ) {
906
+ return false;
907
+ }
908
+
909
+ if (
910
+ !isNullableString(
911
+ metadata.icons.icon
912
+ ) ||
913
+ !isNullableString(
914
+ metadata.icons.shortcut
915
+ ) ||
916
+ !isNullableString(
917
+ metadata.icons.apple
918
+ )
919
+ ) {
920
+ return false;
921
+ }
922
+
923
+ if (metadata.openGraph === null) {
924
+ return true;
925
+ }
926
+
927
+ if (
928
+ typeof metadata.openGraph !==
929
+ "object" ||
930
+ metadata.openGraph === null
931
+ ) {
932
+ return false;
933
+ }
934
+
935
+ return (
936
+ isNullableString(
937
+ metadata.openGraph.title
938
+ ) &&
939
+ isNullableString(
940
+ metadata.openGraph.description
941
+ ) &&
942
+ isNullableString(
943
+ metadata.openGraph.url
944
+ ) &&
945
+ isNullableString(
946
+ metadata.openGraph.type
947
+ ) &&
948
+ isNullableString(
949
+ metadata.openGraph.siteName
950
+ ) &&
951
+ Array.isArray(
952
+ metadata.openGraph.images
953
+ ) &&
954
+ metadata.openGraph.images.every(
955
+ (image) =>
956
+ typeof image ===
957
+ "string"
958
+ )
959
+ );
960
+ }
961
+
962
+ function isNullableString(
963
+ value: unknown
964
+ ): value is string | null {
965
+ return (
966
+ value === null ||
967
+ typeof value === "string"
968
+ );
969
+ }