-
Notifications
You must be signed in to change notification settings - Fork 3
/
hydration.ts
228 lines (208 loc) · 6.05 KB
/
hydration.ts
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import { HeadConfig, renderHeadConfigToString } from "./head.js";
import { convertToPosixPath } from "./paths.js";
import { TemplateRenderProps } from "./types.js";
/**
* Imports the custom hydration template and entrypoint template as modules and calls
* the render function.
*
* Dev has a separate function than {@link getHydrationTemplate} due to how Vite messes
* with the import.meta.url.
*
* @param clientRenderTemplatePath the path to the custom client render template
* @param templateModulePath the path to the template module
* @param props the {@link TemplateRenderProps}
* @returns the HTML as a string
*/
export const getHydrationTemplateDev = (
clientRenderTemplatePath: string,
templateModulePath: string,
props: TemplateRenderProps
): string => {
return `
import {default as Component} from "${convertToPosixPath(
templateModulePath
)}";
import {render} from "${convertToPosixPath(clientRenderTemplatePath)}";
render(
{
Page: Component,
pageProps: ${JSON.stringify(props)},
}
);
`;
};
/**
* Imports the custom hydration template and entrypoint template as modules and calls
* the render function.
*
* The component paths need to be resolved to the current domain's relative path in order
* to support reverse proxies.
*
* @param clientRenderTemplatePath the path to the custom client render template
* @param templateModulePath the path to the template module
* @param props the {@link TemplateRenderProps}
* @returns the HTML as a string
*/
export const getHydrationTemplate = (
clientRenderTemplatePath: string,
templateModulePath: string,
props: TemplateRenderProps
): string => {
return `
const componentUrl = import.meta.resolve("./${convertToPosixPath(
templateModulePath
)}");
const renderUrl = import.meta.resolve("./${convertToPosixPath(
clientRenderTemplatePath
)}");
const component = await import(componentUrl);
const render = await import(renderUrl);
render.render(
{
Page: component.default,
pageProps: ${JSON.stringify(props)},
}
);
`;
};
/**
* Get the server template with injected html common to both the dev and plugin side of things.
* For the most part, injects data into the <head> tag. It also provides validation.
*
* @param clientHydrationString if this is undefined then hydration is skipped
* @param serverHtml
* @param appLanguage
* @param headConfig
* @returns the server template with injected html
*/
const getCommonInjectedServerHtml = (
clientHydrationString: string | undefined,
serverHtml: string,
appLanguage: string,
headConfig?: HeadConfig
): string => {
// Add the language to the <html> tag if it exists
serverHtml.replace("<!--app-lang-->", appLanguage);
if (clientHydrationString) {
serverHtml = injectIntoHead(
serverHtml,
`<script type="module">${clientHydrationString}</script>`
);
}
if (headConfig) {
serverHtml = injectIntoHead(
serverHtml,
renderHeadConfigToString(headConfig)
);
}
return serverHtml;
};
/**
* Use for the Vite dev server.
*
* @param clientHydrationString
* @param serverHtml
* @param appLanguage
* @param headConfig
* @returns the server template to render in the Vite dev environment
*/
export const getServerTemplateDev = (
clientHydrationString: string | undefined,
serverHtml: string,
appLanguage: string,
headConfig?: HeadConfig
): string => {
return getCommonInjectedServerHtml(
clientHydrationString,
serverHtml,
appLanguage,
headConfig
);
};
/**
* Used for the Deno plugin execution context. The major difference between this function
* and {@link getServerTemplateDev} is that it also injects the CSS import tags which is
* not required by Vite since those are injected automatically by the Vite dev server.
*
* @param clientHydrationString
* @param serverHtml
* @param templateFilepath
* @param bundlerManifest
* @param relativePrefixToRoot
* @param appLanguage
* @param headConfig
* @returns the server template to render in the Deno plugin execution context when rendering HTML
*/
export const getServerTemplatePlugin = (
clientHydrationString: string | undefined,
serverHtml: string,
templateFilepath: string,
bundlerManifest: bundlerManifest,
relativePrefixToRoot: string,
appLanguage: string,
headConfig?: HeadConfig
) => {
let html = getCommonInjectedServerHtml(
clientHydrationString,
serverHtml,
appLanguage,
headConfig
);
html = injectIntoHead(
html,
getCssHtml(templateFilepath, bundlerManifest, relativePrefixToRoot)
);
return html;
};
type chunkName = string;
type bundlerManifest = Record<chunkName, ManifestInfo>;
type ManifestInfo = {
file: string;
src: string;
isEntry: boolean;
imports?: string[];
css: string[];
};
const getCssHtml = (
templateFilepath: string,
bundlerManifest: bundlerManifest,
relativePrefixToRoot: string
): string => {
return Array.from(getCssTags(templateFilepath, bundlerManifest, new Set()))
.map((f) => `<link rel="stylesheet" href="${relativePrefixToRoot + f}"/>`)
.join("\n");
};
const getCssTags = (
filepath: string,
manifest: bundlerManifest,
seen: Set<string>
): Set<string> => {
const entry = structuredClone(
Object.entries(manifest).find(([file]) => file === filepath)
);
if (!entry) {
return new Set();
}
const [file, info] = entry;
seen.add(file);
const cssFiles = new Set(info.css);
(info.imports || [])
.flatMap((f) => Array.from(getCssTags(f, manifest, seen)))
.forEach((f) => cssFiles.add(f));
return cssFiles;
};
/**
* Finds the closing </head> tag and injects the input string into it.
* @param html
*/
const injectIntoHead = (html: string, stringToInject: string): string => {
const closingHeadIndex = html.indexOf("</head>");
if (closingHeadIndex === -1) {
throw new Error("_server.tsx: No head tag is defined");
}
return (
html.slice(0, closingHeadIndex) +
stringToInject +
html.slice(closingHeadIndex)
);
};