Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: streaming rendering with Suspense boundaries as flush trigger #296

Merged
merged 38 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5799b58
Disable eslint lines-around-comment rule
marvinhagemeister Oct 15, 2022
8b104e2
Update test scripts to allow watch usage
marvinhagemeister Oct 16, 2022
20d189b
Add streaming renderer
marvinhagemeister Oct 16, 2022
c2ae535
Switch to element nodes as markers instead
marvinhagemeister Oct 19, 2022
bee9748
Switch away from global ids
marvinhagemeister Oct 19, 2022
3e6afe2
Remove subtree option
marvinhagemeister Oct 19, 2022
a84fca4
feat: use comments instead of element as marker
jacob-ebey Oct 31, 2022
d61cda8
chore: use NodeIterator to locate comments
jacob-ebey Nov 1, 2022
bca7c53
chore: remove redundancy and minify code
jacob-ebey Nov 1, 2022
31f1839
more minification
jacob-ebey Nov 1, 2022
3ad1d62
even more minification
jacob-ebey Nov 1, 2022
95c42d3
Merge pull request #260 from jacob-ebey/comments-ce-script
marvinhagemeister Nov 8, 2022
5b06b07
Merge branch 'master' into streaming-render
marvinhagemeister Nov 8, 2022
7b4fefa
Move files to new test dir structure
marvinhagemeister Nov 8, 2022
6771eda
Fix linting error
marvinhagemeister Nov 8, 2022
14cf8ce
Merge branch 'master' into streaming-render
marvinhagemeister Nov 8, 2022
02ac131
fix ts types
developit Nov 13, 2022
1e7d3c1
fix Web Streams tests on Node <18
developit Nov 13, 2022
96f9f4b
Streaming renderer: factor chunking out of main entrypoint and rebase…
developit Feb 2, 2023
34966cd
Merge branch 'master' into streaming-render
developit Feb 3, 2023
27dbdce
fix d8 bench path
developit Feb 3, 2023
7590c6f
Merge branch 'master' into streaming-render-update
JoviDeCroock Mar 30, 2023
189cdb8
try new way of getting mask as we are not setting it anymore
JoviDeCroock Mar 30, 2023
aa93b8b
stop interfering with the real useId
JoviDeCroock Mar 30, 2023
bfa808f
show bug
JoviDeCroock Mar 30, 2023
1579648
partial fix
JoviDeCroock Mar 30, 2023
216ef73
continuously fork promises
JoviDeCroock Mar 30, 2023
cbabc9e
fix tests
JoviDeCroock Mar 30, 2023
76d0e91
Merge branch 'main' into streaming-render-update
JoviDeCroock Jul 19, 2023
1f06bbf
fixes
JoviDeCroock Jul 19, 2023
7b52e34
Merge branch 'main' into streaming-render-update
JoviDeCroock Sep 7, 2023
25ab473
update lockfiles
JoviDeCroock Sep 7, 2023
58631fb
Create twelve-candles-walk.md
JoviDeCroock Sep 7, 2023
4a95b37
add build command
JoviDeCroock Sep 8, 2023
0561720
Merge branch 'main' into streaming-render-update
JoviDeCroock May 6, 2024
577a976
fix rebase issues
JoviDeCroock May 6, 2024
b5bcd7b
address comments
JoviDeCroock May 6, 2024
cb505ac
bump deps
JoviDeCroock May 6, 2024
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
5 changes: 5 additions & 0 deletions .changeset/grumpy-kings-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'preact-render-to-string': minor
---

Introduce a streaming renderer which can be imported from `preact-render-to-string/stream` and `preact-render-to-string/stream-node`
5 changes: 5 additions & 0 deletions .changeset/twelve-candles-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"preact-render-to-string": patch
---

streaming rendering with Suspense boundaries as flush trigger
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
/npm-debug.log
.DS_Store
/src/preact-render-to-string-tests.d.ts
/benchmarks/.v8.mjs
/benchmarks/.v8.modern.js
/demo/node_modules
12 changes: 12 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/entry-client.jsx"></script>
</body>
</html>
2,165 changes: 2,165 additions & 0 deletions demo/package-lock.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "server.js",
"type": "module",
"scripts": {
"dev": "vite --force",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server"
},
"author": "",
"license": "ISC",
"devDependencies": {
"vite": "^4.1.4"
},
"dependencies": {
"@preact/preset-vite": "^2.8.0",
"express": "^4.18.2",
"graphql": "^16.6.0",
"preact": "^10.21.0",
"urql": "latest"
}
}
41 changes: 41 additions & 0 deletions demo/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { h } from 'preact';
import { Suspense, lazy } from 'preact/compat';
import { Client, Provider, cacheExchange, fetchExchange } from 'urql';

const client = new Client({
url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
exchanges: [cacheExchange, fetchExchange],
suspense: true
});

export function App({ head }) {
const Pokemons = lazy(
() =>
new Promise((res) => {
setTimeout(
() => {
res(import('./Pokemons.jsx'));
},
typeof document === 'undefined' ? 500 : 3000
);
})
);
return (
<html>
<head dangerouslySetInnerHTML={{ __html: head }} />
<body>
<Provider value={client}>
<main>
<h1>Our Counter application</h1>
<Suspense fallback={<p>Loading...</p>}>
<Pokemons />
</Suspense>
</main>
{import.meta.env.DEV && (
<script type="module" src="/src/entry-client.jsx" />
)}
</Provider>
</body>
</html>
);
}
38 changes: 38 additions & 0 deletions demo/src/Pokemons.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { gql, useQuery } from 'urql';
import { h } from 'preact';

const POKEMONS_QUERY = gql`
query Pokemons($limit: Int!) {
pokemons(limit: $limit) {
id
name
}
}
`;

const Counter = () => {
const [result] = useQuery({
query: POKEMONS_QUERY,
variables: { limit: 10 }
});

const { data, fetching, error } = result;
console.log('hydrated!');
return (
<div>
{fetching && <p>Loading...</p>}

{error && <p>Oh no... {error.message}</p>}

{data && (
<ul>
{data.pokemons.map((pokemon) => (
<li key={pokemon.id}>{pokemon.name}</li>
))}
</ul>
)}
</div>
);
};

export default Counter;
19 changes: 19 additions & 0 deletions demo/src/entry-client.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { hydrate } from 'preact';
import { App } from './App';

const config = { attributes: true, childList: true, subtree: true };
const mut = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.', mutation);
} else if (mutation.type === 'attributes') {
console.log(
`The ${mutation.attributeName} attribute was modified.`,
mutation
);
}
}
});
mut.observe(document, config);

hydrate(<App />, document);
25 changes: 25 additions & 0 deletions demo/src/entry-server.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { renderToPipeableStream } from '../../src/stream-node';
import { App } from './App';

export function render({ res, head }) {
res.socket.on('error', (error) => {
console.error('Fatal', error);
});
const { pipe, abort } = renderToPipeableStream(<App head={head} />, {
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
},
onErrorShell(error) {
res.statusCode = 500;
res.send(
`<!doctype html><p>An error ocurred:</p><pre>${error.message}</pre>`
);
}
});

// Abandon and switch to client rendering if enough time passes.
// Try lowering this to see the client recover.
setTimeout(abort, 20000);
}
60 changes: 60 additions & 0 deletions demo/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import path from 'path';
import { promises as fs } from 'fs';

function ssrPlugin() {
return {
name: 'ssrPlugin',

configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (req.url !== '/') {
return next();
}

const { render } = await server.ssrLoadModule(
path.resolve(__dirname, './src/entry-server')
);

// setTimeout(abort, 10000);

const indexHtml = await fs.readFile(
path.resolve(__dirname, './index.html'),
'utf-8'
);

const url = new URL('http://localhost:5173/' + req.url);
const template = await server.transformIndexHtml(
url.toString(),
indexHtml
);

const head = template.match(/<head>(.+?)<\/head>/s)[1];

return render({ res, head });
});
}
};
}

export default defineConfig({
// @ts-ignore
ssr: {
noExternal: /./
},
build: {
ssrManifest: true,
commonjsOptions: {
transformMixedEsModules: true
}
},
resolve: {
alias: {
preact: path.resolve(__dirname, './node_modules/preact'),
'preact/compat': path.resolve(__dirname, './node_modules/preact/compat'),
'preact/hooks': path.resolve(__dirname, './node_modules/preact/hooks')
}
},
plugins: [ssrPlugin(), preact()]
});
2 changes: 1 addition & 1 deletion jsx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Options {
skipFalseAttributes?: boolean;
}

export default function renderToStringPretty(
export default function render(
vnode: VNode,
context?: any,
options?: Options
Expand Down
Loading