-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
routePlanForContentWithCss.mjs
144 lines (122 loc) · 4.74 KB
/
routePlanForContentWithCss.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// @ts-check
import { createElement as h } from "react";
import documentHasStyleSheet from "./documentHasStyleSheet.mjs";
import HeadManager from "./HeadManager.mjs";
import LinkCss from "./LinkCss.mjs";
/**
* Creates a Ruck app route plan for route content with CSS dependencies that
* must be loaded in the document head before the route content mounts and be
* removed again when navigation to this route aborts, or after navigation to
* the next route for a different page.
* @param {RouteContentWithCss
* | Promise<RouteContentWithCss>} routeContentWithCss Route content with CSS
* dependencies.
* @param {import("./HeadManager.mjs").default} headManager Head tag manager.
* @param {boolean} isInitialRoute Is it the initial route.
* @returns {import("./serve.mjs").RoutePlan}
*/
export default function routePlanForContentWithCss(
routeContentWithCss,
headManager,
isInitialRoute,
) {
if (typeof routeContentWithCss !== "object" || !routeContentWithCss) {
throw new TypeError(
"Argument 1 `routeContentWithCss` must be an object or promise.",
);
}
if (!(headManager instanceof HeadManager)) {
throw new TypeError(
"Argument 2 `headManager` must be a `HeadManager` instance.",
);
}
if (typeof isInitialRoute !== "boolean") {
throw new TypeError("Argument 3 `isInitialRoute` must be a boolean.");
}
/** @type {Set<import("react").ReactNode>} */
const links = new Set();
/** @type {(() => void) | null} */
let endPoll = null;
return {
content: Promise.resolve(routeContentWithCss).then(
async (resolvedRouteContent) => {
if (typeof routeContentWithCss !== "object" || !routeContentWithCss) {
throw new TypeError(
"Function `routePlanForContentWithCss` argument 1 `routeContentWithCss` must resolve an object.",
);
}
if (!(resolvedRouteContent.css instanceof Set)) {
throw new TypeError("Export `css` must be a `Set` instance.");
}
for (const href of resolvedRouteContent.css) {
if (typeof href !== "string") {
throw new TypeError("CSS `href` must be a string.");
}
const link = h(LinkCss, { href });
links.add(link);
headManager.add(
// Todo: Unopinionated way to name the key.
`2-${href}`,
link,
);
}
// Skip waiting for CSS loading for the initial route. It doesn’t make
// sense to wait for CSS to load in SSR, but SSR only involves the
// initial route anyway.
if (typeof document !== "undefined" && !isInitialRoute) {
// Await the stylesheets adding to the DOM and loading. Any that are
// already added to the DOM need to be awaited loading, if they
// haven’t loaded yet, but not if it’s initial SSR page load hydration
// time.
// Poll until all the route CSS dependencies have loaded (regardless
// of success or failure due to 404s, unparsable CSS, etc.) or route
// cleanup (due to abort) happens.
await /** @type {Promise<void>} */ (
new Promise((resolve) => {
endPoll = () => {
endPoll = null;
clearInterval(interval);
// Wait for the CSS to apply to the document.
setTimeout(resolve, 50);
};
const interval = setInterval(() => {
// The wait for the style sheets to load is done when the
// document has every one, ignoring ones who’s loading status
// can’t be checked.
let done = true;
for (const href of resolvedRouteContent.css) {
try {
done = documentHasStyleSheet(href);
} catch (cause) {
// This style sheet’s loading status can’t be checked so
// it’s ignored.
console.error(
new Error(
`Check if the document has stylesheet ${href} failed.`,
{ cause },
),
);
}
if (!done) break;
}
if (done && endPoll) endPoll();
}, 10);
})
);
}
return resolvedRouteContent.content;
},
),
// Shouldn’t run during SSR.
cleanup() {
if (endPoll) endPoll();
for (const link of links) headManager.remove(link);
},
};
}
/**
* Ruck app route content with CSS dependencies.
* @typedef {object} RouteContentWithCss
* @prop {import("react").ReactNode} content Content.
* @prop {Set<string>} css CSS absolute or relative URLs.
*/