Skip to content

Commit

Permalink
Add files needed for React Streaming SSR (redwoodjs#8810)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe committed Jul 2, 2023
1 parent fd14fd5 commit 6da5aae
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 34 deletions.
194 changes: 160 additions & 34 deletions packages/cli/src/commands/experimental/setupStreamingSsrHandler.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import fs from 'fs'
import path from 'path'

import { Listr } from 'listr2'

import { getConfigPath } from '@redwoodjs/project-config'
import { errorTelemetry } from '@redwoodjs/telemetry'

import { writeFile } from '../../lib'
import { getPaths, transformTSToJS, writeFile } from '../../lib'
import c from '../../lib/colors'
import { isTypeScriptProject } from '../../lib/project'

import {
command,
Expand All @@ -11,44 +17,164 @@ import {
} from './setupStreamingSsr'
import { printTaskEpilogue } from './util'

export const handler = async ({ force }) => {
export const handler = async ({ force, verbose }) => {
const rwPaths = getPaths()
const redwoodTomlPath = getConfigPath()
const configContent = fs.readFileSync(redwoodTomlPath, 'utf-8')
const ts = isTypeScriptProject()
const ext = path.extname(rwPaths.web.entryClient || '')

if (!configContent.includes('[experimental.streamingSsr]')) {
console.log('Adding config to redwood.toml...')
const tasks = new Listr(
[
{
title: 'Check prerequisites',
task: () => {
if (!rwPaths.web.entryClient || !rwPaths.web.viteConfig) {
throw new Error(
'Vite needs to be setup before you can enable Streaming SSR'
)
}
},
},
{
title: 'Adding config to redwood.toml...',
task: (_ctx, task) => {
if (!configContent.includes('[experimental.streamingSsr]')) {
writeFile(
redwoodTomlPath,
configContent.concat(
`\n[experimental.streamingSsr]\n enabled = true\n`
),
{
overwriteExisting: true, // redwood.toml always exists
}
)
} else {
if (force) {
task.output = 'Overwriting config in redwood.toml'

// Use string replace to preserve comments and formatting
writeFile(
redwoodTomlPath,
configContent.concat(`\n[experimental.streamingSsr]\n enabled = true\n`),
writeFile(
redwoodTomlPath,
configContent.replace(
// Enable if it's currently disabled
`\n[experimental.streamingSsr]\n enabled = false\n`,
`\n[experimental.streamingSsr]\n enabled = true\n`
),
{
overwriteExisting: true, // redwood.toml always exists
}
)
} else {
task.skip(
`The [experimental.streamingSsr] config block already exists in your 'redwood.toml' file.`
)
}
}
},
options: { persistentOutput: true },
},
{
overwriteExisting: true, // redwood.toml always exists
}
)
} else {
if (force) {
console.log('Updating config in redwood.toml...')
writeFile(
redwoodTomlPath,
configContent.replace(
// Enable if it's currently disabled
`\n[experimental.streamingSsr]\n enabled = false\n`,
`\n[experimental.streamingSsr]\n enabled = true\n`
),
{
overwriteExisting: true, // redwood.toml always exists
}
)
} else {
console.log('Adding config to redwood.toml...')
console.log(
" The [experimental.studio] config block already exists in your 'redwood.toml' file."
)
}
}
title: `Adding entry.client${ext}...`,
task: async (_ctx, task) => {
const entryClientTemplate = fs.readFileSync(
path.resolve(
__dirname,
'templates',
'streamingSsr',
'entry.client.tsx.template'
),
'utf-8'
)
let entryClientPath = rwPaths.web.entryClient
const entryClientContent = ts
? entryClientTemplate
: transformTSToJS(entryClientPath, entryClientTemplate)

let overwriteExisting = force

console.log()
if (!force) {
overwriteExisting = await task.prompt({
type: 'Confirm',
message: `Overwrite ${entryClientPath}?`,
})

printTaskEpilogue(command, description, EXPERIMENTAL_TOPIC_ID)
if (!overwriteExisting) {
entryClientPath = entryClientPath.replace(ext, `.new${ext}`)
task.output =
`File will be written to ${entryClientPath}\n` +
`You'll manually need to merge it with your existing entry.client${ext} file.`
}
}

writeFile(entryClientPath, entryClientContent, { overwriteExisting })
},
options: { persistentOutput: true },
},
{
title: `Adding entry.server${ext}...`,
task: async () => {
const entryServerTemplate = fs.readFileSync(
path.resolve(
__dirname,
'templates',
'streamingSsr',
'entry.server.tsx.template'
),
'utf-8'
)
// Can't use rwPaths.web.entryServer because it might not be not created yet
const entryServerPath = path.join(
rwPaths.web.src,
`entry.server${ext}`
)
const entryServerContent = ts
? entryServerTemplate
: transformTSToJS(entryServerPath, entryServerTemplate)

writeFile(entryServerPath, entryServerContent, {
overwriteExisting: force,
})
},
},
{
title: `Adding Document${ext}...`,
task: async () => {
const documentTemplate = fs.readFileSync(
path.resolve(
__dirname,
'templates',
'streamingSsr',
'Document.tsx.template'
),
'utf-8'
)
const documentPath = path.join(rwPaths.web.src, `Document${ext}`)
const documentContent = ts
? documentTemplate
: transformTSToJS(documentPath, documentTemplate)

writeFile(documentPath, documentContent, {
overwriteExisting: force,
})
},
},
{
task: () => {
printTaskEpilogue(command, description, EXPERIMENTAL_TOPIC_ID)
},
},
],
{
rendererOptions: { collapseSubtasks: false, persistentOutput: true },
renderer: verbose ? 'verbose' : 'default',
}
)

try {
await tasks.run()
} catch (e) {
errorTelemetry(process.argv, e.message)
console.error(c.error(e.message))
process.exit(e?.exitCode || 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'

import { Css, Meta } from '@redwoodjs/web'
import type { TagDescriptor } from '@redwoodjs/web'

interface DocumentProps {
children: React.ReactNode
css: string[] // array of css import strings
meta?: TagDescriptor[]
}

export const Document: React.FC<DocumentProps> = ({ children, css, meta }) => {
return (
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon.png" />
<Css css={css} />
<Meta tags={meta} />
</head>
<body>
<div id="redwood-app">{children}</div>
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { hydrateRoot, createRoot } from 'react-dom/client'

// TODO (STREAMING) This was marked "temporary workaround"
// Need to figure out why it's a temporary workaround and what we
// should do instead.
import { ServerContextProvider } from '@redwoodjs/web/dist/serverContext'

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

/**
* When `#redwood-app` isn't empty then it's very likely that you're using
* prerendering. So React attaches event listeners to the existing markup
* rather than replacing it.
* https://reactjs.org/docs/react-dom-client.html#hydrateroot
*/
const redwoodAppElement = document.getElementById('redwood-app')

if (redwoodAppElement.children?.length > 0) {
hydrateRoot(
document,
<ServerContextProvider value={{}}>
<Document css={window.__assetMap?.()?.css}>
<App />
</Document>
</ServerContextProvider>
)
} else {
console.log('Rendering from scratch')
const root = createRoot(document)
root.render(
<ServerContextProvider value={{}}>
<Document css={window.__assetMap?.()?.css}>
<App />
</Document>
</ServerContextProvider>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { LocationProvider } from '@redwoodjs/router'
import { ServerContextProvider } from '@redwoodjs/web/dist/serverContext'

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

interface Props {
routeContext: any
url: string
css: string[]
meta?: any[]
}

export const ServerEntry: React.FC<Props> = ({
routeContext,
url,
css,
meta,
}) => {
return (
<ServerContextProvider value={routeContext}>
<LocationProvider location={{ pathname: url }}>
<Document css={css} meta={meta}>
<App />
</Document>
</LocationProvider>
</ServerContextProvider>
)
}

0 comments on commit 6da5aae

Please sign in to comment.