1 | import { h, hydrate as rehydrate, render } from "preact";
|
2 |
|
3 | if (!("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 |
|
21 | const 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 |
|
37 | function 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 |
|
83 | function 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 |
|
102 | const ATTR_REGEX = /(:?\w+)=(\w+|[{[].*?[}\]])/g;
|
103 | function 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 |
|
121 | function 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 |
|
164 | export 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 | };
|