Skip to content

Commit c5ba488

Browse files
authored
feat(streaming): Cleanup/Unify streaming dev and prod server (#9047)
1 parent 8aa7688 commit c5ba488

File tree

13 files changed

+466
-437
lines changed

13 files changed

+466
-437
lines changed

packages/internal/src/routes.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,23 @@ export function warningForDuplicateRoutes() {
6767
return message.trimEnd()
6868
}
6969

70-
export interface RouteSpec {
70+
export interface RWRouteManifestItem {
7171
name: string
72-
path: string
72+
pathDefinition: string
73+
matchRegexString: string | null
74+
routeHooks: string | null
75+
bundle: string | null
7376
hasParams: boolean
77+
redirect: { to: string; permanent: boolean } | null
78+
renderMode: 'html' | 'stream'
79+
// Probably want isNotFound here, so we can attach a separate 404 handler
80+
}
81+
82+
export interface RouteSpec extends RWRouteManifestItem {
7483
id: string
7584
isNotFound: boolean
7685
filePath: string | undefined
7786
relativeFilePath: string | undefined
78-
routeHooks: string | undefined | null
79-
matchRegexString: string | null
80-
redirect: { to: string; permanent: boolean } | null
81-
renderMode: 'stream' | 'html'
8287
}
8388

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

9398
return {
9499
name: route.isNotFound ? 'NotFoundPage' : route.name,
95-
path: route.isNotFound ? 'notfound' : route.path,
100+
pathDefinition: route.isNotFound ? 'notfound' : route.path,
96101
hasParams: route.hasParameters,
97102
id: route.id,
98103
isNotFound: route.isNotFound,

packages/project-config/src/paths.ts

+35-8
Original file line numberDiff line numberDiff line change
@@ -262,16 +262,43 @@ export const getRouteHookForPage = (pagePath: string | undefined | null) => {
262262

263263
// We just use fg, so if they make typos in the routeHook file name,
264264
// it's all good, we'll still find it
265-
return fg
266-
.sync('*.routeHooks.{js,ts,tsx,jsx}', {
267-
absolute: true,
268-
cwd: path.dirname(pagePath), // the page's folder
269-
})
270-
.at(0)
265+
return (
266+
fg
267+
.sync('*.routeHooks.{js,ts,tsx,jsx}', {
268+
absolute: true,
269+
cwd: path.dirname(pagePath), // the page's folder
270+
})
271+
.at(0) || null
272+
)
271273
}
272274

273-
export const getAppRouteHook = () => {
274-
return resolveFile(path.join(getPaths().web.src, 'App.routeHooks'))
275+
/**
276+
* Use this function to find the app route hook.
277+
* If it is present, you get the path to the file - in prod, you get the built version in dist.
278+
* In dev, you get the source version.
279+
*
280+
* @param forProd
281+
* @returns string | null
282+
*/
283+
export const getAppRouteHook = (forProd = false) => {
284+
const rwPaths = getPaths()
285+
286+
if (forProd) {
287+
const distAppRouteHook = path.join(
288+
rwPaths.web.distRouteHooks,
289+
'App.routeHooks.js'
290+
)
291+
292+
try {
293+
// Stat sync throws if file doesn't exist
294+
fs.statSync(distAppRouteHook).isFile()
295+
return distAppRouteHook
296+
} catch (e) {
297+
return null
298+
}
299+
}
300+
301+
return resolveFile(path.join(rwPaths.web.src, 'App.routeHooks'))
275302
}
276303

277304
/**

packages/vite/src/buildFeServer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,15 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => {
144144
const routesList = getProjectRoutes()
145145

146146
const routeManifest = routesList.reduce<RWRouteManifest>((acc, route) => {
147-
acc[route.path] = {
147+
acc[route.pathDefinition] = {
148148
name: route.name,
149149
bundle: route.relativeFilePath
150150
? clientBuildManifest[route.relativeFilePath]?.file
151151
: null,
152152
matchRegexString: route.matchRegexString,
153153
// @NOTE this is the path definition, not the actual path
154154
// E.g. /blog/post/{id:Int}
155-
pathDefinition: route.path,
155+
pathDefinition: route.pathDefinition,
156156
hasParams: route.hasParams,
157157
routeHooks: FIXME_constructRouteHookPath(route.routeHooks),
158158
redirect: route.redirect

packages/vite/src/buildRscFeServer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,15 @@ export const buildRscFeServer = async ({
218218

219219
// This is all a no-op for now
220220
const routeManifest = routesList.reduce<RWRouteManifest>((acc, route) => {
221-
acc[route.path] = {
221+
acc[route.pathDefinition] = {
222222
name: route.name,
223223
bundle: route.relativeFilePath
224224
? clientBuildManifest[route.relativeFilePath].file
225225
: null,
226226
matchRegexString: route.matchRegexString,
227227
// NOTE this is the path definition, not the actual path
228228
// E.g. /blog/post/{id:Int}
229-
pathDefinition: route.path,
229+
pathDefinition: route.pathDefinition,
230230
hasParams: route.hasParams,
231231
routeHooks: null,
232232
redirect: route.redirect

packages/vite/src/devFeServer.ts

+45-90
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@ import express from 'express'
44
import { createServer as createViteServer } from 'vite'
55

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

9+
import { createReactStreamingHandler } from './streaming/createReactStreamingHandler'
1110
import { registerFwGlobals } from './streaming/registerGlobals'
12-
import { reactRenderToStream } from './streaming/streamHelpers'
13-
import { loadAndRunRouteHooks } from './streaming/triggerRouteHooks'
14-
import { ensureProcessDirWeb, stripQueryStringAndHashFromPath } from './utils'
11+
import { ensureProcessDirWeb } from './utils'
1512

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

24+
// ~~~ Dev time validations ~~~~
2725
// TODO (STREAMING) When Streaming is released Vite will be the only bundler,
2826
// and this file should always exist. So the error message needs to change
2927
// (or be removed perhaps)
28+
if (!rwPaths.web.entryServer || !rwPaths.web.entryClient) {
29+
throw new Error(
30+
'Vite entry points not found. Please check that your project has ' +
31+
'an entry.client.{jsx,tsx} and entry.server.{jsx,tsx} file in ' +
32+
'the web/src directory.'
33+
)
34+
}
35+
3036
if (!rwPaths.web.viteConfig) {
3137
throw new Error(
3238
'Vite config not found. You need to setup your project with Vite using `yarn rw setup vite`'
3339
)
3440
}
41+
// ~~~~ Dev time validations ~~~~
3542

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

50-
app.use('*', async (req, res, next) => {
51-
const currentPathName = stripQueryStringAndHashFromPath(req.originalUrl)
52-
globalThis.__REDWOOD__HELMET_CONTEXT = {}
53-
54-
try {
55-
const routes = getProjectRoutes()
56-
57-
// Do a simple match with regex, don't bother parsing params yet
58-
const currentRoute = routes.find((route) => {
59-
if (!route.matchRegexString) {
60-
// This is the 404/NotFoundPage case
61-
return false
62-
}
63-
64-
const matches = [
65-
...currentPathName.matchAll(new RegExp(route.matchRegexString, 'g')),
66-
]
67-
68-
return matches.length > 0
69-
})
70-
71-
let metaTags: TagDescriptor[] = []
72-
73-
if (currentRoute?.redirect) {
74-
return res.redirect(currentRoute.redirect.to)
75-
}
76-
77-
if (currentRoute) {
78-
const parsedParams = currentRoute.hasParams
79-
? matchPath(currentRoute.path, currentPathName).params
80-
: undefined
81-
82-
const routeHookOutput = await loadAndRunRouteHooks({
83-
paths: [getAppRouteHook(), currentRoute.routeHooks],
84-
reqMeta: {
85-
req,
86-
parsedParams,
87-
},
88-
viteDevServer: vite, // because its dev
89-
})
90-
91-
metaTags = routeHookOutput.meta
92-
}
93-
94-
if (!currentRoute) {
95-
// TODO (STREAMING) do something
96-
}
97-
98-
if (!rwPaths.web.entryServer || !rwPaths.web.entryClient) {
99-
throw new Error(
100-
'Vite entry points not found. Please check that your project has ' +
101-
'an entry.client.{jsx,tsx} and entry.server.{jsx,tsx} file in ' +
102-
'the web/src directory.'
103-
)
104-
}
105-
106-
// 3. Load the server entry. vite.ssrLoadModule automatically transforms
107-
// your ESM source code to be usable in Node.js! There is no bundling
108-
// required, and provides efficient invalidation similar to HMR.
109-
const { ServerEntry } = await vite.ssrLoadModule(rwPaths.web.entryServer)
110-
111-
const pageWithJs = currentRoute?.renderMode !== 'html'
112-
113-
res.setHeader('content-type', 'text/html; charset=utf-8')
114-
115-
reactRenderToStream({
116-
ServerEntry,
117-
currentPathName,
118-
metaTags,
119-
includeJs: pageWithJs,
120-
res,
121-
})
122-
} catch (e) {
123-
// TODO (STREAMING) Is this what we want to do?
124-
// send back a SPA page
125-
// res.status(200).set({ 'Content-Type': 'text/html' }).end(template)
126-
127-
// If an error is caught, let Vite fix the stack trace so it maps back to
128-
// your actual source code.
129-
vite.ssrFixStacktrace(e as any)
130-
next(e)
57+
const routes = getProjectRoutes()
58+
59+
// TODO (STREAMING) CSS is handled by Vite in dev mode, we don't need to
60+
// worry about it in dev but..... it causes a flash of unstyled content.
61+
// For now I'm just injecting index css here
62+
// Look at collectStyles in packages/vite/src/fully-react/find-styles.ts
63+
const FIXME_HardcodedIndexCss = ['index.css']
64+
65+
for (const route of routes) {
66+
const routeHandler = await createReactStreamingHandler(
67+
{
68+
route,
69+
clientEntryPath: rwPaths.web.entryClient as string,
70+
cssLinks: FIXME_HardcodedIndexCss,
71+
},
72+
vite
73+
)
74+
75+
// @TODO if it is a 404, hand over to 404 handler
76+
if (!route.matchRegexString) {
77+
continue
13178
}
132-
})
79+
80+
const expressPathDef = route.hasParams
81+
? route.matchRegexString
82+
: route.pathDefinition
83+
84+
app.get(expressPathDef, routeHandler)
85+
}
13386

13487
const port = getConfig().web.port
13588
console.log(`Started server on http://localhost:${port}`)
@@ -141,7 +94,9 @@ let devApp = createServer()
14194
process.stdin.on('data', async (data) => {
14295
const str = data.toString().trim().toLowerCase()
14396
if (str === 'rs' || str === 'restart') {
144-
;(await devApp).close()
145-
devApp = createServer()
97+
console.log('Restarting dev web server.....')
98+
;(await devApp).close(() => {
99+
devApp = createServer()
100+
})
146101
}
147102
})

0 commit comments

Comments
 (0)