File
Blob: src/client/sites/hydrate.tsx
| 1 | import type { ComponentType } from "react"; |
| 2 | |
| 3 | import type { IslandName, IslandPropsByName } from "@/shared/sites/island-schemas"; |
| 4 | |
| 5 | type IslandComponent<K extends IslandName> = ComponentType<IslandPropsByName[K]>; |
| 6 | type IslandLoader<K extends IslandName> = () => Promise<IslandComponent<K>>; |
| 7 | type HydrateRoot = typeof import("react-dom/client").hydrateRoot; |
| 8 | type ReadIslandProps = typeof import("@/shared/sites/island-schemas").readIslandProps; |
| 9 | type IsIslandName = typeof import("@/shared/sites/island-schemas").isIslandName; |
| 10 | |
| 11 | const 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 | |
| 18 | export 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 | |
| 45 | async 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 | |
| 87 | function isHydratableHost(host: HTMLElement, isIslandName: IsIslandName): boolean { |
| 88 | return host.dataset.islandHydrated !== "true" && isIslandName(host.dataset.island); |
| 89 | } |