Skip to content

Commit b7b288c

Browse files
committed
Add doctype html and changeset
1 parent c63d294 commit b7b288c

File tree

3 files changed

+64
-4
lines changed

3 files changed

+64
-4
lines changed

.changeset/lovely-ways-smoke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'preact-render-to-string': patch
3+
---
4+
5+
Fix issues regarding streaming full HTML documents

src/lib/chunked.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ export async function renderToChunks(vnode, { context, onWrite, abortSignal }) {
3232
// and causes browsers to reject the content. Instead, we inject the deferred
3333
// content before the closing tags, then emit them last.
3434
const docSuffixIndex = getDocumentClosingTagsIndex(shell);
35-
onWrite(docSuffixIndex !== -1 ? shell.slice(0, docSuffixIndex) : shell);
35+
const hasHtmlTag = shell.trimStart().startsWith('<html');
36+
const initialWrite =
37+
docSuffixIndex !== -1 ? shell.slice(0, docSuffixIndex) : shell;
38+
const prefix = hasHtmlTag ? '<!DOCTYPE html>' : '';
39+
onWrite(prefix + initialWrite);
3640
onWrite('<div hidden>');
3741
onWrite(createInitScript(len));
3842
// We should keep checking all promises
@@ -51,7 +55,7 @@ export async function renderToChunks(vnode, { context, onWrite, abortSignal }) {
5155
* @returns {number}
5256
*/
5357
function getDocumentClosingTagsIndex(html) {
54-
return html.indexOf('</body>');
58+
return html.lastIndexOf('</body>');
5559
}
5660

5761
async function forkPromises(renderer) {

test/compat/render-chunked.test.jsx

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,57 @@ describe('renderToChunks', () => {
228228
expect(result[result.length - 1]).toBe('</body></html>');
229229
});
230230

231+
it('should prepend <!DOCTYPE html> when rendering a full document with suspended content', async () => {
232+
const { Suspender, suspended } = createSuspender();
233+
234+
const result = [];
235+
const promise = renderToChunks(
236+
<html>
237+
<head>
238+
<title>Test</title>
239+
</head>
240+
<body>
241+
<Suspense fallback="loading...">
242+
<Suspender />
243+
</Suspense>
244+
</body>
245+
</html>,
246+
{ onWrite: (s) => result.push(s) }
247+
);
248+
suspended.resolve();
249+
await promise;
250+
251+
// The first chunk must be prefixed with <!DOCTYPE html>
252+
expect(result[0].startsWith('<!DOCTYPE html>')).toBe(true);
253+
254+
// The full output must start with the doctype
255+
const fullHtml = result.join('');
256+
expect(fullHtml.startsWith('<!DOCTYPE html>')).toBe(true);
257+
258+
// The doctype should appear exactly once
259+
const doctypeCount = (fullHtml.match(/<!DOCTYPE html>/gi) || []).length;
260+
expect(doctypeCount).toBe(1);
261+
});
262+
263+
it('should not prepend <!DOCTYPE html> when rendering a non-document fragment with suspended content', async () => {
264+
const { Suspender, suspended } = createSuspender();
265+
266+
const result = [];
267+
const promise = renderToChunks(
268+
<div>
269+
<Suspense fallback="loading...">
270+
<Suspender />
271+
</Suspense>
272+
</div>,
273+
{ onWrite: (s) => result.push(s) }
274+
);
275+
suspended.resolve();
276+
await promise;
277+
278+
const fullHtml = result.join('');
279+
expect(fullHtml.includes('<!DOCTYPE html>')).toBe(false);
280+
});
281+
231282
it('should support a component that suspends multiple times', async () => {
232283
const { Suspender, suspended } = createSuspender();
233284
const { Suspender: Suspender2, suspended: suspended2 } = createSuspender();
@@ -254,10 +305,10 @@ describe('renderToChunks', () => {
254305
await promise;
255306

256307
expect(result).to.deep.equal([
257-
'<div><!--preact-island:57-->loading part 1...<!--/preact-island:57--></div>',
308+
'<div><!--preact-island:70-->loading part 1...<!--/preact-island:70--></div>',
258309
'<div hidden>',
259310
createInitScript(1),
260-
createSubtree('57', '<p>it works</p><p>it works</p>'),
311+
createSubtree('70', '<p>it works</p><p>it works</p>'),
261312
'</div>'
262313
]);
263314
});

0 commit comments

Comments
 (0)