Skip to content

[SSR]: Add server side rendering support for Nala components #943

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
1,306 changes: 1,306 additions & 0 deletions examples/ssr/package-lock.json

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions examples/ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "ssr",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@types/express": "^5.0.0",
"express": "^4.21.2",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}
31 changes: 31 additions & 0 deletions examples/ssr/raw.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using HTML & Web Components"
/>
<link href="variables.css" rel="stylesheet" />
<title>Web Components</title>
<style>
body {
background: var(--leo-color-page-background);
color: var(--leo-color-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
}
</style>
</head>

<body>
<leo-button>Click Me</leo-button>
<leo-button href="https://brave.com" kind="outline"
>External link</leo-button
>
<leo-input placeholder="Input"> Custom Label for Input </leo-input>
<leo-checkbox checked>Checkbox</leo-checkbox>
</body>
</html>
33 changes: 33 additions & 0 deletions examples/ssr/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import express from 'express'
import { render } from '../../src/ssr/render'
import fs from 'fs/promises'

const app = express()

app.get('/icons/:name', async (req, res) => {
const icon = await fs.readFile(`../../icons/${req.params.name}`, 'utf-8')
res.type('svg')
res.send(icon)
})

app.get('/variables.css', async (req, res) => {
const css = await fs.readFile('../../tokens/css/variables.css', 'utf-8')
res.type('css')
res.send(css)
})

app.get('/', async (req, res) => {
const rawHtml = await fs.readFile('./raw.html', 'utf-8')

const html = await render(rawHtml, [
() => import('../../web-components/button'),
() => import('../../web-components/checkbox'),
() => import('../../web-components/input')
])
res.type('html')
res.send(html)
})

app.listen(3000, () => {
console.log('Server is running on port 3000')
})
10 changes: 10 additions & 0 deletions examples/ssr/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "commonjs" /* Specify what module code is generated. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipDefaultLibCheck": true
}
}
25 changes: 22 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,17 @@
},
"peerDependencies": {
"@material/material-color-utilities": ">= 0.2.7",
"happy-dom": ">= 15.11.7",
"react": ">= 16.0.0",
"typescript": ">= 4.7.0"
},
"peerDependenciesMeta": {
"@material/material-color-utilities": {
"optional": true
},
"happy-dom": {
"optional": true
},
"typescript": {
"optional": true
},
Expand Down
3 changes: 2 additions & 1 deletion src/components/svelte-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ export default function registerWebComponent(
// we need to also clear the contents of the node, to ensure we don't
// duplicate content.
const shadow =
this.shadowRoot ?? this.attachShadow({ mode, delegatesFocus: true })
this.shadowRoot ??
this.attachShadow({ mode, serializable: true, delegatesFocus: true })
shadow.replaceChildren()

// Unfortunately we need a DOMMutationObserver to let us know when
Expand Down
32 changes: 32 additions & 0 deletions src/ssr/render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Window } from 'happy-dom'

const globals = new Window()

global.window = globals.window as any
global.document = globals.document as any
global.customElements = globals.customElements as any
global.HTMLElement = globals.HTMLElement as any
global.MutationObserver = globals.MutationObserver as any

/**
* Renders custom elements to the DOM, so they render before JavaScript is
* loaded. Elements which require JavaScript for interactivity will **NOT**
* work until the JavaScript loads.
* @param html The HTML to render
* @param imports The custom elements to load - these must be imported
* dynamically so HappyDOM can load before they are imported.
* @returns The rendered HTML
*/
export async function render(
html: string,
imports: (() => Promise<unknown>)[] = []
) {
await Promise.all(imports.map((init) => init()))

const document = globals.document
document.documentElement.innerHTML = html

await globals.happyDOM.waitUntilComplete()

return document.documentElement.getHTML({ serializableShadowRoots: true })
}
Loading