Skip to content

Commit

Permalink
feat: bring back server utils via tanstack/react-router-server
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Dec 5, 2023
1 parent 6fb3e2d commit 15f7ea4
Show file tree
Hide file tree
Showing 61 changed files with 4,074 additions and 452 deletions.
6 changes: 0 additions & 6 deletions examples/react/basic-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,5 @@
"devDependencies": {
"@types/react": "^18.2.41",
"@types/react-dom": "^18.2.17"
},
"pnpm": {
"overrides": {
"@tanstack/react-router": "workspace:*",
"@tanstack/router-devtools": "workspace:*"
}
}
}
6 changes: 0 additions & 6 deletions examples/react/basic-react-query-file-based/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,5 @@
"devDependencies": {
"@types/react": "^18.2.41",
"@types/react-dom": "^18.2.17"
},
"pnpm": {
"overrides": {
"@tanstack/react-router": "workspace:*",
"@tanstack/router-devtools": "workspace:*"
}
}
}
6 changes: 0 additions & 6 deletions examples/react/basic-react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,5 @@
"devDependencies": {
"@types/react": "^18.2.41",
"@types/react-dom": "^18.2.17"
},
"pnpm": {
"overrides": {
"@tanstack/react-router": "workspace:*",
"@tanstack/router-devtools": "workspace:*"
}
}
}
5 changes: 5 additions & 0 deletions examples/react/basic-ssr-file-based/.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-file-based/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`
- `npm start` or `yarn start`
42 changes: 42 additions & 0 deletions examples/react/basic-ssr-file-based/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "tanstack-router-react-example-basic-ssr-file-based",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "concurrently \"tsr watch\" \"node server\"",
"build": "npm run build:tsr && npm run build:client && npm run build:server",
"build:tsr": "tsr generate",
"build:client": "vite build src/entry-client.tsx --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
"serve": "NODE_ENV=production node server",
"debug": "node --inspect-brk server"
},
"dependencies": {
"@tanstack/react-router": "0.0.1-beta.204",
"@tanstack/react-start": "0.0.1-beta.204",
"@tanstack/router-cli": "0.0.1-beta.204",
"@tanstack/router-devtools": "0.0.1-beta.204",
"axios": "^1.1.3",
"get-port": "^7.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@babel/generator": "^7.20.4",
"@rollup/plugin-babel": "^6.0.2",
"@types/express": "^4.17.14",
"@types/jsesc": "^3.0.1",
"@vitejs/plugin-react": "^4",
"compression": "^1.7.4",
"concurrently": "^7.6.0",
"express": "^4.18.2",
"isbot": "^3.6.5",
"jsesc": "^3.0.2",
"node-fetch": "^3.3.0",
"serve-static": "^1.15.0",
"vite": "^4",
"vite-plugin-babel": "^1.1.2"
}
}
93 changes: 93 additions & 0 deletions examples/react/basic-ssr-file-based/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import express from 'express'
import getPort, { portNumbers } from 'get-port'

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

export async function createServer(
root = process.cwd(),
isProd = process.env.NODE_ENV === 'production',
hmrPort,
) {
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('*', async (req, res) => {
try {
const url = req.originalUrl

if (url.includes('.')) {
console.warn(`${url} is not valid router path`)
res.status(404)
res.end(`${url} is not valid router path`)
return
}

// Extract the head from vite's index transformation hook
let viteHead = !isProd
? await vite.transformIndexHtml(
url,
`<html><head></head><body></body></html>`,
)
: ''

viteHead = viteHead.substring(
viteHead.indexOf('<head>') + 6,
viteHead.indexOf('</head>'),
)

const entry = await (async () => {
if (!isProd) {
return vite.ssrLoadModule('/src/entry-server.tsx')
} else {
return import('./dist/server/entry-server.tsx')
}
})()

console.log('Rendering: ', url, '...')
entry.render({ req, res, url, head: viteHead })
} catch (e) {
!isProd && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})

return { app, vite }
}

if (!isTest) {
createServer().then(async ({ app }) =>
app.listen(await getPort({ port: portNumbers(3000, 3100) }), () => {
console.log('Client Server: http://localhost:3000')
}),
)
}
10 changes: 10 additions & 0 deletions examples/react/basic-ssr-file-based/src/entry-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from 'react'
import ReactDOM from 'react-dom/client'

import { StartClient } from '@tanstack/react-start/client'
import { createRouter } from './router'

const router = createRouter()
router.hydrate()

ReactDOM.hydrateRoot(document, <StartClient router={router} />)
42 changes: 42 additions & 0 deletions examples/react/basic-ssr-file-based/src/entry-server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as React from 'react'
import ReactDOMServer from 'react-dom/server'
import { createMemoryHistory } from '@tanstack/react-router'
import { ServerResponse } from 'http'
import express from 'express'
import { StartServer } from '@tanstack/react-start/server'
import { createRouter } from './router'

// index.js
import './fetch-polyfill'

export async function render(opts: {
url: string
head: string
req: express.Request
res: ServerResponse
}) {
const router = createRouter()

const memoryHistory = createMemoryHistory({
initialEntries: [opts.url],
})

// Update the history and context
router.update({
history: memoryHistory,
context: {
...router.options.context,
head: opts.head,
},
})

// Since we're using renderToString, Wait for the router to finish loading
await router.load()

// Render the app
const appHtml = ReactDOMServer.renderToString(<StartServer router={router} />)

opts.res.statusCode = 200
opts.res.setHeader('Content-Type', 'text/html')
opts.res.end(`<!DOCTYPE html>${appHtml}`)
}
20 changes: 20 additions & 0 deletions examples/react/basic-ssr-file-based/src/fetch-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// fetch-polyfill.js
import fetch, {
Blob,
blobFrom,
blobFromSync,
File,
fileFrom,
fileFromSync,
FormData,
Headers,
Request,
Response,
} from 'node-fetch'

if (!globalThis.fetch) {
globalThis.fetch = fetch
globalThis.Headers = Headers
globalThis.Request = Request
globalThis.Response = Response
}
47 changes: 47 additions & 0 deletions examples/react/basic-ssr-file-based/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Route as rootRoute } from "./routes/__root"
import { Route as PostsRoute } from "./routes/posts"
import { Route as IndexRoute } from "./routes/index"
import { Route as PostsPostIdRoute } from "./routes/posts/$postId"
import { Route as PostsIndexRoute } from "./routes/posts/index"

declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/": {
parentRoute: typeof rootRoute
}
"/posts": {
parentRoute: typeof rootRoute
}
"/posts/": {
parentRoute: typeof PostsRoute
}
"/posts/$postId": {
parentRoute: typeof PostsRoute
}
}
}

Object.assign(IndexRoute.options, {
path: "/",
getParentRoute: () => rootRoute,
})

Object.assign(PostsRoute.options, {
path: "/posts",
getParentRoute: () => rootRoute,
})

Object.assign(PostsIndexRoute.options, {
path: "/",
getParentRoute: () => PostsRoute,
})

Object.assign(PostsPostIdRoute.options, {
path: "/$postId",
getParentRoute: () => PostsRoute,
})

export const routeTree = rootRoute.addChildren([
IndexRoute,
PostsRoute.addChildren([PostsIndexRoute, PostsPostIdRoute]),
])
18 changes: 18 additions & 0 deletions examples/react/basic-ssr-file-based/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from '@tanstack/react-router'

import { routeTree } from './routeTree.gen'

export function createRouter() {
return new Router({
routeTree,
context: {
head: '',
},
})
}

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}
3 changes: 3 additions & 0 deletions examples/react/basic-ssr-file-based/src/routerContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type RouterContext = {
head: string
}
62 changes: 62 additions & 0 deletions examples/react/basic-ssr-file-based/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import * as React from 'react'
import { Link, Outlet, rootRouteWithContext } from '@tanstack/react-router'
import { DehydrateRouter } from '@tanstack/react-start/client'
import { RouterContext } from '../routerContext'

export const Route = rootRouteWithContext<RouterContext>()({
component: RootComponent,
})

function RootComponent() {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<script src="https://cdn.tailwindcss.com" />
<script
type="module"
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
`,
}}
/>
<script type="module" src="/@vite/client" />
<script type="module" src="/src/entry-client.tsx" />
</head>
<body>
<div className="p-2 flex gap-2 text-lg">
<Link
to="/"
activeProps={{
className: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</Link>{' '}
<Link
to="/posts"
activeProps={{
className: 'font-bold',
}}
>
Posts
</Link>
</div>
<hr />
<Outlet /> {/* Start rendering router matches */}
<TanStackRouterDevtools position="bottom-right" />
<DehydrateRouter />
</body>
</html>
)
}
Loading

0 comments on commit 15f7ea4

Please sign in to comment.