1 | import { h, hydrate as mount } 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 }) {
|
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 |
|
77 | function 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 |
|
96 | const ATTR_REGEX = /(:?\w+)=(\w+|[{[].*?[}\]])/g;
|
97 | function 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 |
|
115 | function 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 |
|
158 | export 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 | };
|