Skip to content
File

Blob: src/client/sites/hydrate.tsx

typescript90 lines
1import type { ComponentType } from "react";
2 
3import type { IslandName, IslandPropsByName } from "@/shared/sites/island-schemas";
4 
5type IslandComponent<K extends IslandName> = ComponentType<IslandPropsByName[K]>;
6type IslandLoader<K extends IslandName> = () => Promise<IslandComponent<K>>;
7type HydrateRoot = typeof import("react-dom/client").hydrateRoot;
8type ReadIslandProps = typeof import("@/shared/sites/island-schemas").readIslandProps;
9type IsIslandName = typeof import("@/shared/sites/island-schemas").isIslandName;
10 
11const islandLoaders: { [K in IslandName]: IslandLoader<K> } = {
12 "sites-image": () => import("@/client/sites/islands/sites-image").then((mod) => mod.SitesImage),
13 "copy-code": () => import("@/client/sites/islands/copy-code-button").then((mod) => mod.CopyCodeButton),
14 "site-outline-controller": () =>
15 import("@/client/sites/islands/site-outline-controller").then((mod) => mod.SiteOutlineController),
16};
17 
18export async function bootstrapIslands(): Promise<void> {
19 const candidates = Array.from(document.querySelectorAll<HTMLElement>("[data-island]")).filter(
20 (host) => host.dataset.islandHydrated !== "true",
21 );
22 if (candidates.length === 0) return;
23 
24 const [{ hydrateRoot }, { readIslandProps, isIslandName }] = await Promise.all([
25 import("react-dom/client"),
26 import("@/shared/sites/island-schemas"),
27 ]);
28 const hosts = candidates.filter((host) => isHydratableHost(host, isIslandName));
29 if (hosts.length === 0) return;
30 
31 await Promise.all(
32 hosts.map(async (host) => {
33 const name = host.dataset.island;
34 if (!isIslandName(name)) return;
35 
36 const root = host.querySelector<HTMLElement>("[data-island-root]");
37 const template = host.querySelector<HTMLTemplateElement>("template[data-island-props]");
38 if (!root || !template) return;
39 
40 await hydrateHost({ host, name, root, propsText: template.innerHTML, hydrateRoot, readIslandProps });
41 }),
42 );
43}
44 
45async function hydrateHost({
46 host,
47 name,
48 root,
49 propsText,
50 hydrateRoot,
51 readIslandProps,
52}: {
53 host: HTMLElement;
54 name: IslandName;
55 root: HTMLElement;
56 propsText: string;
57 hydrateRoot: HydrateRoot;
58 readIslandProps: ReadIslandProps;
59}): Promise<void> {
60 switch (name) {
61 case "sites-image": {
62 const props = readIslandProps(name, propsText);
63 if (!props) return;
64 const Component = await islandLoaders[name]();
65 hydrateRoot(root, <Component {...props} />);
66 break;
67 }
68 case "copy-code": {
69 const props = readIslandProps(name, propsText);
70 if (!props) return;
71 const Component = await islandLoaders[name]();
72 hydrateRoot(root, <Component {...props} />);
73 break;
74 }
75 case "site-outline-controller": {
76 const props = readIslandProps(name, propsText);
77 if (!props) return;
78 const Component = await islandLoaders[name]();
79 hydrateRoot(root, <Component {...props} />);
80 break;
81 }
82 }
83 
84 host.dataset.islandHydrated = "true";
85}
86 
87function isHydratableHost(host: HTMLElement, isIslandName: IsIslandName): boolean {
88 return host.dataset.islandHydrated !== "true" && isIslandName(host.dataset.island);
89}