UNPKG

4.31 kBJavaScriptView Raw
1import { h, hydrate as mount } from "preact";
2
3if (!("requestIdleCallback" in window)) {
4 window.requestIdleCallback = function (cb) {
5 return setTimeout(function () {
6 var start = Date.now();
7 cb({
8 didTimeout: false,
9 timeRemaining: function () {
10 return Math.max(0, 50 - (Date.now() - start));
11 },
12 });
13 }, 1);
14 };
15
16 window.cancelIdleCallback = function (id) {
17 clearTimeout(id);
18 };
19}
20
21const createObserver = (hydrate) => {
22 if (!("IntersectionObserver" in window)) return null;
23
24 const io = new IntersectionObserver((entries) => {
25 entries.forEach((entry) => {
26 const isIntersecting =
27 entry.isIntersecting || entry.intersectionRatio > 0;
28 if (!isIntersecting) return;
29 hydrate();
30 io.disconnect();
31 });
32 });
33
34 return io;
35};
36
37function attach(fragment, data, { key, name, source }) {
38 const { p: { children = null, ...props } = {}, m: method = "idle" } = data;
39
40 const hydrate = async () => {
41 if (window.__MICROSITE_DEBUG)
42 console.log(`[Hydrate] <${key} /> hydrated via "${method}"`);
43 const { [name]: Component } = await import(source);
44 mount(h(Component, props, children), fragment);
45 };
46
47 switch (method) {
48 case "idle": {
49 if (
50 !("requestAnimationFrame" in window)
51 )
52 return setTimeout(hydrate, 0);
53
54 requestIdleCallback(
55 () => {
56 requestAnimationFrame(hydrate);
57 },
58 { timeout: 500 }
59 );
60 break;
61 }
62 case "visible": {
63 if (!("IntersectionObserver" in window)) return hydrate();
64
65 const observer = createObserver(hydrate);
66 const childElements = fragment.childNodes.filter(
67 (node) => node.nodeType === node.ELEMENT_NODE
68 );
69 for (const child of childElements) {
70 observer.observe(child);
71 }
72 break;
73 }
74 }
75}
76
77function createPersistentFragment(parentNode, childNodes) {
78 const last = childNodes && childNodes[childNodes.length - 1].nextSibling;
79 function insert(child, before) {
80 try {
81 parentNode.insertBefore(child, before || last);
82 } catch (e) {}
83 }
84 return {
85 parentNode,
86 firstChild: childNodes[0],
87 childNodes,
88 appendChild: insert,
89 insertBefore: insert,
90 removeChild(child) {
91 parentNode.removeChild(child);
92 },
93 };
94}
95
96const ATTR_REGEX = /(:?\w+)=(\w+|[{[].*?[}\]])/g;
97function parseHydrateBoundary(node) {
98 if (!node.textContent) return {};
99 const text = node.textContent.slice("?h ".length, -1);
100
101 let props = {};
102 let result = ATTR_REGEX.exec(text);
103 while (result) {
104 let [, attr, val] = result;
105 if (attr === "p") {
106 props[attr] = JSON.parse(val);
107 } else {
108 props[attr] = val;
109 }
110 result = ATTR_REGEX.exec(text);
111 }
112 return props;
113}
114
115function findHydrationPoints() {
116 const nodeIterator = document.createNodeIterator(
117 document.documentElement,
118 NodeFilter.SHOW_COMMENT,
119 {
120 acceptNode(node) {
121 if (node.textContent && node.textContent.startsWith("?h c"))
122 return NodeFilter.FILTER_ACCEPT;
123 return NodeFilter.FILTER_REJECT;
124 },
125 }
126 );
127
128 const toHydrate = [];
129
130 while (nodeIterator.nextNode()) {
131 const start = nodeIterator.referenceNode;
132 const data = parseHydrateBoundary(start);
133 const childNodes = [];
134
135 let end = start.nextSibling;
136 while (end) {
137 if (
138 end.nodeType === end.COMMENT_NODE &&
139 end.textContent &&
140 end.textContent.startsWith("?h p")
141 ) {
142 Object.assign(data, parseHydrateBoundary(end));
143 break;
144 }
145 childNodes.push(end);
146 end = end.nextSibling;
147 }
148
149 toHydrate.push([
150 createPersistentFragment(start.parentNode, childNodes),
151 data,
152 [start, end],
153 ]);
154 }
155 return toHydrate;
156}
157
158export default (manifest) => {
159 const init = () => {
160 const $cmps = findHydrationPoints();
161
162 for (const [fragment, data, markers] of $cmps) {
163 const { c: Component } = data;
164 const [name, source] = manifest[Component];
165 if (name && source) {
166 attach(fragment, data, { key: Component, name, source });
167 }
168
169 requestIdleCallback(() => {
170 markers.forEach((marker) => marker.remove());
171 });
172 }
173 };
174
175 requestIdleCallback(init, { timeout: 1000 });
176};