diff --git a/packages/cli/src/commands/__tests__/serve.test.js b/packages/cli/src/commands/__tests__/serve.test.js index d5b482565511..f45e60380c43 100644 --- a/packages/cli/src/commands/__tests__/serve.test.js +++ b/packages/cli/src/commands/__tests__/serve.test.js @@ -36,10 +36,15 @@ jest.mock('fs', () => { } }) -jest.mock('../serveHandler', () => { +jest.mock('../serveApiHandler', () => { return { - ...jest.requireActual('../serveHandler'), + ...jest.requireActual('../serveApiHandler'), apiServerHandler: jest.fn(), + } +}) +jest.mock('../serveBothHandler', () => { + return { + ...jest.requireActual('../serveBothHandler'), bothServerHandler: jest.fn(), } }) @@ -54,7 +59,8 @@ import execa from 'execa' import yargs from 'yargs' import { builder } from '../serve' -import { apiServerHandler, bothServerHandler } from '../serveHandler' +import { apiServerHandler } from '../serveApiHandler' +import { bothServerHandler } from '../serveBothHandler' describe('yarn rw serve', () => { afterEach(() => { diff --git a/packages/cli/src/commands/deploy/flightcontrol.js b/packages/cli/src/commands/deploy/flightcontrol.js index c5645105a3aa..1274773bc17a 100644 --- a/packages/cli/src/commands/deploy/flightcontrol.js +++ b/packages/cli/src/commands/deploy/flightcontrol.js @@ -7,7 +7,7 @@ import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' import { getConfig } from '@redwoodjs/project-config' import { getPaths } from '../../lib' -import { apiServerHandler } from '../serveHandler' +import { apiServerHandler } from '../serveApiHandler' export const command = 'flightcontrol ' export const alias = 'fc' diff --git a/packages/cli/src/commands/deploy/render.js b/packages/cli/src/commands/deploy/render.js index 5e0ee249fadb..1faf8a23eeea 100644 --- a/packages/cli/src/commands/deploy/render.js +++ b/packages/cli/src/commands/deploy/render.js @@ -7,7 +7,7 @@ import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' import { getConfig } from '@redwoodjs/project-config' import { getPaths } from '../../lib' -import { apiServerHandler } from '../serveHandler' +import { apiServerHandler } from '../serveApiHandler' export const command = 'render ' export const description = 'Build, Migrate, and Serve command for Render deploy' diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js index 9674b9dcb754..10475aa1a83f 100644 --- a/packages/cli/src/commands/serve.js +++ b/packages/cli/src/commands/serve.js @@ -1,8 +1,6 @@ import fs from 'fs' import path from 'path' -import chalk from 'chalk' -import execa from 'execa' import terminalLink from 'terminal-link' import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' @@ -10,6 +8,8 @@ import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' import { getPaths, getConfig } from '../lib' import c from '../lib/colors' +import { webServerHandler, webSsrServerHandler } from './serveWebHandler' + export const command = 'serve [side]' export const description = 'Run server for api or web in production' @@ -43,99 +43,16 @@ export const builder = async (yargs) => { // Run the experimental server file, if it exists, with web side also if (hasExperimentalServerFile()) { - console.log( - [ - separator, - `🧪 ${chalk.green('Experimental Feature')} 🧪`, - separator, - 'Using the experimental API server file at api/dist/server.js', - separator, - ].join('\n') + const { bothExperimentalServerFileHandler } = await import( + './serveBothHandler.js' ) - - if (getConfig().experimental?.rsc?.enabled) { - console.warn('') - console.warn('⚠️ Skipping Fastify web server ⚠️') - console.warn('⚠️ Using new RSC server instead ⚠️') - console.warn('') - await execa( - 'node', - [ - '--conditions react-server', - './node_modules/@redwoodjs/vite/dist/runRscFeServer.js', - ], - { - cwd: getPaths().base, - stdio: 'inherit', - shell: true, - } - ) - } else if (getConfig().experimental?.streamingSsr?.enabled) { - console.warn('') - console.warn('⚠️ Skipping Fastify web server ⚠️') - console.warn('⚠️ Using new Streaming FE server instead ⚠️') - console.warn('') - await execa('yarn', ['rw-serve-fe'], { - cwd: getPaths().web.base, - stdio: 'inherit', - shell: true, - }) - } else { - await execa( - 'yarn', - ['node', path.join('dist', 'server.js'), '--enable-web'], - { - cwd: getPaths().api.base, - stdio: 'inherit', - shell: true, - } - ) - } - return - } - - if (getConfig().experimental?.rsc?.enabled) { - const { apiServerHandler } = await import('./serveHandler.js') - // TODO (RSC) Allow specifying port, socket and apiRootPath - const apiPromise = apiServerHandler({ - ...argv, - port: 8911, - apiRootPath: '/', - }) - - // TODO (RSC) More gracefully handle Ctrl-C - const fePromise = execa( - 'node', - [ - '--conditions react-server', - './node_modules/@redwoodjs/vite/dist/runRscFeServer.js', - ], - { - cwd: getPaths().base, - stdio: 'inherit', - shell: true, - } - ) - - await Promise.all([apiPromise, fePromise]) + await bothExperimentalServerFileHandler() + } else if (getConfig().experimental?.rsc?.enabled) { + const { bothRscServerHandler } = await import('./serveBothHandler.js') + await bothRscServerHandler(argv) } else if (getConfig().experimental?.streamingSsr?.enabled) { - const { apiServerHandler } = await import('./serveHandler.js') - // TODO (STREAMING) Allow specifying port, socket and apiRootPath - const apiPromise = apiServerHandler({ - ...argv, - port: 8911, - apiRootPath: '/', - }) - - // TODO (STREAMING) More gracefully handle Ctrl-C - // Right now you get a big red error box when you kill the process - const fePromise = execa('yarn', ['rw-serve-fe'], { - cwd: getPaths().web.base, - stdio: 'inherit', - shell: true, - }) - - await Promise.all([apiPromise, fePromise]) + const { bothSsrServerHandler } = await import('./serveBothHandler.js') + await bothSsrServerHandler(argv) } else { // Wanted to use the new web-server package here, but can't because // of backwards compatibility reasons. With `bothServerHandler` both @@ -144,7 +61,7 @@ export const builder = async (yargs) => { // them on the same port, and so we lose backwards compatibility. // TODO: Use @redwoodjs/web-server when we're ok with breaking // backwards compatibility. - const { bothServerHandler } = await import('./serveHandler.js') + const { bothServerHandler } = await import('./serveBothHandler.js') await bothServerHandler(argv) } }, @@ -179,25 +96,14 @@ export const builder = async (yargs) => { // Run the experimental server file, if it exists, api side only if (hasExperimentalServerFile()) { - console.log( - [ - separator, - `🧪 ${chalk.green('Experimental Feature')} 🧪`, - separator, - 'Using the experimental API server file at api/dist/server.js', - separator, - ].join('\n') + const { apiExperimentalServerFileHandler } = await import( + './serveApiHandler.js' ) - await execa('yarn', ['node', path.join('dist', 'server.js')], { - cwd: getPaths().api.base, - stdio: 'inherit', - shell: true, - }) - return + await apiExperimentalServerFileHandler() + } else { + const { apiServerHandler } = await import('./serveApiHandler.js') + await apiServerHandler(argv) } - - const { apiServerHandler } = await import('./serveHandler.js') - await apiServerHandler(argv) }, }) .command({ @@ -227,29 +133,9 @@ export const builder = async (yargs) => { }) if (getConfig().experimental?.streamingSsr?.enabled) { - await execa('yarn', ['rw-serve-fe'], { - cwd: getPaths().web.base, - stdio: 'inherit', - shell: true, - }) + await webSsrServerHandler() } else { - await execa( - 'yarn', - [ - 'rw-web-server', - '--port', - argv.port, - '--socket', - argv.socket, - '--api-host', - argv.apiHost, - ], - { - cwd: getPaths().base, - stdio: 'inherit', - shell: true, - } - ) + await webServerHandler(argv) } }, }) @@ -312,10 +198,6 @@ export const builder = async (yargs) => { ) } -const separator = chalk.hex('#ff845e')( - '------------------------------------------------------------------' -) - // We'll clean this up later, but for now note that this function is // duplicated between this package and @redwoodjs/fastify // to avoid importing @redwoodjs/fastify when the CLI starts. diff --git a/packages/cli/src/commands/serveApiHandler.js b/packages/cli/src/commands/serveApiHandler.js new file mode 100644 index 000000000000..278d5031b51a --- /dev/null +++ b/packages/cli/src/commands/serveApiHandler.js @@ -0,0 +1,89 @@ +import path from 'path' + +import chalk from 'chalk' +import execa from 'execa' + +import { createFastifyInstance, redwoodFastifyAPI } from '@redwoodjs/fastify' +import { getPaths } from '@redwoodjs/project-config' + +export const apiExperimentalServerFileHandler = async () => { + logExperimentalHeader() + + await execa('yarn', ['node', path.join('dist', 'server.js')], { + cwd: getPaths().api.base, + stdio: 'inherit', + shell: true, + }) + return +} + +export const apiServerHandler = async (options) => { + const { port, socket, apiRootPath } = options + const tsApiServer = Date.now() + + console.log(chalk.dim.italic('Starting API Server...')) + + const fastify = createFastifyInstance() + + process.on('exit', () => { + fastify?.close() + }) + + await fastify.register(redwoodFastifyAPI, { + redwood: { + ...options, + }, + }) + + let listenOptions + + if (socket) { + listenOptions = { path: socket } + } else { + listenOptions = { + port, + host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::', + } + } + + fastify.listen(listenOptions) + + fastify.ready(() => { + fastify.log.trace( + { custom: { ...fastify.initialConfig } }, + 'Fastify server configuration' + ) + fastify.log.trace(`Registered plugins \n${fastify.printPlugins()}`) + console.log(chalk.italic.dim('Took ' + (Date.now() - tsApiServer) + ' ms')) + + const on = socket + ? socket + : chalk.magenta(`http://localhost:${port}${apiRootPath}`) + + console.log(`API listening on ${on}`) + const graphqlEnd = chalk.magenta(`${apiRootPath}graphql`) + console.log(`GraphQL endpoint at ${graphqlEnd}`) + + sendProcessReady() + }) +} + +function sendProcessReady() { + return process.send && process.send('ready') +} + +const separator = chalk.hex('#ff845e')( + '------------------------------------------------------------------' +) + +function logExperimentalHeader() { + console.log( + [ + separator, + `🧪 ${chalk.green('Experimental Feature')} 🧪`, + separator, + 'Using the experimental API server file at api/dist/server.js', + separator, + ].join('\n') + ) +} diff --git a/packages/cli/src/commands/serveBothHandler.js b/packages/cli/src/commands/serveBothHandler.js new file mode 100644 index 000000000000..ea37ad298545 --- /dev/null +++ b/packages/cli/src/commands/serveBothHandler.js @@ -0,0 +1,185 @@ +import path from 'path' + +import chalk from 'chalk' +import execa from 'execa' + +import { + coerceRootPath, + createFastifyInstance, + redwoodFastifyAPI, + redwoodFastifyWeb, +} from '@redwoodjs/fastify' +import { getConfig, getPaths } from '@redwoodjs/project-config' + +export const bothExperimentalServerFileHandler = async () => { + logExperimentalHeader() + + if (getConfig().experimental?.rsc?.enabled) { + logSkippingFastifyWebServer() + + await execa( + 'node', + [ + '--conditions react-server', + './node_modules/@redwoodjs/vite/dist/runRscFeServer.js', + ], + { + cwd: getPaths().base, + stdio: 'inherit', + shell: true, + } + ) + } else if (getConfig().experimental?.streamingSsr?.enabled) { + logSkippingFastifyWebServer() + + await execa('yarn', ['rw-serve-fe'], { + cwd: getPaths().web.base, + stdio: 'inherit', + shell: true, + }) + } else { + await execa( + 'yarn', + ['node', path.join('dist', 'server.js'), '--enable-web'], + { + cwd: getPaths().api.base, + stdio: 'inherit', + shell: true, + } + ) + } +} + +export const bothRscServerHandler = async (argv) => { + const { apiServerHandler } = await import('./serveApiHandler.js') + + // TODO (RSC) Allow specifying port, socket and apiRootPath + const apiPromise = apiServerHandler({ + ...argv, + port: 8911, + apiRootPath: '/', + }) + + // TODO (RSC) More gracefully handle Ctrl-C + const fePromise = execa( + 'node', + [ + '--conditions react-server', + './node_modules/@redwoodjs/vite/dist/runRscFeServer.js', + ], + { + cwd: getPaths().base, + stdio: 'inherit', + shell: true, + } + ) + + await Promise.all([apiPromise, fePromise]) +} + +export const bothSsrServerHandler = async (argv) => { + const { apiServerHandler } = await import('./serveApiHandler.js') + + // TODO (STREAMING) Allow specifying port, socket and apiRootPath + const apiPromise = apiServerHandler({ + ...argv, + port: 8911, + apiRootPath: '/', + }) + + // TODO (STREAMING) More gracefully handle Ctrl-C + // Right now you get a big red error box when you kill the process + const fePromise = execa('yarn', ['rw-serve-fe'], { + cwd: getPaths().web.base, + stdio: 'inherit', + shell: true, + }) + + await Promise.all([apiPromise, fePromise]) +} + +export const bothServerHandler = async (options) => { + const { port, socket } = options + const tsServer = Date.now() + + console.log(chalk.italic.dim('Starting API and Web Servers...')) + + const fastify = createFastifyInstance() + + process.on('exit', () => { + fastify?.close() + }) + + await fastify.register(redwoodFastifyWeb, { + redwood: { + ...options, + }, + }) + + const apiRootPath = coerceRootPath(getConfig().web.apiUrl) + + await fastify.register(redwoodFastifyAPI, { + redwood: { + ...options, + apiRootPath, + }, + }) + + let listenOptions + + if (socket) { + listenOptions = { path: socket } + } else { + listenOptions = { + port, + host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::', + } + } + + fastify.listen(listenOptions) + + fastify.ready(() => { + console.log(chalk.italic.dim('Took ' + (Date.now() - tsServer) + ' ms')) + + const on = socket + ? socket + : chalk.magenta(`http://localhost:${port}${apiRootPath}`) + + const webServer = chalk.green(`http://localhost:${port}`) + const apiServer = chalk.magenta(`http://localhost:${port}`) + console.log(`Web server started on ${webServer}`) + console.log(`API serving from ${apiServer}`) + console.log(`API listening on ${on}`) + const graphqlEnd = chalk.magenta(`${apiRootPath}graphql`) + console.log(`GraphQL endpoint at ${graphqlEnd}`) + + sendProcessReady() + }) +} + +function sendProcessReady() { + return process.send && process.send('ready') +} + +const separator = chalk.hex('#ff845e')( + '------------------------------------------------------------------' +) + +function logExperimentalHeader() { + console.log( + [ + separator, + `🧪 ${chalk.green('Experimental Feature')} 🧪`, + separator, + 'Using the experimental API server file at api/dist/server.js', + separator, + ].join('\n') + ) +} + +function logSkippingFastifyWebServer() { + console.warn('') + console.warn('⚠️ Skipping Fastify web server ⚠️') + console.warn('⚠️ Using new RSC server instead ⚠️') + console.warn('') +} diff --git a/packages/cli/src/commands/serveHandler.js b/packages/cli/src/commands/serveHandler.js deleted file mode 100644 index 7582da3d7cad..000000000000 --- a/packages/cli/src/commands/serveHandler.js +++ /dev/null @@ -1,123 +0,0 @@ -import chalk from 'chalk' - -import { - coerceRootPath, - createFastifyInstance, - redwoodFastifyAPI, - redwoodFastifyWeb, -} from '@redwoodjs/fastify' -import { getConfig } from '@redwoodjs/project-config' - -export const apiServerHandler = async (options) => { - const { port, socket, apiRootPath } = options - const tsApiServer = Date.now() - - console.log(chalk.dim.italic('Starting API Server...')) - - const fastify = createFastifyInstance() - - process.on('exit', () => { - fastify?.close() - }) - - await fastify.register(redwoodFastifyAPI, { - redwood: { - ...options, - }, - }) - - let listenOptions - - if (socket) { - listenOptions = { path: socket } - } else { - listenOptions = { - port, - host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::', - } - } - - fastify.listen(listenOptions) - - fastify.ready(() => { - fastify.log.trace( - { custom: { ...fastify.initialConfig } }, - 'Fastify server configuration' - ) - fastify.log.trace(`Registered plugins \n${fastify.printPlugins()}`) - console.log(chalk.italic.dim('Took ' + (Date.now() - tsApiServer) + ' ms')) - - const on = socket - ? socket - : chalk.magenta(`http://localhost:${port}${apiRootPath}`) - - console.log(`API listening on ${on}`) - const graphqlEnd = chalk.magenta(`${apiRootPath}graphql`) - console.log(`GraphQL endpoint at ${graphqlEnd}`) - - sendProcessReady() - }) -} - -export const bothServerHandler = async (options) => { - const { port, socket } = options - const tsServer = Date.now() - - console.log(chalk.italic.dim('Starting API and Web Servers...')) - - const fastify = createFastifyInstance() - - process.on('exit', () => { - fastify?.close() - }) - - await fastify.register(redwoodFastifyWeb, { - redwood: { - ...options, - }, - }) - - const apiRootPath = coerceRootPath(getConfig().web.apiUrl) - - await fastify.register(redwoodFastifyAPI, { - redwood: { - ...options, - apiRootPath, - }, - }) - - let listenOptions - - if (socket) { - listenOptions = { path: socket } - } else { - listenOptions = { - port, - host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::', - } - } - - fastify.listen(listenOptions) - - fastify.ready(() => { - console.log(chalk.italic.dim('Took ' + (Date.now() - tsServer) + ' ms')) - - const on = socket - ? socket - : chalk.magenta(`http://localhost:${port}${apiRootPath}`) - - const webServer = chalk.green(`http://localhost:${port}`) - const apiServer = chalk.magenta(`http://localhost:${port}`) - console.log(`Web server started on ${webServer}`) - console.log(`API serving from ${apiServer}`) - console.log(`API listening on ${on}`) - const graphqlEnd = chalk.magenta(`${apiRootPath}graphql`) - console.log(`GraphQL endpoint at ${graphqlEnd}`) - - sendProcessReady() - }) -} - -function sendProcessReady() { - return process.send && process.send('ready') -} diff --git a/packages/cli/src/commands/serveWebHandler.js b/packages/cli/src/commands/serveWebHandler.js new file mode 100644 index 000000000000..ae3c33972132 --- /dev/null +++ b/packages/cli/src/commands/serveWebHandler.js @@ -0,0 +1,31 @@ +import execa from 'execa' + +import { getPaths } from '@redwoodjs/project-config' + +export const webSsrServerHandler = async () => { + await execa('yarn', ['rw-serve-fe'], { + cwd: getPaths().web.base, + stdio: 'inherit', + shell: true, + }) +} + +export const webServerHandler = async (argv) => { + await execa( + 'yarn', + [ + 'rw-web-server', + '--port', + argv.port, + '--socket', + argv.socket, + '--api-host', + argv.apiHost, + ], + { + cwd: getPaths().base, + stdio: 'inherit', + shell: true, + } + ) +}