Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions src/lib/chunked.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { renderToString } from '../index.js';
import { CHILD_DID_SUSPEND, COMPONENT, PARENT } from './constants.js';
import { Deferred } from './util.js';
import { createInitScript, createSubtree } from './client.js';
import { renderToString } from "../index.js";
import { CHILD_DID_SUSPEND, COMPONENT, PARENT } from "./constants.js";
import { Deferred } from "./util.js";
import { createInitScript, createSubtree } from "./client.js";

/**
* @param {VNode} vnode
Expand All @@ -17,7 +17,7 @@ export async function renderToChunks(vnode, { context, onWrite, abortSignal }) {
abortSignal,
onWrite,
onError: handleError,
suspended: []
suspended: [],
};

// Synchronously render the shell
Expand All @@ -32,16 +32,14 @@ export async function renderToChunks(vnode, { context, onWrite, abortSignal }) {
// and causes browsers to reject the content. Instead, we inject the deferred
// content before the closing tags, then emit them last.
const docSuffixIndex = getDocumentClosingTagsIndex(shell);
const hasHtmlTag = shell.trimStart().startsWith('<html');
const hasHtmlTag = shell.trimStart().startsWith("<html");
const initialWrite =
docSuffixIndex !== -1 ? shell.slice(0, docSuffixIndex) : shell;
const prefix = hasHtmlTag ? '<!DOCTYPE html>' : '';
const prefix = hasHtmlTag ? "<!DOCTYPE html>" : "";
onWrite(prefix + initialWrite);
onWrite('<div hidden>');
onWrite(createInitScript(len));
// We should keep checking all promises
await forkPromises(renderer);
onWrite('</div>');
if (docSuffixIndex !== -1) onWrite(shell.slice(docSuffixIndex));
} else {
onWrite(shell);
Expand All @@ -55,15 +53,15 @@ export async function renderToChunks(vnode, { context, onWrite, abortSignal }) {
* @returns {number}
*/
function getDocumentClosingTagsIndex(html) {
return html.lastIndexOf('</body>');
return html.lastIndexOf("</body>");
}

async function forkPromises(renderer) {
if (renderer.suspended.length > 0) {
const suspensions = [...renderer.suspended];
await Promise.all(renderer.suspended.map((s) => s.promise));
renderer.suspended = renderer.suspended.filter(
(s) => !suspensions.includes(s)
(s) => !suspensions.includes(s),
);
await forkPromises(renderer);
}
Expand Down Expand Up @@ -91,7 +89,7 @@ function handleError(error, vnode, renderChild) {
if (abortSignal) {
// @ts-ignore 2554 - implicit undefined arg
if (abortSignal.aborted) race.resolve();
else abortSignal.addEventListener('abort', race.resolve);
else abortSignal.addEventListener("abort", race.resolve);
}

const promise = error.then(
Expand All @@ -102,16 +100,18 @@ function handleError(error, vnode, renderChild) {
},
// TODO: Abort and send hydration code snippet to client
// to attempt to recover during hydration
this.onError
this.onError,
);

this.suspended.push({
id,
vnode,
promise: Promise.race([promise, race.promise])
promise: Promise.race([promise, race.promise]),
});

const fallback = renderChild(vnode.props.fallback);

return found ? '' : `<!--$s:${id}-->${fallback}<!--/$s:${id}-->`;
return found
? ""
: `<!--$s:${id}--><?start name="${id}">${fallback}<?end name="${id}"><!--/$s:${id}-->`;
}
92 changes: 54 additions & 38 deletions src/lib/client.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,68 @@
/* eslint-disable no-var, key-spacing, object-curly-spacing, prefer-arrow-callback, semi, keyword-spacing */

// function initPreactIslandElement() {
// class PreactIslandElement extends HTMLElement {
// connectedCallback() {
// var d = this;
// if (!d.isConnected) return;

// let i = this.getAttribute('data-target');
// if (!i) return;
// let template = document.createElement("template");
// template.innerHTML = '<?start name="x">';
// if (template.content.firstChild && template.content.firstChild.nodeType == 7) {
// return;
// }

// var s,
// e,
// c = document.createNodeIterator(document, 128);
// while (c.nextNode()) {
// let n = c.referenceNode;
// function findDirectives(id) {
// var s,
// e,
// c = document.createNodeIterator(document, 128);
// while (c.nextNode()) {
// let n = c.referenceNode;

// if (n.data == '$s:' + i) s = n;
// else if (n.data == '/$s:' + i) e = n;
// if (s && e) break;
// }
// if (s && e && s.parentNode !== document) {
// requestAnimationFrame(() => {
// var p = e.previousSibling;
// while (p != s) {
// if (!p || p == s) break;
// e.parentNode.removeChild(p);
// p = e.previousSibling;
// }
// if (n.data == '?start name="' + id + '"') s = n;
// else if (n.data == '?end name="' + id + '"') e = n;
// if (s && e) return [s, e];
// }
// }

// c = s;
// while (d.firstChild) {
// s = d.firstChild;
// d.removeChild(s);
// c.after(s);
// c = s;
// let observer = new MutationObserver((mutations) => {
// for (let mutation of mutations) {
// for (let node of mutation.addedNodes) {
// if (node.nodeName === "TEMPLATE") {
// let id = node.getAttribute("for");
// if (id) {
// let pair = findDirectives(id);
// if (pair) {
// requestAnimationFrame(() => {
// pair = findDirectives(node.getAttribute("for"));
// if (!pair) {
// node.remove();
// return;
// }
// let [s, e] = pair;
// let p = e.previousSibling;
// while (p != s) {
// if (!p || p == s) break;
// e.parentNode.removeChild(p);
// p = e.previousSibling;
// }
// // TODO: flush this out to better polyfill the browser behavior,
// // this is pretty basic but works as a x-browser POC
// let n = document.createElement("template");
// s.replaceWith(n);
// n.insertAdjacentHTML("beforebegin", node.innerHTML);
// n.remove();
// e.remove();
// node.remove();
// });
// } else {
// node.remove();
// }
// }

// d.parentNode.removeChild(d);
// });
// }
// }
// }
// }

// customElements.define('preact-island', PreactIslandElement);
// });
// observer.observe(document.body, { childList: true, subtree: true });
// }

// To modify the INIT_SCRIPT, uncomment the above code, modify it, and paste it into https://try.terser.org/.
const INIT_SCRIPT = `class e extends HTMLElement{connectedCallback(){var e=this;if(!e.isConnected)return;let t=this.getAttribute("data-target");if(t){for(var r,a,i=document.createNodeIterator(document,128);i.nextNode();){let e=i.referenceNode;if(e.data=="$s:"+t?r=e:e.data=="/$s:"+t&&(a=e),r&&a)break}r&&a&&r.parentNode!==document&&requestAnimationFrame((()=>{for(var t=a.previousSibling;t!=r&&t&&t!=r;)a.parentNode.removeChild(t),t=a.previousSibling;for(i=r;e.firstChild;)r=e.firstChild,e.removeChild(r),i.after(r),i=r;e.parentNode.removeChild(e)}))}}}customElements.define("preact-island",e);`;
const INIT_SCRIPT = `let t=document.createElement("template");if(t.innerHTML='<?start name="x">',t.content.firstChild&&7==t.content.firstChild.nodeType)return;let e=new MutationObserver(e=>{let t=e=>{for(var o,r,n=document.createNodeIterator(document,128);n.nextNode();){let l=n.referenceNode;if(l.data=='?start name="'+e+'"'?o=l:l.data=='?end name="'+e+'"'&&(r=l),o&&r)return[o,r]}};for(let o of e)for(let e of o.addedNodes)if("TEMPLATE"===e.nodeName){let o=e.getAttribute("for");if(o){let r=t(o);r?requestAnimationFrame(()=>{let o=t(e.getAttribute("for"));if(!o)return void e.remove();let[r,n]=o,l=n.previousSibling;for(;l!=r&&l&&l!=r;)n.parentNode.removeChild(l),l=n.previousSibling;let d=document.createElement("template");r.replaceWith(d),d.insertAdjacentHTML("beforebegin",e.innerHTML),d.remove(),n.remove(),e.remove()}):e.remove()}}});e.observe(document.body,{childList:!0,subtree:!0});`;

export function createInitScript() {
return `<script>(function(){${INIT_SCRIPT}}())</script>`;
Expand All @@ -58,5 +74,5 @@ export function createInitScript() {
* @returns {string}
*/
export function createSubtree(id, content) {
return `<preact-island hidden data-target="${id}">${content}</preact-island>`;
return `<template for="${id}">${content}</template>`;
}
28 changes: 8 additions & 20 deletions test/compat/render-chunked.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@ describe('renderToChunks', () => {
await promise;

expect(result).to.deep.equal([
'<div><!--$s:5-->loading...<!--/$s:5--></div>',
'<div hidden>',
'<div><!--$s:5--><?start name="5">loading...<?end name="5"><!--/$s:5--></div>',
createInitScript(),
createSubtree('5', '<p>it works</p>'),
'</div>'
]);
});

Expand All @@ -62,10 +60,8 @@ describe('renderToChunks', () => {
suspended.resolve();

expect(result).to.deep.equal([
'<div><!--$s:10-->loading...<!--/$s:10--></div>',
'<div hidden>',
'<div><!--$s:10--><?start name="10">loading...<?end name="10"><!--/$s:10--></div>',
createInitScript(1),
'</div>'
]);
});

Expand Down Expand Up @@ -109,11 +105,9 @@ describe('renderToChunks', () => {
}

expect(result).to.deep.equal([
'<div><!--$s:16-->loading...<!--/$s:16--></div>',
'<div hidden>',
'<div><!--$s:16--><?start name="16">loading...<?end name="16"><!--/$s:16--></div>',
createInitScript(1),
createSubtree('16', '<p>it works</p>'),
'</div>'
]);
});

Expand Down Expand Up @@ -142,11 +136,9 @@ describe('renderToChunks', () => {
await promise;

expect(result).to.deep.equal([
'<div><p>id: P0-0</p><!--$s:24-->loading...<!--/$s:24--></div>',
'<div hidden>',
'<div><p>id: P0-0</p><!--$s:24--><?start name="24">loading...<?end name="24"><!--/$s:24--></div>',
createInitScript(1),
createSubtree('24', '<p>id: P0-1</p>'),
'</div>'
]);
});

Expand Down Expand Up @@ -182,12 +174,10 @@ describe('renderToChunks', () => {
await promise;

expect(result).toEqual([
'<div><p>id: P0-0</p><!--$s:33-->loading...<!--/$s:33--><!--$s:36-->loading...<!--/$s:36--></div>',
'<div hidden>',
'<div><p>id: P0-0</p><!--$s:33--><?start name="33">loading...<?end name="33"><!--/$s:33--><!--$s:36--><?start name="36">loading...<?end name="36"><!--/$s:36--></div>',
createInitScript(1),
createSubtree('33', '<p>id: P0-1</p>'),
createSubtree('36', '<p>id: P0-2</p>'),
'</div>'
]);
});

Expand All @@ -213,8 +203,8 @@ describe('renderToChunks', () => {

const fullHtml = result.join('');

// Deferred wrapper must appear before </body></html>, not after
const deferredPos = fullHtml.indexOf('<div hidden>');
// Deferred templates must appear before </body></html>, not after
const deferredPos = fullHtml.indexOf('<template for=');
const bodyClosePos = fullHtml.indexOf('</body>');
const htmlClosePos = fullHtml.indexOf('</html>');

Expand Down Expand Up @@ -305,11 +295,9 @@ describe('renderToChunks', () => {
await promise;

expect(result).to.deep.equal([
'<div><!--$s:70-->loading part 1...<!--/$s:70--></div>',
'<div hidden>',
'<div><!--$s:70--><?start name="70">loading part 1...<?end name="70"><!--/$s:70--></div>',
createInitScript(1),
createSubtree('70', '<p>it works</p><p>it works</p>'),
'</div>'
]);
});
});
6 changes: 2 additions & 4 deletions test/compat/stream-node.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,9 @@ describe('renderToPipeableStream', () => {
const result = await sink.promise;

expect(result).to.deep.equal([
'<div><!--$s:5-->loading...<!--/$s:5--></div>',
'<div hidden>',
'<div><!--$s:5--><?start name="5">loading...<?end name="5"><!--/$s:5--></div>',
createInitScript(),
createSubtree('5', '<p>it works</p>'),
'</div>'
createSubtree('5', '<p>it works</p>')
]);
});

Expand Down
6 changes: 2 additions & 4 deletions test/compat/stream.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,9 @@ describe('renderToReadableStream', () => {
const result = await sink.promise;

expect(result).toEqual([
'<div><!--$s:5-->loading...<!--/$s:5--></div>',
'<div hidden>',
'<div><!--$s:5--><?start name="5">loading...<?end name="5"><!--/$s:5--></div>',
createInitScript(),
createSubtree('5', '<p>it works</p>'),
'</div>'
createSubtree('5', '<p>it works</p>')
]);
});
});
Loading