Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit 55f0482

Browse files
committed
Improve ssr renderer
1 parent 207910f commit 55f0482

File tree

5 files changed

+265
-171
lines changed

5 files changed

+265
-171
lines changed

commands/dev.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ if (import.meta.main) {
103103
clientDependencyGraph?.update(specifier);
104104
serverDependencyGraph?.update(specifier);
105105
}
106+
if (specifier === "./index.html") {
107+
Reflect.deleteProperty(globalThis, "__ALEPH_INDEX_HTML");
108+
}
106109
if (kind === "modify") {
107110
emitters.forEach((e) => {
108111
e.emit(`modify:${specifier}`, { specifier });

lib/html.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

server/html.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { concat } from "https://deno.land/[email protected]/bytes/mod.ts";
2+
import type { Comment, DocumentEnd, Element } from "https://deno.land/x/[email protected]/types.d.ts";
3+
import initLolHtml, { HTMLRewriter } from "https://deno.land/x/[email protected]/mod.js";
4+
import decodeLolHtmlWasm from "https://deno.land/x/[email protected]/wasm.js";
5+
import { toLocalPath } from "../lib/helpers.ts";
6+
import util from "../lib/util.ts";
7+
import { getAlephPkgUri } from "./config.ts";
8+
9+
await initLolHtml(decodeLolHtmlWasm());
10+
11+
// laod the `index.html`
12+
// - fix relative url to absolute url of `src` and `href`
13+
// - add `./framework/core/hmr.ts` when in `development` mode
14+
// - add `./framework/core/nomodule.ts`
15+
// - check the `<head>` and `<body>` elements
16+
// - check the `<ssr-body>` element if the ssr is enabled
17+
// - add `data-suspense` attribute to `<body>` if using suspense ssr
18+
export async function loadAndFixIndexHtml(isDev: boolean, ssr?: { suspense?: boolean }): Promise<Uint8Array> {
19+
const { html, hasSSRBody } = await loadIndexHtml();
20+
return fixIndexHtml(html, { isDev, ssr, hasSSRBody });
21+
}
22+
23+
async function loadIndexHtml(): Promise<{ html: Uint8Array; hasSSRBody: boolean }> {
24+
const chunks: Uint8Array[] = [];
25+
let hasHead = false;
26+
let hasBody = false;
27+
let hasSSRBody = false;
28+
const rewriter = new HTMLRewriter("utf8", (chunk: Uint8Array) => chunks.push(chunk));
29+
30+
rewriter.on("head", {
31+
element: () => hasHead = true,
32+
});
33+
rewriter.on("body", {
34+
element: () => hasBody = true,
35+
});
36+
rewriter.on("ssr-body", {
37+
element: () => hasSSRBody = true,
38+
});
39+
rewriter.on("*", {
40+
comments: (c: Comment) => {
41+
const text = c.text.trim();
42+
if (text === "ssr-body" || text === "ssr-output") {
43+
if (hasSSRBody) {
44+
c.remove();
45+
} else {
46+
c.replace("<ssr-body></ssr-body>", { html: true });
47+
hasSSRBody = true;
48+
}
49+
}
50+
},
51+
});
52+
rewriter.onDocument({
53+
end: (end: DocumentEnd) => {
54+
if (!hasHead) {
55+
end.append(`<head></head>`, { html: true });
56+
}
57+
if (!hasBody) {
58+
end.append(`<body></body>`, { html: true });
59+
}
60+
},
61+
});
62+
63+
try {
64+
rewriter.write(await Deno.readFile("index.html"));
65+
rewriter.end();
66+
return {
67+
html: concat(...chunks),
68+
hasSSRBody,
69+
};
70+
} catch (err) {
71+
throw err;
72+
} finally {
73+
rewriter.free();
74+
}
75+
}
76+
77+
function fixIndexHtml(
78+
html: Uint8Array,
79+
options: { isDev: boolean; ssr?: { suspense?: boolean }; hasSSRBody: boolean },
80+
): Uint8Array {
81+
const { isDev, ssr, hasSSRBody } = options;
82+
const alephPkgUri = getAlephPkgUri();
83+
const chunks: Uint8Array[] = [];
84+
const rewriter = new HTMLRewriter("utf8", (chunk: Uint8Array) => chunks.push(chunk));
85+
86+
rewriter.on("link", {
87+
element: (el: Element) => {
88+
let href = el.getAttribute("href");
89+
if (href) {
90+
const isHttpUrl = util.isLikelyHttpURL(href);
91+
if (!isHttpUrl) {
92+
href = util.cleanPath(href);
93+
el.setAttribute("href", href);
94+
}
95+
if (href.endsWith(".css") && !isHttpUrl && isDev) {
96+
const specifier = `.${href}`;
97+
el.setAttribute("data-module-id", specifier);
98+
el.after(
99+
`<script type="module">import hot from "${toLocalPath(alephPkgUri)}/framework/core/hmr.ts";hot(${
100+
JSON.stringify(specifier)
101+
}).accept();</script>`,
102+
{ html: true },
103+
);
104+
}
105+
}
106+
},
107+
});
108+
let nomoduleInserted = false;
109+
rewriter.on("script", {
110+
element: (el: Element) => {
111+
const src = el.getAttribute("src");
112+
if (src && !util.isLikelyHttpURL(src)) {
113+
el.setAttribute("src", util.cleanPath(src));
114+
}
115+
if (!nomoduleInserted && el.getAttribute("type") === "module") {
116+
el.after(
117+
`<script nomodule src="${toLocalPath(alephPkgUri)}/framework/core/nomodule.ts"></script>`,
118+
{ html: true },
119+
);
120+
nomoduleInserted = true;
121+
}
122+
},
123+
});
124+
rewriter.on("head", {
125+
element: (el: Element) => {
126+
if (isDev) {
127+
el.append(
128+
`<script type="module">import hot from "${
129+
toLocalPath(alephPkgUri)
130+
}/framework/core/hmr.ts";hot("./index.html").decline();</script>`,
131+
{ html: true },
132+
);
133+
}
134+
},
135+
});
136+
if (!hasSSRBody && ssr) {
137+
rewriter.on("body", {
138+
element: (el: Element) => {
139+
el.prepend("<ssr-body></ssr-body>", { html: true });
140+
},
141+
});
142+
}
143+
if (ssr?.suspense) {
144+
rewriter.on("body", {
145+
element: (el: Element) => {
146+
el.setAttribute("data-suspense", "true");
147+
},
148+
});
149+
}
150+
151+
try {
152+
rewriter.write(html);
153+
rewriter.end();
154+
return concat(...chunks);
155+
} catch (err) {
156+
throw err;
157+
} finally {
158+
rewriter.free();
159+
}
160+
}
161+
162+
export function parseHtmlLinks(html: string | Uint8Array): Promise<string[]> {
163+
return new Promise((resolve, reject) => {
164+
try {
165+
const links: string[] = [];
166+
const rewriter = new HTMLRewriter("utf8", (_chunk: Uint8Array) => {});
167+
rewriter.on("link", {
168+
element(el: Element) {
169+
const href = el.getAttribute("href");
170+
if (href) {
171+
links.push(href);
172+
}
173+
},
174+
});
175+
rewriter.on("script", {
176+
element(el: Element) {
177+
const src = el.getAttribute("src");
178+
if (src) {
179+
links.push(src);
180+
}
181+
},
182+
});
183+
rewriter.onDocument({
184+
end: () => {
185+
resolve(links);
186+
},
187+
});
188+
try {
189+
rewriter.write(typeof html === "string" ? util.utf8TextEncoder.encode(html) : html);
190+
rewriter.end();
191+
} finally {
192+
rewriter.free();
193+
}
194+
} catch (error) {
195+
reject(error);
196+
}
197+
});
198+
}
199+
200+
export { Comment, Element, HTMLRewriter };

server/mod.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Routes } from "../lib/route.ts";
77
import util from "../lib/util.ts";
88
import { VERSION } from "../version.ts";
99
import { initModuleLoaders, loadImportMap, loadJSXConfig } from "./config.ts";
10+
import { loadAndFixIndexHtml } from "./html.ts";
1011
import type { HTMLRewriterHandlers, SSR } from "./renderer.ts";
1112
import renderer from "./renderer.ts";
1213
import { content, type CookieOptions, json, setCookieHeader } from "./response.ts";
@@ -257,10 +258,10 @@ export const serve = (options: ServerOptions = {}) => {
257258
}
258259

259260
// load the `index.html`
260-
let indexHtml: string | null | undefined = Reflect.get(globalThis, "__ALEPH_INDEX_HTML");
261+
let indexHtml: Uint8Array | null | undefined = Reflect.get(globalThis, "__ALEPH_INDEX_HTML");
261262
if (indexHtml === undefined) {
262263
try {
263-
indexHtml = await Deno.readTextFile("./index.html");
264+
indexHtml = await loadAndFixIndexHtml(isDev, typeof ssr === "function" ? {} : ssr);
264265
} catch (err) {
265266
if (err instanceof Deno.errors.NotFound) {
266267
indexHtml = null;
@@ -270,11 +271,8 @@ export const serve = (options: ServerOptions = {}) => {
270271
}
271272
}
272273
}
273-
274-
// cache indexHtml to global(memory) in production mode
275-
if (!isDev) {
276-
Reflect.set(globalThis, "__ALEPH_INDEX_HTML", indexHtml);
277-
}
274+
// cache `index.html` to memory
275+
Reflect.set(globalThis, "__ALEPH_INDEX_HTML", indexHtml);
278276

279277
// no root `index.html` found
280278
if (indexHtml === null) {
@@ -296,7 +294,6 @@ export const serve = (options: ServerOptions = {}) => {
296294
indexHtml,
297295
routes,
298296
customHTMLRewriter,
299-
isDev,
300297
ssr,
301298
});
302299
};

0 commit comments

Comments
 (0)