UNPKG

4.54 kBJavaScriptView Raw
1import { h, hydrate as rehydrate, render } 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 }, cb) {
38 const { p: { children = null, ...props } = {}, m: method = "idle", f: flush } = 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
45 if (flush) {
46 render(h(Component, props, children), fragment);
47 } else {
48 rehydrate(h(Component, props, children), fragment);
49 }
50 if (cb) cb();
51 };
52
53 switch (method) {
54 case "idle": {
55 if (
56 !("requestAnimationFrame" in window)
57 )
58 return setTimeout(hydrate, 0);
59
60 requestIdleCallback(
61 () => {
62 requestAnimationFrame(hydrate);
63 },
64 { timeout: 500 }
65 );
66 break;
67 }
68 case "visible": {
69 if (!("IntersectionObserver" in window)) return hydrate();
70
71 const observer = createObserver(hydrate);
72 const childElements = fragment.childNodes.filter(
73 (node) => node.nodeType === node.ELEMENT_NODE
74 );
75 for (const child of childElements) {
76 observer.observe(child);
77 }
78 break;
79 }
80 }
81}
82
83function createPersistentFragment(parentNode, childNodes) {
84 const last = childNodes && childNodes[childNodes.length - 1].nextSibling;
85 function insert(child, before) {
86 try {
87 parentNode.insertBefore(child, before || last);
88 } catch (e) {}
89 }
90 return {
91 parentNode,
92 firstChild: childNodes[0],
93 childNodes,
94 appendChild: insert,
95 insertBefore: insert,
96 removeChild(child) {
97 parentNode.removeChild(child);
98 },
99 };
100}
101
102const ATTR_REGEX = /(:?\w+)=(\w+|[{[].*?[}\]])/g;
103function parseHydrateBoundary(node) {
104 if (!node.textContent) return {};
105 const text = node.textContent.slice("?h ".length, -1);
106
107 let props = {};
108 let result = ATTR_REGEX.exec(text);
109 while (result) {
110 let [, attr, val] = result;
111 if (attr === "p") {
112 props[attr] = JSON.parse(val);
113 } else {
114 props[attr] = val;
115 }
116 result = ATTR_REGEX.exec(text);
117 }
118 return props;
119}
120
121function findHydrationPoints() {
122 const nodeIterator = document.createNodeIterator(
123 document.documentElement,
124 NodeFilter.SHOW_COMMENT,
125 {
126 acceptNode(node) {
127 if (node.textContent && node.textContent.startsWith("?h c"))
128 return NodeFilter.FILTER_ACCEPT;
129 return NodeFilter.FILTER_REJECT;
130 },
131 }
132 );
133
134 const toHydrate = [];
135
136 while (nodeIterator.nextNode()) {
137 const start = nodeIterator.referenceNode;
138 const data = parseHydrateBoundary(start);
139 const childNodes = [];
140
141 let end = start.nextSibling;
142 while (end) {
143 if (
144 end.nodeType === end.COMMENT_NODE &&
145 end.textContent &&
146 end.textContent.startsWith("?h p")
147 ) {
148 Object.assign(data, parseHydrateBoundary(end));
149 break;
150 }
151 childNodes.push(end);
152 end = end.nextSibling;
153 }
154
155 toHydrate.push([
156 createPersistentFragment(start.parentNode, childNodes),
157 data,
158 [start, end],
159 ]);
160 }
161 return toHydrate;
162}
163
164export default (manifest) => {
165 const init = () => {
166 const $cmps = findHydrationPoints();
167
168 for (const [fragment, data, markers] of $cmps) {
169 const { c: Component } = data;
170 const [name, source] = manifest[Component];
171 if (name && source) {
172 attach(fragment, data, { key: Component, name, source }, () => {
173 fragment.childNodes.forEach(child => child.tagName === 'HYDRATE-PLACEHOLDER' ? child.remove() : null);
174 markers.forEach((marker) => marker.remove())
175 });
176 }
177 }
178 };
179
180 requestIdleCallback(init, { timeout: 1000 });
181};