Skip to content

Commit

Permalink
examples basic ssr
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Nov 11, 2022
1 parent 9926957 commit bcbfb25
Show file tree
Hide file tree
Showing 15 changed files with 1,945 additions and 50 deletions.
5 changes: 5 additions & 0 deletions examples/react/basic-ssr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
6 changes: 6 additions & 0 deletions examples/react/basic-ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install` or `yarn`
- `yarn start` or `yarn start`
13 changes: 13 additions & 0 deletions examples/react/basic-ssr/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!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>
<!--app-head-->
</head>
<body>
<div id="root"><!--app-html--></div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions examples/react/basic-ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "tanstack-router-react-example-basic-ssr",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "node server",
"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",
"serve": "NODE_ENV=production node server",
"debug": "node --inspect-brk server"
},
"dependencies": {
"@tanstack/react-router": "0.0.1-beta.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/jsesc": "^3.0.1",
"@vitejs/plugin-react": "^2.2.0",
"compression": "^1.7.4",
"express": "^4.18.2",
"jsesc": "^3.0.2",
"serve-static": "^1.15.0",
"vite": "^3.2.3"
}
}
105 changes: 105 additions & 0 deletions examples/react/basic-ssr/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD

process.env.MY_CUSTOM_SECRET = 'API_KEY_qwertyuiop'

export async function createServer(
root = process.cwd(),
isProd = process.env.NODE_ENV === 'production',
hmrPort,
) {
const resolve = (p) => path.resolve(__dirname, p)

const indexProd = isProd
? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
: ''

const app = express()

/**
* @type {import('vite').ViteDevServer}
*/
let vite
if (!isProd) {
vite = await (
await import('vite')
).createServer({
root,
logLevel: isTest ? 'error' : 'info',
server: {
middlewareMode: true,
watch: {
// During tests we edit the files too fast and sometimes chokidar
// misses change events, so enforce polling for consistency
usePolling: true,
interval: 100,
},
hmr: {
port: hmrPort,
},
},
appType: 'custom',
})
// use vite's connect instance as middleware
app.use(vite.middlewares)
} else {
app.use((await import('compression')).default())
app.use(
(await import('serve-static')).default(resolve('dist/client'), {
index: false,
}),
)
}

app.use('*', async (req, res) => {
try {
const url = req.originalUrl

let template, render
if (!isProd) {
// always read fresh template in dev
template = fs.readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render
} else {
template = indexProd
// @ts-ignore
render = (await import('./dist/server/entry-server.tsx')).render
}

const context = {}
const [appHead, appHtml] = await render(url, context)

if (context.url) {
// Somewhere a `<Redirect>` was rendered
return res.redirect(301, context.url)
}

const html = template
.replace('<!--app-head-->', appHead)
.replace(`<!--app-html-->`, appHtml)

res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
!isProd && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})

return { app, vite }
}

if (!isTest) {
createServer().then(({ app }) =>
app.listen(3000, () => {
console.log('Client Server: http://localhost:3000')
}),
)
}
33 changes: 33 additions & 0 deletions examples/react/basic-ssr/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react'
import { Outlet } from '@tanstack/react-router'

import { router } from './router'

export function App() {
return (
// Build our routes and render our router
<>
<div>
<router.Link
to="/"
activeProps={{
className: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</router.Link>{' '}
<router.Link
to="/posts"
activeProps={{
className: 'font-bold',
}}
>
Posts
</router.Link>
</div>
<hr />
<Outlet /> {/* Start rendering router matches */}
</>
)
}
29 changes: 29 additions & 0 deletions examples/react/basic-ssr/src/entry-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from '@tanstack/react-router'

import { router } from './router'
import { App } from './App'

const state = (window as any).__TANSTACK_ROUTER_STATE__

console.log(state)

router.state = {
...router.state,
...state,
matches: router.state.matches.map((match) => {
const serverMatch = state.matches.find(
(serverMatch: any) => serverMatch.route === match.matchId,
)
Object.assign(match, serverMatch)
return match
}),
}

ReactDOM.hydrateRoot(
document.getElementById('root')!,
<RouterProvider router={router}>
<App />
</RouterProvider>,
)
56 changes: 56 additions & 0 deletions examples/react/basic-ssr/src/entry-server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react'
import ReactDOMServer from 'react-dom/server'
import { createMemoryHistory, RouterProvider } from '@tanstack/react-router'
import jsesc from 'jsesc'
import { App } from './App'
import { router } from './router'

export async function render(url: string) {
const memoryHistory = createMemoryHistory({
initialEntries: [url],
})

router.update({
history: memoryHistory,
})

router.mount()

await router.loadLocation()

const { status, location, matches, lastUpdated } = router.state

const payload = {
status,
location,
matches: matches.map((match) => {
const { status, routeLoaderData, loaderData, isInvalid, invalidAt } =
match

return {
status,
routeLoaderData,
loaderData,
isInvalid,
invalidAt,
}
}),
lastUpdated,
}

return [
`<script>window.__TANSTACK_ROUTER_STATE__ = JSON.parse(${jsesc(
JSON.stringify(payload),
{
isScriptContext: true,
wrap: true,
json: true,
},
)})</script>`,
ReactDOMServer.renderToString(
<RouterProvider router={router}>
<App />
</RouterProvider>,
),
]
}
Loading

0 comments on commit bcbfb25

Please sign in to comment.