Skip to content

Commit

Permalink
feat(streaming): Cleanup/Unify streaming dev and prod server (#9047)
Browse files Browse the repository at this point in the history
  • Loading branch information
dac09 authored Aug 21, 2023
1 parent 8aa7688 commit c5ba488
Show file tree
Hide file tree
Showing 13 changed files with 466 additions and 437 deletions.
19 changes: 12 additions & 7 deletions packages/internal/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,23 @@ export function warningForDuplicateRoutes() {
return message.trimEnd()
}

export interface RouteSpec {
export interface RWRouteManifestItem {
name: string
path: string
pathDefinition: string
matchRegexString: string | null
routeHooks: string | null
bundle: string | null
hasParams: boolean
redirect: { to: string; permanent: boolean } | null
renderMode: 'html' | 'stream'
// Probably want isNotFound here, so we can attach a separate 404 handler
}

export interface RouteSpec extends RWRouteManifestItem {
id: string
isNotFound: boolean
filePath: string | undefined
relativeFilePath: string | undefined
routeHooks: string | undefined | null
matchRegexString: string | null
redirect: { to: string; permanent: boolean } | null
renderMode: 'stream' | 'html'
}

export const getProjectRoutes = (): RouteSpec[] => {
Expand All @@ -92,7 +97,7 @@ export const getProjectRoutes = (): RouteSpec[] => {

return {
name: route.isNotFound ? 'NotFoundPage' : route.name,
path: route.isNotFound ? 'notfound' : route.path,
pathDefinition: route.isNotFound ? 'notfound' : route.path,
hasParams: route.hasParameters,
id: route.id,
isNotFound: route.isNotFound,
Expand Down
43 changes: 35 additions & 8 deletions packages/project-config/src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,16 +262,43 @@ export const getRouteHookForPage = (pagePath: string | undefined | null) => {

// We just use fg, so if they make typos in the routeHook file name,
// it's all good, we'll still find it
return fg
.sync('*.routeHooks.{js,ts,tsx,jsx}', {
absolute: true,
cwd: path.dirname(pagePath), // the page's folder
})
.at(0)
return (
fg
.sync('*.routeHooks.{js,ts,tsx,jsx}', {
absolute: true,
cwd: path.dirname(pagePath), // the page's folder
})
.at(0) || null
)
}

export const getAppRouteHook = () => {
return resolveFile(path.join(getPaths().web.src, 'App.routeHooks'))
/**
* Use this function to find the app route hook.
* If it is present, you get the path to the file - in prod, you get the built version in dist.
* In dev, you get the source version.
*
* @param forProd
* @returns string | null
*/
export const getAppRouteHook = (forProd = false) => {
const rwPaths = getPaths()

if (forProd) {
const distAppRouteHook = path.join(
rwPaths.web.distRouteHooks,
'App.routeHooks.js'
)

try {
// Stat sync throws if file doesn't exist
fs.statSync(distAppRouteHook).isFile()
return distAppRouteHook
} catch (e) {
return null
}
}

return resolveFile(path.join(rwPaths.web.src, 'App.routeHooks'))
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/buildFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,15 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => {
const routesList = getProjectRoutes()

const routeManifest = routesList.reduce<RWRouteManifest>((acc, route) => {
acc[route.path] = {
acc[route.pathDefinition] = {
name: route.name,
bundle: route.relativeFilePath
? clientBuildManifest[route.relativeFilePath]?.file
: null,
matchRegexString: route.matchRegexString,
// @NOTE this is the path definition, not the actual path
// E.g. /blog/post/{id:Int}
pathDefinition: route.path,
pathDefinition: route.pathDefinition,
hasParams: route.hasParams,
routeHooks: FIXME_constructRouteHookPath(route.routeHooks),
redirect: route.redirect
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/buildRscFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,15 @@ export const buildRscFeServer = async ({

// This is all a no-op for now
const routeManifest = routesList.reduce<RWRouteManifest>((acc, route) => {
acc[route.path] = {
acc[route.pathDefinition] = {
name: route.name,
bundle: route.relativeFilePath
? clientBuildManifest[route.relativeFilePath].file
: null,
matchRegexString: route.matchRegexString,
// NOTE this is the path definition, not the actual path
// E.g. /blog/post/{id:Int}
pathDefinition: route.path,
pathDefinition: route.pathDefinition,
hasParams: route.hasParams,
routeHooks: null,
redirect: route.redirect
Expand Down
135 changes: 45 additions & 90 deletions packages/vite/src/devFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import express from 'express'
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 { getConfig, getPaths } from '@redwoodjs/project-config'

import { createReactStreamingHandler } from './streaming/createReactStreamingHandler'
import { registerFwGlobals } from './streaming/registerGlobals'
import { reactRenderToStream } from './streaming/streamHelpers'
import { loadAndRunRouteHooks } from './streaming/triggerRouteHooks'
import { ensureProcessDirWeb, stripQueryStringAndHashFromPath } from './utils'
import { ensureProcessDirWeb } from './utils'

// TODO (STREAMING) Just so it doesn't error out. Not sure how to handle this.
globalThis.__REDWOOD__PRERENDER_PAGES = {}
Expand All @@ -24,14 +21,24 @@ async function createServer() {
const app = express()
const rwPaths = getPaths()

// ~~~ Dev time validations ~~~~
// TODO (STREAMING) When Streaming is released Vite will be the only bundler,
// and this file should always exist. So the error message needs to change
// (or be removed perhaps)
if (!rwPaths.web.entryServer || !rwPaths.web.entryClient) {
throw new Error(
'Vite entry points not found. Please check that your project has ' +
'an entry.client.{jsx,tsx} and entry.server.{jsx,tsx} file in ' +
'the web/src directory.'
)
}

if (!rwPaths.web.viteConfig) {
throw new Error(
'Vite config not found. You need to setup your project with Vite using `yarn rw setup vite`'
)
}
// ~~~~ Dev time validations ~~~~

// Create Vite server in middleware mode and configure the app type as
// 'custom', disabling Vite's own HTML serving logic so parent server
Expand All @@ -47,89 +54,35 @@ async function createServer() {
// use vite's connect instance as middleware
app.use(vite.middlewares)

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

try {
const routes = getProjectRoutes()

// Do a simple match with regex, don't bother parsing params yet
const currentRoute = routes.find((route) => {
if (!route.matchRegexString) {
// This is the 404/NotFoundPage case
return false
}

const matches = [
...currentPathName.matchAll(new RegExp(route.matchRegexString, 'g')),
]

return matches.length > 0
})

let metaTags: TagDescriptor[] = []

if (currentRoute?.redirect) {
return res.redirect(currentRoute.redirect.to)
}

if (currentRoute) {
const parsedParams = currentRoute.hasParams
? matchPath(currentRoute.path, currentPathName).params
: undefined

const routeHookOutput = await loadAndRunRouteHooks({
paths: [getAppRouteHook(), currentRoute.routeHooks],
reqMeta: {
req,
parsedParams,
},
viteDevServer: vite, // because its dev
})

metaTags = routeHookOutput.meta
}

if (!currentRoute) {
// TODO (STREAMING) do something
}

if (!rwPaths.web.entryServer || !rwPaths.web.entryClient) {
throw new Error(
'Vite entry points not found. Please check that your project has ' +
'an entry.client.{jsx,tsx} and entry.server.{jsx,tsx} file in ' +
'the web/src directory.'
)
}

// 3. Load the server entry. vite.ssrLoadModule automatically transforms
// your ESM source code to be usable in Node.js! There is no bundling
// required, and provides efficient invalidation similar to HMR.
const { ServerEntry } = await vite.ssrLoadModule(rwPaths.web.entryServer)

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

res.setHeader('content-type', 'text/html; charset=utf-8')

reactRenderToStream({
ServerEntry,
currentPathName,
metaTags,
includeJs: pageWithJs,
res,
})
} catch (e) {
// TODO (STREAMING) Is this what we want to do?
// send back a SPA page
// res.status(200).set({ 'Content-Type': 'text/html' }).end(template)

// If an error is caught, let Vite fix the stack trace so it maps back to
// your actual source code.
vite.ssrFixStacktrace(e as any)
next(e)
const routes = getProjectRoutes()

// 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
// Look at collectStyles in packages/vite/src/fully-react/find-styles.ts
const FIXME_HardcodedIndexCss = ['index.css']

for (const route of routes) {
const routeHandler = await createReactStreamingHandler(
{
route,
clientEntryPath: rwPaths.web.entryClient as string,
cssLinks: FIXME_HardcodedIndexCss,
},
vite
)

// @TODO if it is a 404, hand over to 404 handler
if (!route.matchRegexString) {
continue
}
})

const expressPathDef = route.hasParams
? route.matchRegexString
: route.pathDefinition

app.get(expressPathDef, routeHandler)
}

const port = getConfig().web.port
console.log(`Started server on http://localhost:${port}`)
Expand All @@ -141,7 +94,9 @@ let devApp = createServer()
process.stdin.on('data', async (data) => {
const str = data.toString().trim().toLowerCase()
if (str === 'rs' || str === 'restart') {
;(await devApp).close()
devApp = createServer()
console.log('Restarting dev web server.....')
;(await devApp).close(() => {
devApp = createServer()
})
}
})
Loading

0 comments on commit c5ba488

Please sign in to comment.