Skip to content

Commit

Permalink
feat(streaming-ssr): Fix build and server html injection (#8978)
Browse files Browse the repository at this point in the history
- Fixes cases where route hooks not firing correctly when root Route
hook missing
- Fixes build erroring out when pages are explicitly imported
- Ensures webdir for build too
- Adds new way to inject html to `<head>` for Apollo client
- Makes the `<MetaTags>` component partially work with the new streaming
setup. Helmet doesn't work with streaming (their docs are for the old
way of streaming).
- Right now it doesn't support "templates" like Helmet does (neither do
route hooks)

Note: I haven't applied HTML injection to prod just yet.

---------

Co-authored-by: Dominic Saadi, Kris Coulson
  • Loading branch information
dac09 committed Aug 9, 2023
1 parent 8dc8e80 commit 58a2421
Show file tree
Hide file tree
Showing 19 changed files with 447 additions and 105 deletions.
19 changes: 16 additions & 3 deletions packages/vite/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
/* eslint-disable no-var */
/// <reference types="react/canary" />
import type { HelmetServerState } from 'react-helmet-async'

declare global {
// Provided by Vite.config, or Webpack in the user's project
// but "regsitered" in packages/vite/src/streaming/registerGlobals.ts
// for it to be available to framework code
var RWJS_ENV: {
RWJS_API_GRAPHQL_URL?: string
RWJS_API_GRAPHQL_URL: string
/** URL or absolute path to serverless functions */
RWJS_API_URL?: string
RWJS_API_URL: string
RWJS_EXP_STREAMING_SSR: boolean
RWJS_EXP_RSC: boolean

__REDWOOD__APP_TITLE?: string
__REDWOOD__APP_TITLE: string
}

var RWJS_DEBUG_ENV: {
RWJS_SRC_ROOT: string
REDWOOD_ENV_EDITOR: string
}

var __REDWOOD__PRERENDER_PAGES: any

var __REDWOOD__HELMET_CONTEXT: { helmet?: HelmetServerState }
}

export {}
7 changes: 4 additions & 3 deletions packages/vite/bins/rw-vite-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ const buildWebSide = async (webDir) => {
throw new Error('Could not locate your web/vite.config.{js,ts} file')
}

// @NOTE: necessary for keeping the cwd correct for postcss/tailwind
process.chdir(webDir)
process.env.NODE_ENV = 'production'

if (getConfig().experimental?.streamingSsr?.enabled) {
await buildFeServer({ verbose })
// Webdir checks handled in the rwjs/vite package in new build system
await buildFeServer({ verbose, webDir })
} else {
// Ensure cwd to be web: required for postcss/tailwind to work correctly
process.chdir(webDir)
// Right now, the buildWeb function looks up the config file from project-config
// In the future, if we have multiple web spaces we could pass in the cwd here
buildWeb({ verbose })
Expand Down
8 changes: 6 additions & 2 deletions packages/vite/src/buildFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ import { getAppRouteHook, getConfig, getPaths } from '@redwoodjs/project-config'

import { buildRscFeServer } from './buildRscFeServer'
import { RWRouteManifest } from './types'
import { ensureProcessDirWeb } from './utils'

export interface BuildOptions {
verbose?: boolean
webDir?: string
}

export const buildFeServer = async ({ verbose }: BuildOptions) => {
export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => {
ensureProcessDirWeb(webDir)

const rwPaths = getPaths()
const rwConfig = getConfig()
const viteConfigPath = rwPaths.web.viteConfig
Expand Down Expand Up @@ -143,7 +147,7 @@ export const buildFeServer = async ({ verbose }: BuildOptions) => {
acc[route.path] = {
name: route.name,
bundle: route.relativeFilePath
? clientBuildManifest[route.relativeFilePath].file
? clientBuildManifest[route.relativeFilePath]?.file
: null,
matchRegexString: route.matchRegexString,
// @NOTE this is the path definition, not the actual path
Expand Down
71 changes: 15 additions & 56 deletions packages/vite/src/devFeServer.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
// TODO (STREAMING) Merge with runFeServer so we only have one file
import path from 'path'

import express from 'express'
import { renderToPipeableStream } from 'react-dom/server'
import { createServer as createViteServer } from 'vite'

import { getProjectRoutes } from '@redwoodjs/internal/dist/routes'
import { getAppRouteHook, getConfig, getPaths } from '@redwoodjs/project-config'
import { matchPath } from '@redwoodjs/router'
import type { TagDescriptor } from '@redwoodjs/web'

import { loadAndRunRouteHooks } from './triggerRouteHooks'
import { stripQueryStringAndHashFromPath } from './utils'

// These values are defined in the vite.config.ts
globalThis.RWJS_ENV = {}
import { registerFwGlobals } from './streaming/registerGlobals'
import { reactRenderToStream } from './streaming/streamHelpers'
import { loadAndRunRouteHooks } from './streaming/triggerRouteHooks'
import { ensureProcessDirWeb, stripQueryStringAndHashFromPath } from './utils'

// TODO (STREAMING) Just so it doesn't error out. Not sure how to handle this.
globalThis.__REDWOOD__PRERENDER_PAGES = {}

async function createServer() {
// Check CWD: make sure its the web/ directory
// Without this Postcss can misbehave, and its hard to trace why.
if (process.cwd() !== getPaths().web.base) {
console.error('⚠️ Warning: CWD is ', process.cwd())
console.warn('~'.repeat(50))
console.warn(
'The FE dev server cwd must be web/. Please use `yarn rw dev` or start the server from the web/ directory.'
)
console.log(`Changing cwd to ${getPaths().web.base}....`)
console.log()
ensureProcessDirWeb()

process.chdir(getPaths().web.base)
}
registerFwGlobals()

const app = express()
const rwPaths = getPaths()
Expand Down Expand Up @@ -62,6 +49,7 @@ async function createServer() {

app.use('*', async (req, res, next) => {
const currentPathName = stripQueryStringAndHashFromPath(req.originalUrl)
globalThis.__REDWOOD__HELMET_CONTEXT = {}

try {
const routes = getProjectRoutes()
Expand Down Expand Up @@ -120,46 +108,17 @@ async function createServer() {
// required, and provides efficient invalidation similar to HMR.
const { ServerEntry } = await vite.ssrLoadModule(rwPaths.web.entryServer)

// TODO (STREAMING) CSS is handled by Vite in dev mode, we don't need to
// worry about it in dev but..... it causes a flash of unstyled content.
// For now I'm just injecting index css here
// We believe we saw a fix for this somewhere in the Waku sources. Maybe
// it was called something like "Capture Css". And it's also mentioned
// in the Vite issues on GitHub
const FIXME_HardcodedIndexCss = ['index.css']

const assetMap = JSON.stringify({
css: FIXME_HardcodedIndexCss,
meta: metaTags,
})

const bootstrapModules = [
path.join(__dirname, '../inject', 'reactRefresh.js'),
]

const pageWithJs = currentRoute?.renderMode !== 'html'

if (pageWithJs) {
bootstrapModules.push(rwPaths.web.entryClient)
}
res.setHeader('content-type', 'text/html; charset=utf-8')

const { pipe } = renderToPipeableStream(
ServerEntry({
url: currentPathName,
css: FIXME_HardcodedIndexCss,
meta: metaTags,
}),
{
bootstrapScriptContent: pageWithJs
? `window.__assetMap = function() { return ${assetMap} }`
: undefined,
bootstrapModules,
onShellReady() {
res.setHeader('content-type', 'text/html; charset=utf-8')
pipe(res)
},
}
)
reactRenderToStream({
ServerEntry,
currentPathName,
metaTags,
includeJs: pageWithJs,
res,
})
} catch (e) {
// TODO (STREAMING) Is this what we want to do?
// send back a SPA page
Expand Down
14 changes: 8 additions & 6 deletions packages/vite/src/runFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ import { getConfig, getPaths } from '@redwoodjs/project-config'
import { matchPath } from '@redwoodjs/router'
import type { TagDescriptor } from '@redwoodjs/web'

import { loadAndRunRouteHooks } from './triggerRouteHooks'
import { registerFwGlobals } from './streaming/registerGlobals'
import { loadAndRunRouteHooks } from './streaming/triggerRouteHooks'
import { RWRouteManifest } from './types'
import { stripQueryStringAndHashFromPath } from './utils'

globalThis.RWJS_ENV = {}

/**
* TODO (STREAMING)
* We have this server in the vite package only temporarily.
Expand All @@ -50,6 +49,8 @@ export async function runFeServer() {
const rwPaths = getPaths()
const rwConfig = getConfig()

registerFwGlobals()

// TODO When https://github.com/tc39/proposal-import-attributes and
// https://github.com/microsoft/TypeScript/issues/53656 have both landed we
// should try to do this instead:
Expand Down Expand Up @@ -211,15 +212,16 @@ export async function runFeServer() {

const pageWithJs = currentRoute.renderMode !== 'html'
// @NOTE have to add slash so subpaths still pick up the right file
// Vite is currently producing modules not scripts: https://vitejs.dev/config/build-options.html#build-target
const bootstrapModules = pageWithJs
? ['/' + indexEntry.file, '/' + currentRoute.bundle]
? ([
'/' + indexEntry.file,
currentRoute.bundle && '/' + currentRoute.bundle,
].filter(Boolean) as string[])
: undefined

const isSeoCrawler = checkUaForSeoCrawler(req.get('user-agent'))

const { pipe, abort } = renderToPipeableStream(
// we should use the same shape as Remix or Next for the meta object
ServerEntry({
url: currentPathName,
css: indexEntry.css,
Expand Down
5 changes: 3 additions & 2 deletions packages/vite/src/runRscFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ import type { Manifest as ViteBuildManifest } from 'vite'

import { getConfig, getPaths } from '@redwoodjs/project-config'

import { registerFwGlobals } from './streaming/registerGlobals'
import { renderRSC, setClientEntries } from './waku-lib/rsc-handler-worker'

globalThis.RWJS_ENV = {}

/**
* TODO (STREAMING)
* We have this server in the vite package only temporarily.
Expand All @@ -45,6 +44,8 @@ export async function runFeServer() {
const rwPaths = getPaths()
const rwConfig = getConfig()

registerFwGlobals()

await setClientEntries('load')

// TODO When https://github.com/tc39/proposal-import-attributes and
Expand Down
34 changes: 34 additions & 0 deletions packages/vite/src/streaming/registerGlobals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import path from 'node:path'

import { getConfig, getPaths } from '@redwoodjs/project-config'

/**
* Use this function on the web server
*
* Because although this is defined in Vite/index.ts
* They are only available in the user's code (and not in FW code)
* because define STATICALLY replaces it in user's code, not in node_modules
*
* It's still available on the client side though, probably because its processed by Vite
*/
export const registerFwGlobals = () => {
const rwConfig = getConfig()
const rwPaths = getPaths()

globalThis.RWJS_ENV = {
// @NOTE we're avoiding process.env here, unlike webpack
RWJS_API_GRAPHQL_URL:
rwConfig.web.apiGraphQLUrl ?? rwConfig.web.apiUrl + '/graphql',
RWJS_API_URL: rwConfig.web.apiUrl,
__REDWOOD__APP_TITLE: rwConfig.web.title || path.basename(rwPaths.base),
RWJS_EXP_STREAMING_SSR:
rwConfig.experimental.streamingSsr &&
rwConfig.experimental.streamingSsr.enabled,
RWJS_EXP_RSC: rwConfig.experimental?.rsc?.enabled,
}

globalThis.RWJS_DEBUG_ENV = {
RWJS_SRC_ROOT: rwPaths.web.src,
REDWOOD_ENV_EDITOR: JSON.stringify(process.env.REDWOOD_ENV_EDITOR),
}
}
Loading

0 comments on commit 58a2421

Please sign in to comment.