Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce RSC "live reload." #10932

Merged
merged 14 commits into from
Jul 25, 2024
4 changes: 4 additions & 0 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"bin": {
"rw-dev-fe": "./dist/devFeServer.js",
"rw-dev-rsc": "./dist/devRscServer.js",
Copy link
Contributor Author

@peterp peterp Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a command that will be executed by the Redwood CLI.

"rw-serve-fe": "./dist/runFeServer.js",
"rw-vite-build": "./bins/rw-vite-build.mjs",
"rw-vite-dev": "./bins/rw-vite-dev.mjs",
Expand Down Expand Up @@ -83,15 +84,18 @@
"acorn-loose": "8.4.0",
"buffer": "6.0.3",
"busboy": "^1.6.0",
"chokidar": "3.6.0",
"cookie": "0.6.0",
"core-js": "3.37.1",
"dotenv-defaults": "5.0.2",
"execa": "9.3.0",
"express": "4.19.2",
"find-my-way": "8.2.0",
"http-proxy-middleware": "2.0.6",
"isbot": "5.1.9",
"react": "19.0.0-beta-04b058868c-20240508",
"react-server-dom-webpack": "19.0.0-beta-04b058868c-20240508",
"rimraf": "5.0.7",
"vite": "5.3.1",
"vite-plugin-cjs-interop": "2.1.1",
"vite-plugin-node-polyfills": "^0.22.0",
Expand Down
28 changes: 28 additions & 0 deletions packages/vite/src/devRscServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getPaths } from '@redwoodjs/project-config'

import { buildRouteHooks } from './buildRouteHooks'
import { buildRouteManifest } from './buildRouteManifest'
import { buildRscClientAndServer } from './buildRscClientAndServer'
import { buildForStreamingServer } from './streaming/buildForStreamingServer'
import { startLiveReload } from './watch'

export async function build() {
await buildRscClientAndServer({ verbose: false })
await buildForStreamingServer({ verbose: false })
await buildRouteHooks(false, getPaths())
await buildRouteManifest()
}

export async function initDevRscServer() {
process.env.NODE_ENV = 'development'
await startLiveReload()
}

initDevRscServer()
.then(() => {
console.log('we are ready', process.env.NODE_ENV)
})
.catch((e) => {
//
console.log('oh no!', e)
})
3 changes: 3 additions & 0 deletions packages/vite/src/runFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ loadDotEnv({
// ------------------------------------------------

export async function runFeServer() {
const { sendMessage } = await import('execa')
peterp marked this conversation as resolved.
Show resolved Hide resolved

const app = express()
const rwPaths = getPaths()
const rwConfig = getConfig()
Expand Down Expand Up @@ -199,6 +201,7 @@ export async function runFeServer() {
console.log(
`Started production FE server on http://localhost:${rwConfig.web.port}`,
)
sendMessage('server ready')
}

runFeServer()
16 changes: 15 additions & 1 deletion packages/vite/src/streaming/streamHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ globalThis.__webpack_require__ ||= (id) => {
};
`

const rscLiveReload = `\
// NOTE: This code is used during development to enable "live-reload."
window.addEventListener('load', () => {
const sse = new EventSource('http://localhost:8913');
sse.addEventListener('reload', () => {
window.location.reload();
});
window.addEventListener('beforeunload', () => {
sse.close();
});
// TODO: Handle disconnect / error states.
Tobbe marked this conversation as resolved.
Show resolved Hide resolved
});
`

export async function reactRenderToStreamResponse(
mwRes: MiddlewareResponse,
renderOptions: RenderToStreamArgs,
Expand Down Expand Up @@ -179,7 +193,7 @@ export async function reactRenderToStreamResponse(
bootstrapScriptContent:
// Only insert assetMap if client side JS will be loaded
jsBundles.length > 0
? `window.__REDWOOD__ASSET_MAP = ${assetMap}; ${rscWebpackShims}`
? `window.__REDWOOD__ASSET_MAP = ${assetMap}; ${rscWebpackShims}; ${rscLiveReload};`
: undefined,
bootstrapModules: jsBundles,
}
Expand Down
120 changes: 120 additions & 0 deletions packages/vite/src/watch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import http from 'node:http'

import chokidar from 'chokidar'
import { type ResultPromise } from 'execa'
import { rimraf } from 'rimraf'

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

import { build } from './devRscServer'

export const startLiveReload = async () => {
const { execaNode } = await import('execa')

let child: ResultPromise
let client: http.ServerResponse

const sendReloadEvent = () => {
if (typeof client !== 'undefined') {
client.write('event: reload\n')
client.write('data: \n\n')
}
}

const rwjsPaths = getPaths()

const watcher = chokidar.watch('(web|api)/src/**/*.{ts,js,jsx,tsx}', {
persistent: true,
ignored: ['node_modules', '.redwood'],
ignoreInitial: true,
cwd: rwjsPaths.base,
awaitWriteFinish: true,
})

process.stdin.on('data', async (data) => {
const str = data.toString().trim().toLowerCase()
if (str === 'rs') {
await rimraf.rimraf(rwjsPaths.web.dist)
await rimraf.rimraf(rwjsPaths.api.dist)
await build()

try {
child.kill()
} catch (e) {
console.log(e)
}
child = execaNode('../node_modules/@redwoodjs/vite/dist/runFeServer.js', {
ipc: true,
shell: true,
stdio: 'inherit',
})
if (typeof child !== 'undefined') {
const m = await child.getOneMessage()
if (m === 'server ready') {
sendReloadEvent()
}
}

try {
await child
} catch (e) {
console.log('.....')
}
}
})

watcher
.on('ready', async () => {
console.log('[live reload started]')
await rimraf.rimraf(rwjsPaths.web.dist)
await rimraf.rimraf(rwjsPaths.api.dist)
await build()

child = execaNode('../node_modules/@redwoodjs/vite/dist/runFeServer.js', {
ipc: true,
shell: true,
stdio: 'inherit',
})

http
.createServer(async (_req, res) => {
client = res
res.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive',
'access-control-allow-origin': '*',
})
})
.listen(8913)

try {
await child
} catch (e) {
console.log('.....')
}
})
.on('all', async (eventName, p) => {
console.log('[live reload] event', eventName, p)
await rimraf.rimraf(rwjsPaths.web.dist)
await rimraf.rimraf(rwjsPaths.api.dist)
await build()

child.kill()
child = execaNode('../node_modules/@redwoodjs/vite/dist/runFeServer.js', {
ipc: true,
})
if (typeof child !== 'undefined') {
const m = await child.getOneMessage()
if (m === 'server ready') {
sendReloadEvent()
}
}

try {
await child
} catch (e) {
console.log('.....')
}
})
}
Loading
Loading