diff --git a/.vscode/settings.json b/.vscode/settings.json index fe09e853a9bc..26702c59a1f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,5 +20,11 @@ "files.trimTrailingWhitespace": false }, "typescript.tsdk": "node_modules/typescript/lib", - "peacock.color": "#b85833" + "peacock.color": "#b85833", + "cSpell.words": [ + "Fastify", + "pino", + "redwoodjs", + "RWJS" + ] } diff --git a/__fixtures__/empty-project/api/server.config.js b/__fixtures__/empty-project/api/server.config.js deleted file mode 100644 index 9fa6aaa05871..000000000000 --- a/__fixtures__/empty-project/api/server.config.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * This file allows you to configure the Fastify Server settings - * used by the RedwoodJS dev server. - * - * It also applies when running the api server with `yarn rw serve`. - * - * For the Fastify server options that you can set, see: - * https://www.fastify.io/docs/latest/Reference/Server/#factory - * - * Examples include: logger settings, timeouts, maximum payload limits, and more. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('fastify').FastifyServerOptions} */ -const config = { - requestTimeout: 15_000, - logger: { - level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', - }, -} - -module.exports = config diff --git a/__fixtures__/fragment-test-project/api/server.config.js b/__fixtures__/fragment-test-project/api/server.config.js deleted file mode 100644 index 73dca9225a3e..000000000000 --- a/__fixtures__/fragment-test-project/api/server.config.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file allows you to configure the Fastify Server settings - * used by the RedwoodJS dev server. - * - * It also applies when running RedwoodJS with `yarn rw serve`. - * - * For the Fastify server options that you can set, see: - * https://www.fastify.io/docs/latest/Reference/Server/#factory - * - * Examples include: logger settings, timeouts, maximum payload limits, and more. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('fastify').FastifyServerOptions} */ -const config = { - requestTimeout: 15_000, - logger: { - // Note: If running locally using `yarn rw serve` you may want to adjust - // the default non-development level to `info` - level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', - }, -} - -/** - * You can also register Fastify plugins and additional routes for the API and Web sides - * in the configureFastify function. - * - * This function has access to the Fastify instance and options, such as the side - * (web, api, or proxy) that is being configured and other settings like the apiRootPath - * of the functions endpoint. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */ -const configureFastify = async (fastify, options) => { - if (options.side === 'api') { - fastify.log.trace({ custom: { options } }, 'Configuring api side') - } - - if (options.side === 'web') { - fastify.log.trace({ custom: { options } }, 'Configuring web side') - } - - return fastify -} - -module.exports = { - config, - configureFastify, -} diff --git a/__fixtures__/test-project-rsa/api/server.config.js b/__fixtures__/test-project-rsa/api/server.config.js deleted file mode 100644 index 73dca9225a3e..000000000000 --- a/__fixtures__/test-project-rsa/api/server.config.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file allows you to configure the Fastify Server settings - * used by the RedwoodJS dev server. - * - * It also applies when running RedwoodJS with `yarn rw serve`. - * - * For the Fastify server options that you can set, see: - * https://www.fastify.io/docs/latest/Reference/Server/#factory - * - * Examples include: logger settings, timeouts, maximum payload limits, and more. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('fastify').FastifyServerOptions} */ -const config = { - requestTimeout: 15_000, - logger: { - // Note: If running locally using `yarn rw serve` you may want to adjust - // the default non-development level to `info` - level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', - }, -} - -/** - * You can also register Fastify plugins and additional routes for the API and Web sides - * in the configureFastify function. - * - * This function has access to the Fastify instance and options, such as the side - * (web, api, or proxy) that is being configured and other settings like the apiRootPath - * of the functions endpoint. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */ -const configureFastify = async (fastify, options) => { - if (options.side === 'api') { - fastify.log.trace({ custom: { options } }, 'Configuring api side') - } - - if (options.side === 'web') { - fastify.log.trace({ custom: { options } }, 'Configuring web side') - } - - return fastify -} - -module.exports = { - config, - configureFastify, -} diff --git a/__fixtures__/test-project-rsc-external-packages/api/server.config.js b/__fixtures__/test-project-rsc-external-packages/api/server.config.js deleted file mode 100644 index 73dca9225a3e..000000000000 --- a/__fixtures__/test-project-rsc-external-packages/api/server.config.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file allows you to configure the Fastify Server settings - * used by the RedwoodJS dev server. - * - * It also applies when running RedwoodJS with `yarn rw serve`. - * - * For the Fastify server options that you can set, see: - * https://www.fastify.io/docs/latest/Reference/Server/#factory - * - * Examples include: logger settings, timeouts, maximum payload limits, and more. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('fastify').FastifyServerOptions} */ -const config = { - requestTimeout: 15_000, - logger: { - // Note: If running locally using `yarn rw serve` you may want to adjust - // the default non-development level to `info` - level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', - }, -} - -/** - * You can also register Fastify plugins and additional routes for the API and Web sides - * in the configureFastify function. - * - * This function has access to the Fastify instance and options, such as the side - * (web, api, or proxy) that is being configured and other settings like the apiRootPath - * of the functions endpoint. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */ -const configureFastify = async (fastify, options) => { - if (options.side === 'api') { - fastify.log.trace({ custom: { options } }, 'Configuring api side') - } - - if (options.side === 'web') { - fastify.log.trace({ custom: { options } }, 'Configuring web side') - } - - return fastify -} - -module.exports = { - config, - configureFastify, -} diff --git a/__fixtures__/test-project/api/server.config.js b/__fixtures__/test-project/api/server.config.js deleted file mode 100644 index 73dca9225a3e..000000000000 --- a/__fixtures__/test-project/api/server.config.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file allows you to configure the Fastify Server settings - * used by the RedwoodJS dev server. - * - * It also applies when running RedwoodJS with `yarn rw serve`. - * - * For the Fastify server options that you can set, see: - * https://www.fastify.io/docs/latest/Reference/Server/#factory - * - * Examples include: logger settings, timeouts, maximum payload limits, and more. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('fastify').FastifyServerOptions} */ -const config = { - requestTimeout: 15_000, - logger: { - // Note: If running locally using `yarn rw serve` you may want to adjust - // the default non-development level to `info` - level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', - }, -} - -/** - * You can also register Fastify plugins and additional routes for the API and Web sides - * in the configureFastify function. - * - * This function has access to the Fastify instance and options, such as the side - * (web, api, or proxy) that is being configured and other settings like the apiRootPath - * of the functions endpoint. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */ -const configureFastify = async (fastify, options) => { - if (options.side === 'api') { - fastify.log.trace({ custom: { options } }, 'Configuring api side') - } - - if (options.side === 'web') { - fastify.log.trace({ custom: { options } }, 'Configuring web side') - } - - return fastify -} - -module.exports = { - config, - configureFastify, -} diff --git a/packages/adapters/fastify/web/package.json b/packages/adapters/fastify/web/package.json index e6ae98ad9fcd..2ee7d8ed6450 100644 --- a/packages/adapters/fastify/web/package.json +++ b/packages/adapters/fastify/web/package.json @@ -7,16 +7,7 @@ "directory": "packages/adapters/fastify/web" }, "license": "MIT", - "exports": { - ".": { - "types": "./dist/web.d.ts", - "default": "./dist/web.js" - }, - "./helpers": { - "types": "./dist/helpers.d.ts", - "default": "./dist/helpers.js" - } - }, + "main": "./dist/web.js", "types": "./dist/web.d.ts", "files": [ "dist" diff --git a/packages/adapters/fastify/web/src/types.ts b/packages/adapters/fastify/web/src/types.ts index b6ef71673d76..4809ce1da851 100644 --- a/packages/adapters/fastify/web/src/types.ts +++ b/packages/adapters/fastify/web/src/types.ts @@ -4,14 +4,14 @@ export interface RedwoodFastifyWebOptions { * Specify the URL to your API server. * This can be a relative URL on the current domain (`/.redwood/functions`), * in which case the `apiProxyTarget` option must be set, - * or a fully qualified URL (`https://api.redwood.horse`). + * or a fully-qualified URL (`https://api.redwood.horse`). * * Note: This should not include the path to the GraphQL Server. **/ apiUrl?: string /** - * The fully qualified URL to proxy requests to from apiUrl. - * Only valid when apiUrl is a relative URL. + * The fully-qualified URL to proxy requests to from `apiUrl`. + * Only valid when `apiUrl` is a relative URL. */ apiProxyTarget?: string diff --git a/packages/adapters/fastify/web/src/web.ts b/packages/adapters/fastify/web/src/web.ts index 461aa38f7ae0..ac5e25b64f8e 100644 --- a/packages/adapters/fastify/web/src/web.ts +++ b/packages/adapters/fastify/web/src/web.ts @@ -21,28 +21,19 @@ export async function redwoodFastifyWeb( ) { const { redwoodOptions, flags } = resolveOptions(opts) - await fastify.register(fastifyUrlData) + fastify.register(fastifyUrlData) + fastify.register(fastifyStatic, { root: getPaths().web.dist }) - // Serve prerendered files directly, instead of the index - const prerenderedFiles = await fg('**/*.html', { - cwd: getPaths().web.dist, - ignore: ['index.html', '200.html', '404.html'], - }) - - for (const prerenderedFile of prerenderedFiles) { - const [pathName] = prerenderedFile.split('.html') - - fastify.get(`/${pathName}`, (_, reply) => { - reply.header('Content-Type', 'text/html; charset=UTF-8') - reply.sendFile(prerenderedFile) + // If `apiProxyTarget` is set, proxy requests from `apiUrl` to `apiProxyTarget`. + // In this case, `apiUrl` has to be relative; `resolveOptions` above throws if it's not + if (redwoodOptions.apiProxyTarget) { + fastify.register(httpProxy, { + prefix: redwoodOptions.apiUrl, + upstream: redwoodOptions.apiProxyTarget, + disableCache: true, }) } - // Serve static assets - fastify.register(fastifyStatic, { - root: getPaths().web.dist, - }) - // If `shouldRegisterApiUrl` is true, `apiUrl` has to be defined // but TS doesn't know that so it complains about `apiUrl` being undefined // in `fastify.all(...)` below. So we have to do this check for now @@ -69,20 +60,23 @@ export async function redwoodFastifyWeb( fastify.all(`${apiUrlWarningPath}*`, apiUrlHandler) } - // If `apiProxyTarget` is set, proxy requests from `apiUrl` to `apiProxyTarget`. - // In this case, `apiUrl` has to be relative; `resolveOptions` above throws if it's not - if (redwoodOptions.apiProxyTarget) { - fastify.register(httpProxy, { - prefix: redwoodOptions.apiUrl, - upstream: redwoodOptions.apiProxyTarget, - disableCache: true, + // Serve prerendered files directly, instead of the index + const prerenderedFiles = await fg('**/*.html', { + cwd: getPaths().web.dist, + ignore: ['index.html', '200.html', '404.html'], + }) + + for (const prerenderedFile of prerenderedFiles) { + const [pathName] = prerenderedFile.split('.html') + fastify.get(`/${pathName}`, (_, reply) => { + reply.header('Content-Type', 'text/html; charset=UTF-8') + reply.sendFile(prerenderedFile) }) } // If `200.html` exists, the project has been prerendered. // If it doesn't, fallback to the default (`index.html`) const prerenderIndexPath = path.join(getPaths().web.dist, '200.html') - const fallbackIndexPath = fs.existsSync(prerenderIndexPath) ? '200.html' : 'index.html' diff --git a/packages/api-server/build.mjs b/packages/api-server/build.mjs new file mode 100644 index 000000000000..0791dabccd29 --- /dev/null +++ b/packages/api-server/build.mjs @@ -0,0 +1,61 @@ +import { + build, + defaultBuildOptions, + defaultIgnorePatterns, +} from '@redwoodjs/framework-tools' + +// Build the package +await build({ + entryPointOptions: { + ignore: [ + ...defaultIgnorePatterns, + './src/bin.ts', + './src/logFormatter/bin.ts', + './src/types.ts', + './src/watch.ts', + ], + }, +}) + +// Build the rw-server bin +await build({ + buildOptions: { + ...defaultBuildOptions, + banner: { + js: '#!/usr/bin/env node', + }, + bundle: true, + entryPoints: ['./src/bin.ts'], + packages: 'external', + }, + metafileName: 'meta.rwServer.json', +}) + +// Build the logFormatter bin +await build({ + buildOptions: { + ...defaultBuildOptions, + banner: { + js: '#!/usr/bin/env node', + }, + bundle: true, + entryPoints: ['./src/logFormatter/bin.ts'], + outdir: './dist/logFormatter', + packages: 'external', + }, + metafileName: 'meta.logFormatter.json', +}) + +// Build the watch bin +await build({ + buildOptions: { + ...defaultBuildOptions, + banner: { + js: '#!/usr/bin/env node', + }, + bundle: true, + entryPoints: ['./src/watch.ts'], + packages: 'external', + }, + metafileName: 'meta.watch.json', +}) diff --git a/packages/api-server/dist.test.ts b/packages/api-server/dist.test.ts index ef10ff84ea68..c65362034211 100644 --- a/packages/api-server/dist.test.ts +++ b/packages/api-server/dist.test.ts @@ -9,82 +9,12 @@ describe('dist', () => { expect(fs.existsSync(path.join(distPath, '__tests__'))).toEqual(false) }) - // The way this package was written, you can't just import it. It expects to be in a Redwood project. - it('fails if imported outside a Redwood app', async () => { - try { - await import(path.join(distPath, 'cliHandlers.js')) - } catch (e) { - expect(e.message).toMatchInlineSnapshot( - `"Could not find a "redwood.toml" file, are you sure you're in a Redwood project?"` - ) - } - }) - - it('exports CLI options and handlers', async () => { - const original_RWJS_CWD = process.env.RWJS_CWD - - process.env.RWJS_CWD = path.join( - __dirname, - 'src/__tests__/fixtures/redwood-app' - ) - - const mod = await import( - path.resolve(distPath, packageConfig.main.replace('dist/', '')) - ) - - expect(mod).toMatchInlineSnapshot(` - { - "apiCliOptions": { - "apiRootPath": { - "alias": [ - "api-root-path", - "rootPath", - "root-path", - ], - "coerce": [Function], - "default": "/", - "desc": "Root path where your api functions are served", - "type": "string", - }, - "loadEnvFiles": { - "description": "Deprecated; env files are always loaded. This flag is a no-op", - "hidden": true, - "type": "boolean", - }, - "port": { - "alias": "p", - "default": 8911, - "type": "number", - }, - "socket": { - "type": "string", - }, - }, - "apiServerHandler": [Function], - "bothServerHandler": [Function], - "commonOptions": { - "port": { - "alias": "p", - "default": 8910, - "type": "number", - }, - "socket": { - "type": "string", - }, - }, - "createServer": [Function], - } - `) - - process.env.RWJS_CWD = original_RWJS_CWD - }) - it('ships three bins', () => { expect(packageConfig.bin).toMatchInlineSnapshot(` { "rw-api-server-watch": "./dist/watch.js", "rw-log-formatter": "./dist/logFormatter/bin.js", - "rw-server": "./dist/index.js", + "rw-server": "./dist/bin.js", } `) }) diff --git a/packages/api-server/package.json b/packages/api-server/package.json index 4c0b8de6949d..e2fcd4b15e4b 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -8,18 +8,18 @@ "directory": "packages/api-server" }, "license": "MIT", - "main": "dist/cliHandlers", + "main": "./dist/createServer.js", + "types": "./dist/createServer.d.ts", "bin": { "rw-api-server-watch": "./dist/watch.js", "rw-log-formatter": "./dist/logFormatter/bin.js", - "rw-server": "./dist/index.js" + "rw-server": "./dist/bin.js" }, "files": [ "dist" ], "scripts": { - "build": "yarn build:js && yarn build:types", - "build:js": "babel src -d dist --extensions \".js,.jsx,.ts,.tsx\"", + "build": "yarn node ./build.mjs && yarn build:types", "build:pack": "yarn pack -o redwoodjs-api-server.tgz", "build:types": "tsc --build --verbose tsconfig.build.json", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build && yarn fix:permissions\"", @@ -29,16 +29,13 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@babel/runtime-corejs3": "7.23.9", "@fastify/url-data": "5.4.0", "@redwoodjs/context": "6.0.7", "@redwoodjs/fastify-web": "6.0.7", "@redwoodjs/project-config": "6.0.7", "@redwoodjs/web-server": "6.0.7", - "ansi-colors": "4.1.3", "chalk": "4.1.2", "chokidar": "3.5.3", - "core-js": "3.35.1", "dotenv-defaults": "5.0.2", "fast-glob": "3.3.2", "fast-json-parse": "1.0.3", @@ -52,8 +49,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@babel/cli": "7.23.9", - "@babel/core": "^7.22.20", + "@redwoodjs/framework-tools": "6.0.7", "@types/aws-lambda": "8.10.126", "@types/lodash": "4.14.201", "@types/qs": "6.9.11", diff --git a/packages/api-server/src/__tests__/withFunctions.test.ts b/packages/api-server/src/__tests__/api.test.ts similarity index 81% rename from packages/api-server/src/__tests__/withFunctions.test.ts rename to packages/api-server/src/__tests__/api.test.ts index 35a2086f430a..bb5863a70361 100644 --- a/packages/api-server/src/__tests__/withFunctions.test.ts +++ b/packages/api-server/src/__tests__/api.test.ts @@ -1,7 +1,7 @@ import path from 'path' import createFastifyInstance from '../fastify' -import withFunctions from '../plugins/withFunctions' +import { redwoodFastifyAPI } from '../plugins/api' // Suppress terminal logging. console.log = jest.fn() @@ -21,14 +21,14 @@ afterAll(() => { // Set up and teardown the fastify instance for each test. let fastifyInstance: ReturnType -let returnedFastifyInstance: Awaited> beforeAll(async () => { fastifyInstance = createFastifyInstance() - returnedFastifyInstance = await withFunctions(fastifyInstance, { - port: 8911, - apiRootPath: '/', + fastifyInstance.register(redwoodFastifyAPI, { + redwood: { + loadUserConfig: true, + }, }) await fastifyInstance.ready() @@ -38,12 +38,7 @@ afterAll(async () => { await fastifyInstance.close() }) -describe('withFunctions', () => { - // Deliberately using `toBe` here to check for referential equality. - it('returns the same fastify instance', async () => { - expect(returnedFastifyInstance).toBe(fastifyInstance) - }) - +describe('redwoodFastifyAPI', () => { it('configures the `@fastify/url-data` and `fastify-raw-body` plugins', async () => { const plugins = fastifyInstance.printPlugins() @@ -51,15 +46,6 @@ describe('withFunctions', () => { expect(plugins.includes('fastify-raw-body')).toEqual(true) }) - it('configures two additional content type parsers, `application/x-www-form-urlencoded` and `multipart/form-data`', async () => { - expect( - fastifyInstance.hasContentTypeParser('application/x-www-form-urlencoded') - ).toEqual(true) - expect(fastifyInstance.hasContentTypeParser('multipart/form-data')).toEqual( - true - ) - }) - it('can be configured by the user', async () => { const res = await fastifyInstance.inject({ method: 'GET', diff --git a/packages/api-server/src/__tests__/createServer.test.ts b/packages/api-server/src/__tests__/createServer.test.ts index dfb3de9e725e..7b4e98013fe1 100644 --- a/packages/api-server/src/__tests__/createServer.test.ts +++ b/packages/api-server/src/__tests__/createServer.test.ts @@ -5,11 +5,11 @@ import build from 'pino-abstract-transport' import { getConfig } from '@redwoodjs/project-config' +import { createServer } from '../createServer' import { - createServer, resolveOptions, DEFAULT_CREATE_SERVER_OPTIONS, -} from '../createServer' +} from '../createServerHelpers' // Set up RWJS_CWD. let original_RWJS_CWD: string | undefined @@ -221,6 +221,7 @@ describe('resolveOptions', () => { logger: DEFAULT_CREATE_SERVER_OPTIONS.logger, }, port: 8911, + host: '::', }) }) @@ -290,25 +291,35 @@ describe('resolveOptions', () => { }) it('parses `--port`', () => { - expect(resolveOptions({}, ['--port', '8930']).port).toEqual(8930) + expect( + resolveOptions({ parseArgs: true }, ['--port', '8930']).port + ).toEqual(8930) }) it("throws if `--port` can't be converted to an integer", () => { expect(() => { - resolveOptions({}, ['--port', 'eight-nine-ten']) + resolveOptions({ parseArgs: true }, ['--port', 'eight-nine-ten']) }).toThrowErrorMatchingInlineSnapshot(`"\`port\` must be an integer"`) }) it('parses `--apiRootPath`', () => { - expect(resolveOptions({}, ['--apiRootPath', 'foo']).apiRootPath).toEqual( - '/foo/' - ) + expect( + resolveOptions({ parseArgs: true }, ['--apiRootPath', 'foo']).apiRootPath + ).toEqual('/foo/') }) it('the `--apiRootPath` flag has precedence', () => { expect( - resolveOptions({ apiRootPath: 'foo' }, ['--apiRootPath', 'bar']) - .apiRootPath + resolveOptions({ parseArgs: true, apiRootPath: 'foo' }, [ + '--apiRootPath', + 'bar', + ]).apiRootPath ).toEqual('/bar/') }) + + it('parses `--host`', () => { + expect( + resolveOptions({ parseArgs: true }, ['--host', '127.0.0.1']).host + ).toEqual('127.0.0.1') + }) }) diff --git a/packages/api-server/src/apiCLIConfig.ts b/packages/api-server/src/apiCLIConfig.ts new file mode 100644 index 000000000000..42d60a29372e --- /dev/null +++ b/packages/api-server/src/apiCLIConfig.ts @@ -0,0 +1,37 @@ +import type { Argv } from 'yargs' + +import type { APIParsedOptions } from './types' + +export const description = 'Start a server for serving the api side' + +export function builder(yargs: Argv) { + yargs.options({ + port: { + description: 'The port to listen at', + type: 'number', + alias: 'p', + }, + host: { + description: + "The host to listen at. Note that you most likely want this to be '0.0.0.0' in production", + type: 'string', + }, + apiRootPath: { + description: 'Root path where your api functions are served', + type: 'string', + alias: ['api-root-path', 'rootPath', 'root-path'], + default: '/', + }, + // This became a no-op in v7 because env files weren't loaded by default + // but removing it would break yargs parsing for older projects, + // so leaving it here so that yargs doesn't throw an error + loadEnvFiles: { + hidden: true, + }, + }) +} + +export async function handler(options: APIParsedOptions) { + const { handler } = await import('./apiCLIConfigHandler.js') + await handler(options) +} diff --git a/packages/api-server/src/apiCLIConfigHandler.ts b/packages/api-server/src/apiCLIConfigHandler.ts new file mode 100644 index 000000000000..8a7051a4b546 --- /dev/null +++ b/packages/api-server/src/apiCLIConfigHandler.ts @@ -0,0 +1,61 @@ +import chalk from 'chalk' + +import { getAPIPort, getAPIHost } from './cliHelpers' +import createFastifyInstance from './fastify' +import { redwoodFastifyAPI } from './plugins/api' +import type { APIParsedOptions } from './types' + +export async function handler(options: APIParsedOptions) { + const timeStart = Date.now() + console.log(chalk.dim.italic('Starting API Server...')) + + const fastify = createFastifyInstance() + fastify.register(redwoodFastifyAPI, { + redwood: { + ...options, + loadUserConfig: true, + }, + }) + + options.host ??= getAPIHost() + options.port ??= getAPIPort() + + await fastify.listen({ + port: options.port, + host: options.host, + listenTextResolver: (address) => { + // In the past, in development, we've prioritized showing a friendlier + // host than the listen-on-all-ipv6-addresses '[::]'. Here we replace it + // with 'localhost' only if 1) we're not in production and 2) it's there. + // In production it's important to be transparent. + if (process.env.NODE_ENV !== 'production') { + address = address.replace(/http:\/\/\[::\]/, 'http://localhost') + } + + return `Server listening at ${address}` + }, + }) + + fastify.log.trace( + { custom: { ...fastify.initialConfig } }, + 'Fastify server configuration' + ) + fastify.log.trace(`Registered plugins\n${fastify.printPlugins()}`) + + console.log(chalk.dim.italic('Took ' + (Date.now() - timeStart) + ' ms')) + + // We have this logic for `apiServerHandler` because this is the only + // handler called by the watch bin (which is called by `yarn rw dev`). + let address = fastify.listeningOrigin + if (process.env.NODE_ENV !== 'production') { + address = address.replace(/http:\/\/\[::\]/, 'http://localhost') + } + + const apiServer = chalk.magenta(`${address}${options.apiRootPath}`) + const graphqlEndpoint = chalk.magenta(`${apiServer}graphql`) + + console.log(`API server listening at ${apiServer}`) + console.log(`GraphQL endpoint at ${graphqlEndpoint}`) + + process?.send?.('ready') +} diff --git a/packages/api-server/src/bin.ts b/packages/api-server/src/bin.ts new file mode 100644 index 000000000000..cb30140aa0cd --- /dev/null +++ b/packages/api-server/src/bin.ts @@ -0,0 +1,63 @@ +import path from 'path' + +import { config } from 'dotenv-defaults' +import { hideBin } from 'yargs/helpers' +import yargs from 'yargs/yargs' + +import { getPaths } from '@redwoodjs/project-config' +import { + description as webDescription, + builder as webBuilder, + handler as webHandler, +} from '@redwoodjs/web-server' + +import { + description as apiDescription, + builder as apiBuilder, +} from './apiCLIConfig' +import { handler as apiHandler } from './apiCLIConfigHandler' +import { + description as bothDescription, + builder as bothBuilder, +} from './bothCLIConfig' +import { handler as bothHandler } from './bothCLIConfigHandler' + +if (!process.env.REDWOOD_ENV_FILES_LOADED) { + config({ + path: path.join(getPaths().base, '.env'), + defaults: path.join(getPaths().base, '.env.defaults'), + multiline: true, + }) + + process.env.REDWOOD_ENV_FILES_LOADED = 'true' +} + +process.env.NODE_ENV ??= 'production' + +yargs(hideBin(process.argv)) + .scriptName('rw-server') + .strict() + .alias('h', 'help') + .alias('v', 'version') + .command( + '$0', + bothDescription, + // @ts-expect-error The yargs types seem wrong; it's ok for builder to be a function + bothBuilder, + bothHandler + ) + .command( + 'api', + apiDescription, + // @ts-expect-error The yargs types seem wrong; it's ok for builder to be a function + apiBuilder, + apiHandler + ) + .command( + 'web', + webDescription, + // @ts-expect-error The yargs types seem wrong; it's ok for builder to be a function + webBuilder, + webHandler + ) + .parse() diff --git a/packages/api-server/src/bothCLIConfig.ts b/packages/api-server/src/bothCLIConfig.ts new file mode 100644 index 000000000000..2ecb2c4491e8 --- /dev/null +++ b/packages/api-server/src/bothCLIConfig.ts @@ -0,0 +1,43 @@ +import type { Argv } from 'yargs' + +import type { BothParsedOptions } from './types' + +export const description = 'Start a server for serving the api and web sides' + +export function builder(yargs: Argv) { + yargs.options({ + webPort: { + description: 'The port for the web server to listen on', + type: 'number', + alias: ['web-port'], + }, + webHost: { + description: + "The host for the web server to listen on. Note that you most likely want this to be '0.0.0.0' in production", + type: 'string', + alias: ['web-host'], + }, + apiPort: { + description: 'The port for the api server to listen on', + type: 'number', + alias: ['api-port'], + }, + apiHost: { + description: + "The host for the api server to listen on. Note that you most likely want this to be '0.0.0.0' in production", + type: 'string', + alias: ['api-host'], + }, + apiRootPath: { + description: 'Root path where your api functions are served', + type: 'string', + alias: ['api-root-path', 'rootPath', 'root-path'], + default: '/', + }, + }) +} + +export async function handler(options: BothParsedOptions) { + const { handler } = await import('./bothCLIConfigHandler.js') + await handler(options) +} diff --git a/packages/api-server/src/bothCLIConfigHandler.ts b/packages/api-server/src/bothCLIConfigHandler.ts new file mode 100644 index 000000000000..e9b24f1d2ca5 --- /dev/null +++ b/packages/api-server/src/bothCLIConfigHandler.ts @@ -0,0 +1,95 @@ +import chalk from 'chalk' + +import { redwoodFastifyWeb, coerceRootPath } from '@redwoodjs/fastify-web' + +import { getWebHost, getWebPort, getAPIHost, getAPIPort } from './cliHelpers' +import createFastifyInstance from './fastify' +import { redwoodFastifyAPI } from './plugins/api' +import type { BothParsedOptions } from './types' + +export async function handler(options: BothParsedOptions) { + const timeStart = Date.now() + console.log(chalk.dim.italic('Starting API and Web Servers...')) + + options.webHost ??= getWebHost() + options.webPort ??= getWebPort() + options.apiHost ??= getAPIHost() + options.apiPort ??= getAPIPort() + + options.apiRootPath = coerceRootPath(options.apiRootPath ?? '/') + + const apiProxyTarget = [ + 'http://', + options.apiHost.includes(':') ? `[${options.apiHost}]` : options.apiHost, + ':', + options.apiPort, + options.apiRootPath, + ].join('') + + const webFastify = createFastifyInstance() + webFastify.register(redwoodFastifyWeb, { + redwood: { + apiProxyTarget, + }, + }) + + const apiFastify = createFastifyInstance() + apiFastify.register(redwoodFastifyAPI, { + redwood: { + apiRootPath: options.apiRootPath, + loadUserConfig: true, + }, + }) + + await webFastify.listen({ + port: options.webPort, + host: options.webHost, + listenTextResolver: getListenTextResolver('Web'), + }) + + webFastify.log.trace( + { custom: { ...webFastify.initialConfig } }, + 'Fastify server configuration' + ) + webFastify.log.trace(`Registered plugins\n${webFastify.printPlugins()}`) + + await apiFastify.listen({ + port: options.apiPort, + host: options.apiHost, + listenTextResolver: getListenTextResolver('API'), + }) + + apiFastify.log.trace( + { custom: { ...apiFastify.initialConfig } }, + 'Fastify server configuration' + ) + apiFastify.log.trace(`Registered plugins\n${apiFastify.printPlugins()}`) + + console.log(chalk.dim.italic('Took ' + (Date.now() - timeStart) + ' ms')) + + const webServer = chalk.green(webFastify.listeningOrigin) + const apiServer = chalk.magenta( + `${apiFastify.listeningOrigin}${options.apiRootPath}` + ) + const graphqlEndpoint = chalk.magenta(`${apiServer}graphql`) + + console.log(`Web server listening at ${webServer}`) + console.log(`API server listening at ${apiServer}`) + console.log(`GraphQL endpoint at ${graphqlEndpoint}`) + + process?.send?.('ready') +} + +function getListenTextResolver(side: string) { + return (address: string) => { + // In the past, in development, we've prioritized showing a friendlier + // host than the listen-on-all-ipv6-addresses '[::]'. Here we replace it + // with 'localhost' only if 1) we're not in production and 2) it's there. + // In production it's important to be transparent. + if (process.env.NODE_ENV !== 'production') { + address = address.replace(/http:\/\/\[::\]/, 'http://localhost') + } + + return `${side} server listening at ${address}` + } +} diff --git a/packages/api-server/src/cliHandlers.ts b/packages/api-server/src/cliHandlers.ts deleted file mode 100644 index 3ef2117e8ecd..000000000000 --- a/packages/api-server/src/cliHandlers.ts +++ /dev/null @@ -1,112 +0,0 @@ -import c from 'ansi-colors' - -import { redwoodFastifyWeb, coerceRootPath } from '@redwoodjs/fastify-web' -import { getConfig } from '@redwoodjs/project-config' - -import createFastifyInstance from './fastify' -import withFunctions from './plugins/withFunctions' -import { startServer as startFastifyServer } from './server' -import type { BothServerArgs, ApiServerArgs } from './types' - -/* - * This file has defines CLI handlers used by the redwood cli, for `rw serve` - * Also used in index.ts for the api server - */ - -const sendProcessReady = () => { - return process.send && process.send('ready') -} - -export const commonOptions = { - port: { default: getConfig().web?.port || 8910, type: 'number', alias: 'p' }, - socket: { type: 'string' }, -} as const - -export const apiCliOptions = { - port: { default: getConfig().api?.port || 8911, type: 'number', alias: 'p' }, - socket: { type: 'string' }, - apiRootPath: { - alias: ['api-root-path', 'rootPath', 'root-path'], - default: '/', - type: 'string', - desc: 'Root path where your api functions are served', - coerce: coerceRootPath, - }, - loadEnvFiles: { - description: - 'Deprecated; env files are always loaded. This flag is a no-op', - type: 'boolean', - hidden: true, - }, -} as const - -export const apiServerHandler = async (options: ApiServerArgs) => { - const { port, socket, apiRootPath } = options - const tsApiServer = Date.now() - console.log(c.dim.italic('Starting API Server...')) - - let fastify = createFastifyInstance() - - // Import Server Functions. - fastify = await withFunctions(fastify, options) - - fastify = await startFastifyServer({ port, socket, fastify }) - - fastify.ready(() => { - console.log(c.dim.italic('Took ' + (Date.now() - tsApiServer) + ' ms')) - - // In the past, in development, we've prioritized showing a friendlier - // host than the listen-on-all-ipv6-addresses '[::]'. Here we replace it - // with 'localhost' only if 1) we're not in production and 2) it's there. - // In production it's important to be transparent. - // - // We have this logic for `apiServerHandler` because this is the only - // handler called by the watch bin (which is called by `yarn rw dev`). - let address = fastify.listeningOrigin - if (process.env.NODE_ENV !== 'production') { - address = address.replace(/http:\/\/\[::\]/, 'http://localhost') - } - - const apiServer = c.magenta(`${address}${apiRootPath}`) - const graphqlEndpoint = c.magenta(`${apiServer}graphql`) - - console.log(`API server listening at ${apiServer}`) - console.log(`GraphQL endpoint at ${graphqlEndpoint}`) - - sendProcessReady() - }) - process.on('exit', () => { - fastify?.close() - }) -} - -export const bothServerHandler = async (options: BothServerArgs) => { - const { port, socket } = options - const tsServer = Date.now() - console.log(c.dim.italic('Starting API and Web Servers...')) - const apiRootPath = coerceRootPath(getConfig().web.apiUrl) - - let fastify = createFastifyInstance() - - await fastify.register(redwoodFastifyWeb) - fastify = await withFunctions(fastify, { ...options, apiRootPath }) - - fastify = await startFastifyServer({ port, socket, fastify }) - - fastify.ready(() => { - console.log(c.dim.italic('Took ' + (Date.now() - tsServer) + ' ms')) - - const webServer = c.green(fastify.listeningOrigin) - const apiServer = c.magenta(`${fastify.listeningOrigin}${apiRootPath}`) - const graphqlEndpoint = c.magenta(`${apiServer}graphql`) - - console.log(`Web server listening at ${webServer}`) - console.log(`API server listening at ${apiServer}`) - console.log(`GraphQL endpoint at ${graphqlEndpoint}`) - - sendProcessReady() - }) -} - -// Temporarily here till we refactor server code -export { createServer } from './createServer' diff --git a/packages/api-server/src/cliHelpers.ts b/packages/api-server/src/cliHelpers.ts new file mode 100644 index 000000000000..6e2c7c7a8e6c --- /dev/null +++ b/packages/api-server/src/cliHelpers.ts @@ -0,0 +1,27 @@ +import { getConfig } from '@redwoodjs/project-config' + +export function getAPIHost() { + let host = process.env.REDWOOD_API_HOST + host ??= getConfig().api.host + host ??= process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::' + return host +} + +export function getAPIPort() { + return process.env.REDWOOD_API_PORT + ? parseInt(process.env.REDWOOD_API_PORT) + : getConfig().api.port +} + +export function getWebHost() { + let host = process.env.REDWOOD_WEB_HOST + host ??= getConfig().web.host + host ??= process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::' + return host +} + +export function getWebPort() { + return process.env.REDWOOD_WEB_PORT + ? parseInt(process.env.REDWOOD_WEB_PORT) + : getConfig().web.port +} diff --git a/packages/api-server/src/createServer.ts b/packages/api-server/src/createServer.ts index 798cb351a751..6f990686ed89 100644 --- a/packages/api-server/src/createServer.ts +++ b/packages/api-server/src/createServer.ts @@ -1,27 +1,19 @@ import fs from 'fs' import path from 'path' -import { parseArgs } from 'util' -import fastifyUrlData from '@fastify/url-data' -import c from 'ansi-colors' +import chalk from 'chalk' import { config } from 'dotenv-defaults' import fg from 'fast-glob' import fastify from 'fastify' -import type { - FastifyListenOptions, - FastifyServerOptions, - FastifyInstance, -} from 'fastify' -import fastifyRawBody from 'fastify-raw-body' +import type { FastifyListenOptions, FastifyInstance } from 'fastify' import type { GlobalContext } from '@redwoodjs/context' import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' import { getConfig, getPaths } from '@redwoodjs/project-config' -import { - loadFunctionsFromDist, - lambdaRequestHandler, -} from './plugins/lambdaLoader' +import { resolveOptions } from './createServerHelpers' +import type { CreateServerOptions } from './createServerHelpers' +import { redwoodFastifyAPI } from './plugins/api' type StartOptions = Omit @@ -47,24 +39,6 @@ if (process.env.RWJS_CWD && !process.env.REDWOOD_ENV_FILES_LOADED) { process.env.REDWOOD_ENV_FILES_LOADED = 'true' } -export interface CreateServerOptions { - /** - * The prefix for all routes. Defaults to `/`. - */ - apiRootPath?: string - - /** - * Logger instance or options. - */ - logger?: FastifyServerOptions['logger'] - - /** - * Options for the fastify server instance. - * Omitting logger here because we move it up. - */ - fastifyServerOptions?: Omit -} - /** * Creates a server for api functions: * @@ -90,7 +64,8 @@ export interface CreateServerOptions { * ``` */ export async function createServer(options: CreateServerOptions = {}) { - const { apiRootPath, fastifyServerOptions, port } = resolveOptions(options) + const { apiRootPath, fastifyServerOptions, port, host } = + resolveOptions(options) // Warn about `api/server.config.js` const serverConfigPath = path.join( @@ -100,7 +75,7 @@ export async function createServer(options: CreateServerOptions = {}) { if (fs.existsSync(serverConfigPath)) { console.warn( - c.yellow( + chalk.yellow( [ '', `Ignoring \`config\` and \`configureServer\` in api/server.config.js.`, @@ -133,7 +108,14 @@ export async function createServer(options: CreateServerOptions = {}) { getAsyncStoreInstance().run(new Map(), done) }) - await server.register(redwoodFastifyFunctions, { redwood: { apiRootPath } }) + await server.register(redwoodFastifyAPI, { + redwood: { + apiRootPath, + fastGlobOptions: { + ignore: ['**/dist/functions/graphql.js'], + }, + }, + }) // If we can find `api/dist/functions/graphql.js`, register the GraphQL plugin const [graphqlFunctionPath] = await fg('dist/functions/graphql.{ts,js}', { @@ -162,7 +144,7 @@ export async function createServer(options: CreateServerOptions = {}) { server.addHook('onListen', (done) => { console.log( - `Server listening at ${c.magenta( + `Server listening at ${chalk.magenta( `${server.listeningOrigin}${apiRootPath}` )}` ) @@ -181,153 +163,9 @@ export async function createServer(options: CreateServerOptions = {}) { return server.listen({ ...options, port, - host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::', + host, }) } return server } - -type ResolvedOptions = Required< - Omit & { - fastifyServerOptions: FastifyServerOptions - port: number - } -> - -export function resolveOptions( - options: CreateServerOptions = {}, - args?: string[] -) { - options.logger ??= DEFAULT_CREATE_SERVER_OPTIONS.logger - - let defaultPort: number | undefined - - if (process.env.REDWOOD_API_PORT === undefined) { - defaultPort = getConfig().api.port - } else { - defaultPort = parseInt(process.env.REDWOOD_API_PORT) - } - - // Set defaults. - const resolvedOptions: ResolvedOptions = { - apiRootPath: - options.apiRootPath ?? DEFAULT_CREATE_SERVER_OPTIONS.apiRootPath, - - fastifyServerOptions: options.fastifyServerOptions ?? { - requestTimeout: - DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout, - logger: options.logger ?? DEFAULT_CREATE_SERVER_OPTIONS.logger, - }, - - port: defaultPort, - } - - // Merge fastifyServerOptions. - resolvedOptions.fastifyServerOptions.requestTimeout ??= - DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout - resolvedOptions.fastifyServerOptions.logger = options.logger - - const { values } = parseArgs({ - options: { - apiRootPath: { - type: 'string', - }, - port: { - type: 'string', - short: 'p', - }, - }, - - // When running Jest, `process.argv` is... - // - // ```js - // [ - // 'path/to/node' - // 'path/to/jest.js' - // 'file/under/test.js' - // ] - // ``` - // - // `parseArgs` strips the first two, leaving the third, which is interpreted as a positional argument. - // Which fails our options. We'd still like to be strict, but can't do it for tests. - strict: process.env.NODE_ENV === 'test' ? false : true, - ...(args && { args }), - }) - - if (values.apiRootPath && typeof values.apiRootPath !== 'string') { - throw new Error('`apiRootPath` must be a string') - } - - if (values.apiRootPath) { - resolvedOptions.apiRootPath = values.apiRootPath - } - - // Format `apiRootPath` - if (resolvedOptions.apiRootPath.charAt(0) !== '/') { - resolvedOptions.apiRootPath = `/${resolvedOptions.apiRootPath}` - } - - if ( - resolvedOptions.apiRootPath.charAt( - resolvedOptions.apiRootPath.length - 1 - ) !== '/' - ) { - resolvedOptions.apiRootPath = `${resolvedOptions.apiRootPath}/` - } - - if (values.port) { - resolvedOptions.port = +values.port - - if (isNaN(resolvedOptions.port)) { - throw new Error('`port` must be an integer') - } - } - - return resolvedOptions -} - -type DefaultCreateServerOptions = Required< - Omit & { - fastifyServerOptions: Pick - } -> - -export const DEFAULT_CREATE_SERVER_OPTIONS: DefaultCreateServerOptions = { - apiRootPath: '/', - logger: { - level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', - }, - fastifyServerOptions: { - requestTimeout: 15_000, - }, -} - -export interface RedwoodFastifyAPIOptions { - redwood: { - apiRootPath: string - } -} - -export async function redwoodFastifyFunctions( - fastify: FastifyInstance, - opts: RedwoodFastifyAPIOptions -) { - fastify.register(fastifyUrlData) - await fastify.register(fastifyRawBody) - - fastify.addContentTypeParser( - ['application/x-www-form-urlencoded', 'multipart/form-data'], - { parseAs: 'string' }, - fastify.defaultTextParser - ) - - fastify.all(`${opts.redwood.apiRootPath}:routeName`, lambdaRequestHandler) - fastify.all(`${opts.redwood.apiRootPath}:routeName/*`, lambdaRequestHandler) - - await loadFunctionsFromDist({ - fastGlobOptions: { - ignore: ['**/dist/functions/graphql.js'], - }, - }) -} diff --git a/packages/api-server/src/createServerHelpers.ts b/packages/api-server/src/createServerHelpers.ts new file mode 100644 index 000000000000..1ec62083534d --- /dev/null +++ b/packages/api-server/src/createServerHelpers.ts @@ -0,0 +1,129 @@ +import { parseArgs } from 'util' + +import type { FastifyServerOptions } from 'fastify' + +import { coerceRootPath } from '@redwoodjs/fastify-web/dist/helpers' + +import { getAPIHost, getAPIPort } from './cliHelpers' + +export interface CreateServerOptions { + /** + * The prefix for all routes. Defaults to `/`. + */ + apiRootPath?: string + + /** + * Logger instance or options. + */ + logger?: FastifyServerOptions['logger'] + + /** + * Options for the fastify server instance. + * Omitting logger here because we move it up. + */ + fastifyServerOptions?: Omit + + /** + * Whether to parse args or not. Defaults to `true`. + */ + parseArgs?: boolean +} + +type DefaultCreateServerOptions = Required< + Omit & { + fastifyServerOptions: Pick + } +> + +export const DEFAULT_CREATE_SERVER_OPTIONS: DefaultCreateServerOptions = { + apiRootPath: '/', + logger: { + level: + process.env.LOG_LEVEL ?? + (process.env.NODE_ENV === 'development' ? 'debug' : 'warn'), + }, + fastifyServerOptions: { + requestTimeout: 15_000, + }, + parseArgs: true, +} + +type ResolvedOptions = Required< + Omit & { + fastifyServerOptions: FastifyServerOptions + port: number + host: string + } +> + +export function resolveOptions( + options: CreateServerOptions = {}, + args?: string[] +) { + options.logger ??= DEFAULT_CREATE_SERVER_OPTIONS.logger + + // Set defaults. + const resolvedOptions: ResolvedOptions = { + apiRootPath: + options.apiRootPath ?? DEFAULT_CREATE_SERVER_OPTIONS.apiRootPath, + + fastifyServerOptions: options.fastifyServerOptions ?? { + requestTimeout: + DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout, + logger: options.logger ?? DEFAULT_CREATE_SERVER_OPTIONS.logger, + }, + + host: getAPIHost(), + port: getAPIPort(), + } + + // Merge fastifyServerOptions. + resolvedOptions.fastifyServerOptions.requestTimeout ??= + DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout + resolvedOptions.fastifyServerOptions.logger = options.logger + + if (options.parseArgs) { + const { values } = parseArgs({ + options: { + host: { + type: 'string', + short: 'p', + }, + port: { + type: 'string', + short: 'p', + }, + apiRootPath: { + type: 'string', + }, + }, + ...(args && { args }), + }) + + if (values.host && typeof values.host !== 'string') { + throw new Error('`host` must be a string') + } + if (values.host) { + resolvedOptions.host = values.host + } + + if (values.port) { + resolvedOptions.port = +values.port + + if (isNaN(resolvedOptions.port)) { + throw new Error('`port` must be an integer') + } + } + + if (values.apiRootPath && typeof values.apiRootPath !== 'string') { + throw new Error('`apiRootPath` must be a string') + } + if (values.apiRootPath) { + resolvedOptions.apiRootPath = values.apiRootPath + } + } + + resolvedOptions.apiRootPath = coerceRootPath(resolvedOptions.apiRootPath) + + return resolvedOptions +} diff --git a/packages/api-server/src/index.ts b/packages/api-server/src/index.ts deleted file mode 100644 index db0df9ac03fb..000000000000 --- a/packages/api-server/src/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env node - -import path from 'path' - -import { config } from 'dotenv-defaults' -import { hideBin } from 'yargs/helpers' -import yargs from 'yargs/yargs' - -import { getPaths } from '@redwoodjs/project-config' -import * as webServerCLIConfig from '@redwoodjs/web-server' - -import { - apiCliOptions, - commonOptions, - apiServerHandler, - bothServerHandler, -} from './cliHandlers' - -export * from './types' - -if (!process.env.REDWOOD_ENV_FILES_LOADED) { - config({ - path: path.join(getPaths().base, '.env'), - defaults: path.join(getPaths().base, '.env.defaults'), - multiline: true, - }) - - process.env.REDWOOD_ENV_FILES_LOADED = 'true' -} - -if (require.main === module) { - yargs(hideBin(process.argv)) - .scriptName('rw-server') - .usage('usage: $0 ') - .strict() - - .command( - '$0', - 'Run both api and web servers', - // @ts-expect-error just passing yargs though - (yargs) => { - yargs.options(commonOptions) - }, - bothServerHandler - ) - .command( - 'api', - 'Start server for serving only the api', - // @ts-expect-error just passing yargs though - (yargs) => { - yargs.options(apiCliOptions) - }, - apiServerHandler - ) - .command( - 'web', - webServerCLIConfig.description, - // @ts-expect-error just passing yargs though - webServerCLIConfig.builder, - webServerCLIConfig.handler - ) - .parse() -} diff --git a/packages/api-server/src/logFormatter/bin.ts b/packages/api-server/src/logFormatter/bin.ts index d583ebff5de3..e3695b27f706 100644 --- a/packages/api-server/src/logFormatter/bin.ts +++ b/packages/api-server/src/logFormatter/bin.ts @@ -1,4 +1,3 @@ -#! /usr/bin/env node import split from 'split2' import { LogFormatter } from './index' diff --git a/packages/api-server/src/plugins/api.ts b/packages/api-server/src/plugins/api.ts new file mode 100644 index 000000000000..cfe1ca0317c0 --- /dev/null +++ b/packages/api-server/src/plugins/api.ts @@ -0,0 +1,62 @@ +import fastifyUrlData from '@fastify/url-data' +import type { Options as FastGlobOptions } from 'fast-glob' +import type { FastifyInstance } from 'fastify' +import fastifyRawBody from 'fastify-raw-body' + +import type { GlobalContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' +import { coerceRootPath } from '@redwoodjs/fastify-web/dist/helpers' + +import { loadFastifyConfig } from '../fastify' + +import { lambdaRequestHandler, loadFunctionsFromDist } from './lambdaLoader' + +export interface RedwoodFastifyAPIOptions { + redwood: { + apiRootPath?: string + fastGlobOptions?: FastGlobOptions + loadUserConfig?: boolean + } +} + +export async function redwoodFastifyAPI( + fastify: FastifyInstance, + opts: RedwoodFastifyAPIOptions +) { + const redwoodOptions = opts.redwood ?? {} + redwoodOptions.apiRootPath ??= '/' + redwoodOptions.apiRootPath = coerceRootPath(redwoodOptions.apiRootPath) + redwoodOptions.fastGlobOptions ??= {} + redwoodOptions.loadUserConfig ??= false + + fastify.register(fastifyUrlData) + // Starting in Fastify v4, we have to await the fastifyRawBody plugin's registration + // to ensure it's ready + await fastify.register(fastifyRawBody) + + fastify.addHook('onRequest', (_req, _reply, done) => { + getAsyncStoreInstance().run(new Map(), done) + }) + + fastify.addContentTypeParser( + ['application/x-www-form-urlencoded', 'multipart/form-data'], + { parseAs: 'string' }, + fastify.defaultTextParser + ) + + if (redwoodOptions.loadUserConfig) { + const { configureFastify } = loadFastifyConfig() + if (configureFastify) { + await configureFastify(fastify, { + side: 'api', + apiRootPath: redwoodOptions.apiRootPath, + }) + } + } + + fastify.all(`${redwoodOptions.apiRootPath}:routeName`, lambdaRequestHandler) + fastify.all(`${redwoodOptions.apiRootPath}:routeName/*`, lambdaRequestHandler) + await loadFunctionsFromDist({ + fastGlobOptions: redwoodOptions.fastGlobOptions, + }) +} diff --git a/packages/api-server/src/plugins/graphql.ts b/packages/api-server/src/plugins/graphql.ts index d2912d86cbdb..bfc0b37e0d09 100644 --- a/packages/api-server/src/plugins/graphql.ts +++ b/packages/api-server/src/plugins/graphql.ts @@ -9,56 +9,53 @@ import type { import fastifyRawBody from 'fastify-raw-body' import type { Plugin } from 'graphql-yoga' +import type { GlobalContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' +import { coerceRootPath } from '@redwoodjs/fastify-web/dist/helpers' import { createGraphQLYoga } from '@redwoodjs/graphql-server' import type { GraphQLYogaOptions } from '@redwoodjs/graphql-server' import { getPaths } from '@redwoodjs/project-config' -/** - * Transform a Fastify Request to an event compatible with the RedwoodGraphQLContext's event - * which is based on the AWS Lambda event - */ import { lambdaEventForFastifyRequest } from '../requestHandlers/awsLambdaFastify' export interface RedwoodFastifyGraphQLOptions { redwood: { - apiRootPath: string + apiRootPath?: string graphql?: GraphQLYogaOptions } } -/** - * Redwood GraphQL Server Fastify plugin based on GraphQL Yoga - * - * @param {FastifyInstance} fastify Encapsulated Fastify Instance - * @param {GraphQLYogaOptions} options GraphQLYogaOptions options used to configure the GraphQL Yoga Server - */ export async function redwoodFastifyGraphQLServer( fastify: FastifyInstance, options: RedwoodFastifyGraphQLOptions ) { - // These two plugins are needed to transform a Fastify Request to a Lambda event - // which is used by the RedwoodGraphQLContext and mimics the behavior of the - // api-server withFunction plugin - if (!fastify.hasPlugin('@fastify/url-data')) { - await fastify.register(fastifyUrlData) - } + const redwoodOptions = options.redwood ?? {} + redwoodOptions.apiRootPath ??= '/' + redwoodOptions.apiRootPath = coerceRootPath(redwoodOptions.apiRootPath) + + fastify.register(fastifyUrlData) + // Starting in Fastify v4, we have to await the fastifyRawBody plugin's registration + // to ensure it's ready await fastify.register(fastifyRawBody) - try { - const method = ['GET', 'POST', 'OPTIONS'] as HTTPMethods[] + const method = ['GET', 'POST', 'OPTIONS'] as HTTPMethods[] + + fastify.addHook('onRequest', (_req, _reply, done) => { + getAsyncStoreInstance().run(new Map(), done) + }) - // Load the graphql options from the graphql function if none are explicitly provided - if (!options.redwood.graphql) { + try { + // Load the graphql options from the user's graphql function if none are explicitly provided + if (!redwoodOptions.graphql) { const [graphqlFunctionPath] = await fg('dist/functions/graphql.{ts,js}', { cwd: getPaths().api.base, absolute: true, }) - const { __rw_graphqlOptions } = await import(graphqlFunctionPath) - options.redwood.graphql = __rw_graphqlOptions as GraphQLYogaOptions + redwoodOptions.graphql = __rw_graphqlOptions as GraphQLYogaOptions } - const graphqlOptions = options.redwood.graphql + const graphqlOptions = redwoodOptions.graphql // Here we can add any plugins that we want to use with GraphQL Yoga Server // that we do not want to add the the GraphQLHandler in the graphql-server @@ -69,7 +66,7 @@ export async function redwoodFastifyGraphQLServer( const { useRedwoodRealtime } = await import('@redwoodjs/realtime') const originalExtraPlugins: Array> = - graphqlOptions.extraPlugins || [] + graphqlOptions.extraPlugins ?? [] originalExtraPlugins.push(useRedwoodRealtime(graphqlOptions.realtime)) graphqlOptions.extraPlugins = originalExtraPlugins @@ -105,11 +102,9 @@ export async function redwoodFastifyGraphQLServer( const routePaths = ['', '/health', '/readiness', '/stream'] for (const routePath of routePaths) { fastify.route({ - url: `${options.redwood.apiRootPath}${formatGraphQLEndpoint( - yoga.graphqlEndpoint - )}${routePath}`, + url: `${redwoodOptions.apiRootPath}${yoga.graphqlEndpoint}${routePath}`, method, - handler: async (req, reply) => await graphQLYogaHandler(req, reply), + handler: (req, reply) => graphQLYogaHandler(req, reply), }) } @@ -128,7 +123,3 @@ export async function redwoodFastifyGraphQLServer( console.log(e) } } - -function formatGraphQLEndpoint(endpoint: string) { - return endpoint.replace(/^\//, '').replace(/\/$/, '') -} diff --git a/packages/api-server/src/plugins/lambdaLoader.ts b/packages/api-server/src/plugins/lambdaLoader.ts index c002ee601e62..03d8e0342d8d 100644 --- a/packages/api-server/src/plugins/lambdaLoader.ts +++ b/packages/api-server/src/plugins/lambdaLoader.ts @@ -1,7 +1,7 @@ import path from 'path' -import c from 'ansi-colors' import type { Handler } from 'aws-lambda' +import chalk from 'chalk' import fg from 'fast-glob' import type { Options as FastGlobOptions } from 'fast-glob' import type { @@ -22,7 +22,7 @@ export const LAMBDA_FUNCTIONS: Lambdas = {} export const setLambdaFunctions = async (foundFunctions: string[]) => { const tsImport = Date.now() - console.log(c.dim.italic('Importing Server Functions... ')) + console.log(chalk.dim.italic('Importing Server Functions... ')) const imports = foundFunctions.map((fnPath) => { return new Promise((resolve) => { @@ -41,8 +41,8 @@ export const setLambdaFunctions = async (foundFunctions: string[]) => { } // TODO: Use terminal link. console.log( - c.magenta('/' + routeName), - c.dim.italic(Date.now() - ts + ' ms') + chalk.magenta('/' + routeName), + chalk.dim.italic(Date.now() - ts + ' ms') ) return resolve(true) }) @@ -50,7 +50,9 @@ export const setLambdaFunctions = async (foundFunctions: string[]) => { Promise.all(imports).then((_results) => { console.log( - c.dim.italic('...Done importing in ' + (Date.now() - tsImport) + ' ms') + chalk.dim.italic( + '...Done importing in ' + (Date.now() - tsImport) + ' ms' + ) ) }) } diff --git a/packages/api-server/src/plugins/withFunctions.ts b/packages/api-server/src/plugins/withFunctions.ts deleted file mode 100644 index dd0a2006bb79..000000000000 --- a/packages/api-server/src/plugins/withFunctions.ts +++ /dev/null @@ -1,44 +0,0 @@ -import fastifyUrlData from '@fastify/url-data' -import type { FastifyInstance } from 'fastify' -import fastifyRawBody from 'fastify-raw-body' - -import { loadFastifyConfig } from '../fastify' -import type { ApiServerArgs } from '../types' - -import { lambdaRequestHandler, loadFunctionsFromDist } from './lambdaLoader' - -const withFunctions = async ( - fastify: FastifyInstance, - options: Omit -) => { - const { apiRootPath } = options - // Add extra fastify plugins - if (!fastify.hasPlugin('@fastify/url-data')) { - await fastify.register(fastifyUrlData) - } - - // Fastify v4 must await the fastifyRawBody plugin - // registration to ensure the plugin is ready - await fastify.register(fastifyRawBody) - - fastify.addContentTypeParser( - ['application/x-www-form-urlencoded', 'multipart/form-data'], - { parseAs: 'string' }, - fastify.defaultTextParser - ) - - const { configureFastify } = loadFastifyConfig() - - if (configureFastify) { - await configureFastify(fastify, { side: 'api', ...options }) - } - - fastify.all(`${apiRootPath}:routeName`, lambdaRequestHandler) - fastify.all(`${apiRootPath}:routeName/*`, lambdaRequestHandler) - - await loadFunctionsFromDist() - - return fastify -} - -export default withFunctions diff --git a/packages/api-server/src/server.ts b/packages/api-server/src/server.ts deleted file mode 100644 index 1d6a3b540572..000000000000 --- a/packages/api-server/src/server.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { FastifyInstance } from 'fastify' - -export interface HttpServerParams { - port: number - socket?: string - fastify: FastifyInstance -} - -export const startServer = async ({ - port = 8911, - socket, - fastify, -}: HttpServerParams) => { - const host = process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::' - const serverPort = socket ? parseInt(socket) : port - - await fastify.listen({ - port: serverPort, - host, - listenTextResolver: (address) => { - // In the past, in development, we've prioritized showing a friendlier - // host than the listen-on-all-ipv6-addresses '[::]'. Here we replace it - // with 'localhost' only if 1) we're not in production and 2) it's there. - // In production it's important to be transparent. - if (process.env.NODE_ENV !== 'production') { - address = address.replace(/http:\/\/\[::\]/, 'http://localhost') - } - - return `Server listening at ${address}` - }, - }) - - fastify.ready(() => { - fastify.log.trace( - { custom: { ...fastify.initialConfig } }, - 'Fastify server configuration' - ) - fastify.log.trace(`Registered plugins \n${fastify.printPlugins()}`) - }) - - return fastify -} diff --git a/packages/api-server/src/types.ts b/packages/api-server/src/types.ts index fc049055a904..55b350ab59a4 100644 --- a/packages/api-server/src/types.ts +++ b/packages/api-server/src/types.ts @@ -1,25 +1,28 @@ import type { FastifyInstance } from 'fastify' -import type { HttpServerParams } from './server' - -export interface WebServerArgs extends Omit { - apiHost?: string -} - -export interface ApiServerArgs extends Omit { - apiRootPath: string // either user supplied or '/' - loadEnvFiles: boolean -} - -export type BothServerArgs = Omit +import type { RedwoodFastifyAPIOptions } from './plugins/api' // Types for using server.config.js export type FastifySideConfigFnOptions = { - side: SupportedSides -} & (WebServerArgs | BothServerArgs | ApiServerArgs) + side: 'api' | 'web' +} -export type SupportedSides = 'api' | 'web' export type FastifySideConfigFn = ( fastify: FastifyInstance, - options?: FastifySideConfigFnOptions + options?: FastifySideConfigFnOptions & + Pick ) => Promise | void + +export type APIParsedOptions = { + port?: number + host?: string + loadEnvFiles?: boolean +} & Omit + +export type BothParsedOptions = { + webPort?: number + webHost?: string + apiPort?: number + apiHost?: string + apiRootPath?: string +} & Omit diff --git a/packages/api-server/src/watch.ts b/packages/api-server/src/watch.ts index aee598579d97..d1d52046b346 100644 --- a/packages/api-server/src/watch.ts +++ b/packages/api-server/src/watch.ts @@ -1,11 +1,9 @@ -#!/usr/bin/env node - import type { ChildProcess } from 'child_process' import { fork } from 'child_process' import fs from 'fs' import path from 'path' -import c from 'ansi-colors' +import chalk from 'chalk' import chokidar from 'chokidar' import dotenv from 'dotenv' import { debounce } from 'lodash' @@ -25,34 +23,32 @@ import { resolveFile, } from '@redwoodjs/project-config' -const argv = yargs(hideBin(process.argv)) - .option('debug-port', { - alias: 'dp', - description: 'Debugging port', - type: 'number', - }) - .option('port', { - alias: 'p', - description: 'Port', - type: 'number', - }) - .help() - .alias('help', 'h') - .parseSync() - const rwjsPaths = getPaths() if (!process.env.REDWOOD_ENV_FILES_LOADED) { dotenv.config({ - path: path.join(getPaths().base, '.env'), + path: path.join(rwjsPaths.base, '.env'), // @ts-expect-error The types for dotenv-defaults are using an outdated version of dotenv - defaults: path.join(getPaths().base, '.env.defaults'), + defaults: path.join(rwjsPaths.base, '.env.defaults'), multiline: true, }) process.env.REDWOOD_ENV_FILES_LOADED = 'true' } +const argv = yargs(hideBin(process.argv)) + .option('debugPort', { + description: 'Port on which to expose API server debugger', + type: 'number', + alias: ['debug-port', 'dp'], + }) + .option('port', { + description: 'The port to listen at', + type: 'number', + alias: 'p', + }) + .parseSync() + let httpServerProcess: ChildProcess const killApiServer = () => { @@ -66,9 +62,11 @@ const validate = async () => { return true } catch (e: any) { killApiServer() - console.log(c.redBright(`[GQL Server Error] - Schema validation failed`)) - console.error(c.red(e?.message)) - console.log(c.redBright('-'.repeat(40))) + console.error( + chalk.redBright(`[GQL Server Error] - Schema validation failed`) + ) + console.error(chalk.red(e?.message)) + console.error(chalk.redBright('-'.repeat(40))) debouncedBuild.cancel() debouncedRebuild.cancel() @@ -85,7 +83,7 @@ const buildAndRestart = async ({ killApiServer() const buildTs = Date.now() - console.log(c.dim.italic('Building...')) + console.log(chalk.dim.italic('Building...')) if (clean) { await cleanApiBuild() @@ -96,7 +94,7 @@ const buildAndRestart = async ({ } else { await buildApi() } - console.log(c.dim.italic('Took ' + (Date.now() - buildTs) + ' ms')) + console.log(chalk.dim.italic('Took ' + (Date.now() - buildTs) + ' ms')) const forkOpts = { execArgv: process.execArgv, @@ -106,11 +104,11 @@ const buildAndRestart = async ({ if (getConfig().experimental.opentelemetry.enabled) { // We expect the OpenTelemetry SDK setup file to be in a specific location const opentelemetrySDKScriptPath = path.join( - getPaths().api.dist, + rwjsPaths.api.dist, 'opentelemetry.js' ) const opentelemetrySDKScriptPathRelative = path.relative( - getPaths().base, + rwjsPaths.base, opentelemetrySDKScriptPath ) console.log( @@ -145,7 +143,7 @@ const buildAndRestart = async ({ ) } else { httpServerProcess = fork( - path.join(__dirname, 'index.js'), + path.join(__dirname, 'bin.js'), ['api', '--port', port.toString()], forkOpts ) @@ -214,9 +212,10 @@ chokidar await validate() }) .on('all', async (eventName, filePath) => { - // On sufficiently large projects (500+ files, or >= 2000 ms build times) on older machines, esbuild writing to the api directory - // makes chokidar emit an `addDir` event. This starts an infinite loop where the api starts building itself as soon as it's finished. - // This could probably be fixed with some sort of build caching. + // On sufficiently large projects (500+ files, or >= 2000 ms build times) on older machines, + // esbuild writing to the api directory makes chokidar emit an `addDir` event. + // This starts an infinite loop where the api starts building itself as soon as it's finished. + // This could probably be fixed with some sort of build caching if (eventName === 'addDir' && filePath === rwjsPaths.api.base) { return } @@ -235,7 +234,7 @@ chokidar } console.log( - c.dim(`[${eventName}] ${filePath.replace(rwjsPaths.api.base, '')}`) + chalk.dim(`[${eventName}] ${filePath.replace(rwjsPaths.api.base, '')}`) ) if (eventName === 'add' || eventName === 'unlink') { diff --git a/packages/cli/package.json b/packages/cli/package.json index e5b1e9f274fb..383b1c3f9043 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -40,7 +40,6 @@ "@prisma/internals": "5.9.0", "@redwoodjs/api-server": "6.0.7", "@redwoodjs/cli-helpers": "6.0.7", - "@redwoodjs/fastify": "6.0.7", "@redwoodjs/fastify-web": "6.0.7", "@redwoodjs/internal": "6.0.7", "@redwoodjs/prerender": "6.0.7", diff --git a/packages/cli/src/commands/__tests__/serve.test.js b/packages/cli/src/commands/__tests__/serve.test.js index 54eff0c54ceb..54f8c24c20d9 100644 --- a/packages/cli/src/commands/__tests__/serve.test.js +++ b/packages/cli/src/commands/__tests__/serve.test.js @@ -1,3 +1,11 @@ +import { vi, describe, afterEach, it, expect } from 'vitest' +import yargs from 'yargs/yargs' + +import * as apiServerCLIConfig from '@redwoodjs/api-server/dist/apiCLIConfig' +import * as bothServerCLIConfig from '@redwoodjs/api-server/dist/bothCLIConfig' + +import { builder } from '../serve' + globalThis.__dirname = __dirname // We mock these to skip the check for web/dist and api/dist @@ -41,18 +49,20 @@ vi.mock('fs-extra', async (importOriginal) => { } }) -vi.mock('../serveApiHandler', async (importOriginal) => { - const originalHandler = await importOriginal() +vi.mock('@redwoodjs/api-server/dist/apiCLIConfig', async (importOriginal) => { + const originalAPICLIConfig = await importOriginal() return { - ...originalHandler, - apiServerHandler: vi.fn(), + description: originalAPICLIConfig.desciption, + builder: originalAPICLIConfig.builder, + handler: vi.fn(), } }) -vi.mock('../serveBothHandler', async (importOriginal) => { - const originalHandler = await importOriginal() +vi.mock('@redwoodjs/api-server/dist/bothCLIConfig', async (importOriginal) => { + const originalBothCLIConfig = await importOriginal() return { - ...originalHandler, - bothServerHandler: vi.fn(), + description: originalBothCLIConfig.desciption, + builder: originalBothCLIConfig.builder, + handler: vi.fn(), } }) vi.mock('execa', () => ({ @@ -62,13 +72,6 @@ vi.mock('execa', () => ({ })), })) -import { vi, describe, afterEach, it, expect } from 'vitest' -import yargs from 'yargs/yargs' - -import { builder } from '../serve' -import { apiServerHandler } from '../serveApiHandler' -import { bothServerHandler } from '../serveBothHandler' - describe('yarn rw serve', () => { afterEach(() => { vi.clearAllMocks() @@ -79,7 +82,7 @@ describe('yarn rw serve', () => { await parser.parse('serve api --port 5555 --apiRootPath funkyFunctions') - expect(apiServerHandler).toHaveBeenCalledWith( + expect(apiServerCLIConfig.handler).toHaveBeenCalledWith( expect.objectContaining({ port: 5555, apiRootPath: expect.stringMatching(/^\/?funkyFunctions\/?$/), @@ -94,7 +97,7 @@ describe('yarn rw serve', () => { 'serve api --port 5555 --rootPath funkyFunctions/nested/' ) - expect(apiServerHandler).toHaveBeenCalledWith( + expect(apiServerCLIConfig.handler).toHaveBeenCalledWith( expect.objectContaining({ port: 5555, rootPath: expect.stringMatching(/^\/?funkyFunctions\/nested\/$/), @@ -107,7 +110,7 @@ describe('yarn rw serve', () => { await parser.parse('serve --port 9898 --socket abc') - expect(bothServerHandler).toHaveBeenCalledWith( + expect(bothServerCLIConfig.handler).toHaveBeenCalledWith( expect.objectContaining({ port: 9898, socket: 'abc', diff --git a/packages/cli/src/commands/deploy/flightcontrol.js b/packages/cli/src/commands/deploy/flightcontrol.js index 1274773bc17a..6ffa41e95fcd 100644 --- a/packages/cli/src/commands/deploy/flightcontrol.js +++ b/packages/cli/src/commands/deploy/flightcontrol.js @@ -3,11 +3,11 @@ import path from 'path' import execa from 'execa' import terminalLink from 'terminal-link' +import { handler as apiServerHandler } from '@redwoodjs/api-server/dist/apiCLIConfigHandler' import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' import { getConfig } from '@redwoodjs/project-config' import { getPaths } from '../../lib' -import { apiServerHandler } from '../serveApiHandler' export const command = 'flightcontrol ' export const alias = 'fc' @@ -65,7 +65,7 @@ export const handler = async ({ side, serve, prisma, dm: dataMigrate }) => { async function runApiCommands() { if (serve) { console.log('\nStarting api...') - await apiServerHandler({ + await apiServerHandler.handler({ port: getConfig().api?.port || 8911, apiRootPath: '/', }) diff --git a/packages/cli/src/commands/deploy/render.js b/packages/cli/src/commands/deploy/render.js index 1faf8a23eeea..01eceb01535b 100644 --- a/packages/cli/src/commands/deploy/render.js +++ b/packages/cli/src/commands/deploy/render.js @@ -3,11 +3,11 @@ import path from 'path' import execa from 'execa' import terminalLink from 'terminal-link' +import { handler as apiServerHandler } from '@redwoodjs/api-server/dist/apiCLIConfigHandler' import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' import { getConfig } from '@redwoodjs/project-config' import { getPaths } from '../../lib' -import { apiServerHandler } from '../serveApiHandler' export const command = 'render ' export const description = 'Build, Migrate, and Serve command for Render deploy' @@ -70,7 +70,7 @@ export const handler = async ({ side, prisma, dm: dataMigrate }) => { execaConfig ) dataMigrate && execa.sync('yarn rw dataMigrate up', execaConfig) - await apiServerHandler({ + await apiServerHandler.handler({ port: getConfig().api?.port || 8911, apiRootPath: '/', }) diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js index 0cf69bffda15..cac8a947753b 100644 --- a/packages/cli/src/commands/serve.js +++ b/packages/cli/src/commands/serve.js @@ -3,8 +3,9 @@ import path from 'path' import fs from 'fs-extra' import terminalLink from 'terminal-link' +import * as apiServerCLIConfig from '@redwoodjs/api-server/dist/apiCLIConfig' +import * as bothServerCLIConfig from '@redwoodjs/api-server/dist/bothCLIConfig' import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' -import { coerceRootPath } from '@redwoodjs/fastify-web/helpers' import * as webServerCLIConfig from '@redwoodjs/web-server' import { getPaths, getConfig } from '../lib' @@ -13,7 +14,8 @@ import c from '../lib/colors' import { webSsrServerHandler } from './serveWebHandler' export const command = 'serve [side]' -export const description = 'Run server for api or web in production' +export const description = + 'Start a server for serving both the api and web sides' function hasServerFile() { const serverFilePath = path.join(getPaths().api.dist, 'server.js') @@ -22,37 +24,38 @@ function hasServerFile() { export const builder = async (yargs) => { yargs - .usage('usage: $0 ') .command({ command: '$0', - description: 'Run both api and web servers', + description: bothServerCLIConfig.description, builder: (yargs) => { - if (!hasServerFile()) { + if (hasServerFile()) { yargs.options({ - port: { - default: getConfig().web?.port || 8910, - type: 'number', - alias: 'p', - }, - socket: { type: 'string' }, - }) - - return - } - - yargs - .options({ webPort: { - default: getConfig().web?.port || 8910, + description: 'The port for the web server to listen on', type: 'number', + alias: ['web-port'], + }, + webHost: { + description: + "The host for the web server to listen on. Note that you most likely want this to be '0.0.0.0' in production", + type: 'string', + alias: ['web-host'], }, - }) - .options({ apiPort: { - default: getConfig().api?.port || 8911, + description: 'The port for the api server to listen on', type: 'number', + alias: ['api-port'], + }, + apiHost: { + description: + "The host for the api server to listen on. Note that you most likely want this to be '0.0.0.0' in production", + type: 'string', + alias: ['api-host'], }, }) + } + + bothServerCLIConfig.builder(yargs) }, handler: async (argv) => { recordTelemetryAttributes({ @@ -77,37 +80,14 @@ export const builder = async (yargs) => { ) await bothSsrRscServerHandler(argv) } else { - // Wanted to use the new web-server package here, but can't because - // of backwards compatibility reasons. With `bothServerHandler` both - // the web side and the api side run on the same server with the same - // port. If we use a separate fe server and api server we can't run - // 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('./serveBothHandler.js') - await bothServerHandler(argv) + await bothServerCLIConfig.handler(argv) } }, }) .command({ command: 'api', - description: 'Start server for serving only the api', - builder: (yargs) => - yargs.options({ - port: { - default: getConfig().api?.port || 8911, - type: 'number', - alias: 'p', - }, - socket: { type: 'string' }, - apiRootPath: { - alias: ['api-root-path', 'rootPath', 'root-path'], - default: '/', - type: 'string', - desc: 'Root path where your api functions are served', - coerce: coerceRootPath, - }, - }), + description: apiServerCLIConfig.description, + builder: apiServerCLIConfig.builder, handler: async (argv) => { recordTelemetryAttributes({ command: 'serve', @@ -122,8 +102,7 @@ export const builder = async (yargs) => { const { apiServerFileHandler } = await import('./serveApiHandler.js') await apiServerFileHandler(argv) } else { - const { apiServerHandler } = await import('./serveApiHandler.js') - await apiServerHandler(argv) + await apiServerCLIConfig.handler(argv) } }, }) diff --git a/packages/cli/src/commands/serveApiHandler.js b/packages/cli/src/commands/serveApiHandler.js index 5d3fe3a90c6b..19cd3e03b37f 100644 --- a/packages/cli/src/commands/serveApiHandler.js +++ b/packages/cli/src/commands/serveApiHandler.js @@ -1,9 +1,5 @@ -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 apiServerFileHandler = async (argv) => { @@ -11,69 +7,15 @@ export const apiServerFileHandler = async (argv) => { 'yarn', [ 'node', - path.join('dist', 'server.js'), + 'server.js', '--port', argv.port, '--apiRootPath', argv.apiRootPath, ], { - cwd: getPaths().api.base, + cwd: getPaths().api.dist, stdio: 'inherit', } ) } - -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' : '::', - } - } - - const address = await 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.dim.italic('Took ' + (Date.now() - tsApiServer) + ' ms')) - - const apiServer = chalk.magenta(`${address}${apiRootPath}`) - const graphqlEndpoint = chalk.magenta(`${apiServer}graphql`) - - console.log(`API server listening at ${apiServer}`) - console.log(`GraphQL endpoint at ${graphqlEndpoint}`) - - sendProcessReady() - }) -} - -function sendProcessReady() { - return process.send && process.send('ready') -} diff --git a/packages/cli/src/commands/serveBothHandler.js b/packages/cli/src/commands/serveBothHandler.js index cfd9e1775a65..bef36cbfb762 100644 --- a/packages/cli/src/commands/serveBothHandler.js +++ b/packages/cli/src/commands/serveBothHandler.js @@ -1,11 +1,15 @@ import path from 'path' -import chalk from 'chalk' import concurrently from 'concurrently' import execa from 'execa' -import { createFastifyInstance, redwoodFastifyAPI } from '@redwoodjs/fastify' -import { redwoodFastifyWeb, coerceRootPath } from '@redwoodjs/fastify-web' +import { handler as apiServerHandler } from '@redwoodjs/api-server/dist/apiCLIConfigHandler' +import { + getAPIHost, + getAPIPort, + getWebHost, + getWebPort, +} from '@redwoodjs/api-server/dist/cliHelpers' import { getConfig, getPaths } from '@redwoodjs/project-config' import { errorTelemetry } from '@redwoodjs/telemetry' @@ -24,8 +28,18 @@ export const bothServerFileHandler = async (argv) => { shell: true, }) } else { - const apiHost = process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::' - const apiProxyTarget = `http://${apiHost}:${argv.apiPort}` + argv.apiPort ??= getAPIPort() + argv.apiHost ??= getAPIHost() + argv.webPort ??= getWebPort() + argv.webHost ??= getWebHost() + + const apiProxyTarget = [ + 'http://', + argv.apiHost.includes(':') ? `[${argv.apiHost}]` : argv.apiHost, + ':', + argv.apiPort, + argv.apiRootPath, + ].join('') const { result } = concurrently( [ @@ -33,13 +47,13 @@ export const bothServerFileHandler = async (argv) => { name: 'api', command: `yarn node ${path.join('dist', 'server.js')} --port ${ argv.apiPort - }`, + } --host ${argv.apiHost}`, cwd: getPaths().api.base, prefixColor: 'cyan', }, { name: 'web', - command: `yarn rw-web-server --port ${argv.webPort} --api-proxy-target ${apiProxyTarget}`, + command: `yarn rw-web-server --port ${argv.webPort} --host ${argv.webHost} --api-proxy-target ${apiProxyTarget}`, cwd: getPaths().base, prefixColor: 'blue', }, @@ -66,8 +80,6 @@ export const bothServerFileHandler = async (argv) => { } export const bothSsrRscServerHandler = async (argv) => { - const { apiServerHandler } = await import('./serveApiHandler.js') - // TODO Allow specifying port, socket and apiRootPath const apiPromise = apiServerHandler({ ...argv, @@ -86,65 +98,6 @@ export const bothSsrRscServerHandler = async (argv) => { 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' : '::', - } - } - - const address = await fastify.listen(listenOptions) - - fastify.ready(() => { - console.log(chalk.dim.italic('Took ' + (Date.now() - tsServer) + ' ms')) - - const webServer = chalk.green(address) - const apiServer = chalk.magenta(`${address}${apiRootPath}`) - const graphqlEndpoint = chalk.magenta(`${apiServer}graphql`) - - console.log(`Web server listening at ${webServer}`) - console.log(`API server listening at ${apiServer}`) - console.log(`GraphQL endpoint at ${graphqlEndpoint}`) - - sendProcessReady() - }) -} - -function sendProcessReady() { - return process.send && process.send('ready') -} - function logSkippingFastifyWebServer() { console.warn('') console.warn('⚠️ Skipping Fastify web server ⚠️') diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 735bbddc3df8..ff8281269ee2 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -193,6 +193,7 @@ async function runYargs() { .demandCommand() .strict() .exitProcess(false) + .alias('h', 'help') // Commands (Built in or pre-plugin support) .command(buildCommand) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index c88edb9572d1..e473397e5900 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -10,7 +10,6 @@ "references": [ { "path": "../api-server" }, { "path": "../cli-helpers" }, - { "path": "../fastify" }, { "path": "../internal" }, { "path": "../prerender" }, { "path": "../project-config" }, diff --git a/packages/codemods/tasks/generateCodemod/generateCodemod.mjs b/packages/codemods/tasks/generateCodemod/generateCodemod.mjs index e2ac106007dd..32d5347ddc4f 100644 --- a/packages/codemods/tasks/generateCodemod/generateCodemod.mjs +++ b/packages/codemods/tasks/generateCodemod/generateCodemod.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs' import url from 'node:url' -import c from 'ansi-colors' +import chalk from 'chalk' import fse from 'fs-extra' // lodash is commonjs import template from 'lodash/template.js' @@ -57,9 +57,9 @@ await generateCodemod(version, name, kind) */ async function generateCodemod(version, name, kind) { console.log( - `Generating ${c.green(kind)} codemod ${c.green(name)} for ${c.green( - version - )}...` + `Generating ${chalk.green(kind)} codemod ${chalk.green( + name + )} for ${chalk.green(version)}...` ) // Make the destination. diff --git a/packages/create-redwood-app/templates/js/api/server.config.js b/packages/create-redwood-app/templates/js/api/server.config.js deleted file mode 100644 index 73dca9225a3e..000000000000 --- a/packages/create-redwood-app/templates/js/api/server.config.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file allows you to configure the Fastify Server settings - * used by the RedwoodJS dev server. - * - * It also applies when running RedwoodJS with `yarn rw serve`. - * - * For the Fastify server options that you can set, see: - * https://www.fastify.io/docs/latest/Reference/Server/#factory - * - * Examples include: logger settings, timeouts, maximum payload limits, and more. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('fastify').FastifyServerOptions} */ -const config = { - requestTimeout: 15_000, - logger: { - // Note: If running locally using `yarn rw serve` you may want to adjust - // the default non-development level to `info` - level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', - }, -} - -/** - * You can also register Fastify plugins and additional routes for the API and Web sides - * in the configureFastify function. - * - * This function has access to the Fastify instance and options, such as the side - * (web, api, or proxy) that is being configured and other settings like the apiRootPath - * of the functions endpoint. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */ -const configureFastify = async (fastify, options) => { - if (options.side === 'api') { - fastify.log.trace({ custom: { options } }, 'Configuring api side') - } - - if (options.side === 'web') { - fastify.log.trace({ custom: { options } }, 'Configuring web side') - } - - return fastify -} - -module.exports = { - config, - configureFastify, -} diff --git a/packages/create-redwood-app/templates/ts/api/server.config.js b/packages/create-redwood-app/templates/ts/api/server.config.js deleted file mode 100644 index 73dca9225a3e..000000000000 --- a/packages/create-redwood-app/templates/ts/api/server.config.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file allows you to configure the Fastify Server settings - * used by the RedwoodJS dev server. - * - * It also applies when running RedwoodJS with `yarn rw serve`. - * - * For the Fastify server options that you can set, see: - * https://www.fastify.io/docs/latest/Reference/Server/#factory - * - * Examples include: logger settings, timeouts, maximum payload limits, and more. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('fastify').FastifyServerOptions} */ -const config = { - requestTimeout: 15_000, - logger: { - // Note: If running locally using `yarn rw serve` you may want to adjust - // the default non-development level to `info` - level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', - }, -} - -/** - * You can also register Fastify plugins and additional routes for the API and Web sides - * in the configureFastify function. - * - * This function has access to the Fastify instance and options, such as the side - * (web, api, or proxy) that is being configured and other settings like the apiRootPath - * of the functions endpoint. - * - * Note: This configuration does not apply in a serverless deploy. - */ - -/** @type {import('@redwoodjs/api-server/dist/types').FastifySideConfigFn} */ -const configureFastify = async (fastify, options) => { - if (options.side === 'api') { - fastify.log.trace({ custom: { options } }, 'Configuring api side') - } - - if (options.side === 'web') { - fastify.log.trace({ custom: { options } }, 'Configuring web side') - } - - return fastify -} - -module.exports = { - config, - configureFastify, -} diff --git a/packages/create-redwood-app/tests/templates.test.ts b/packages/create-redwood-app/tests/templates.test.ts index be70e2c81a01..9743e7c80f2d 100644 --- a/packages/create-redwood-app/tests/templates.test.ts +++ b/packages/create-redwood-app/tests/templates.test.ts @@ -28,7 +28,6 @@ describe('TS template', () => { "/api/db/schema.prisma", "/api/jest.config.js", "/api/package.json", - "/api/server.config.js", "/api/src", "/api/src/directives", "/api/src/directives/requireAuth", @@ -112,7 +111,6 @@ describe('JS template', () => { "/api/jest.config.js", "/api/jsconfig.json", "/api/package.json", - "/api/server.config.js", "/api/src", "/api/src/directives", "/api/src/directives/requireAuth", diff --git a/packages/fastify/README.md b/packages/fastify/README.md deleted file mode 100644 index 884b5f977dbf..000000000000 --- a/packages/fastify/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Fastify - -> **Warning** -> -> This package is experimental. - -Redwood plugin for [Fastify](https://www.fastify.io/). diff --git a/packages/fastify/build.mjs b/packages/fastify/build.mjs deleted file mode 100644 index 16175a6725c0..000000000000 --- a/packages/fastify/build.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { build } from '@redwoodjs/framework-tools' - -await build() diff --git a/packages/fastify/package.json b/packages/fastify/package.json deleted file mode 100644 index 6015c44dd078..000000000000 --- a/packages/fastify/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@redwoodjs/fastify", - "version": "6.0.7", - "repository": { - "type": "git", - "url": "https://github.com/redwoodjs/redwood.git", - "directory": "packages/fastify" - }, - "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "build": "yarn node ./build.mjs && yarn build:types", - "build:pack": "yarn pack -o redwoodjs-fastify.tgz", - "build:types": "tsc --build --verbose", - "prepublishOnly": "NODE_ENV=production yarn build" - }, - "dependencies": { - "@fastify/http-proxy": "9.3.0", - "@fastify/static": "6.12.0", - "@fastify/url-data": "5.4.0", - "@redwoodjs/context": "6.0.7", - "@redwoodjs/project-config": "6.0.7", - "ansi-colors": "4.1.3", - "fast-glob": "3.3.2", - "fastify": "4.25.2", - "fastify-raw-body": "4.3.0", - "lodash": "4.17.21", - "qs": "6.11.2" - }, - "devDependencies": { - "@redwoodjs/framework-tools": "6.0.7", - "@types/aws-lambda": "8.10.126", - "@types/lodash": "4.14.201", - "@types/qs": "6.9.11", - "typescript": "5.3.3" - }, - "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" -} diff --git a/packages/fastify/src/api.ts b/packages/fastify/src/api.ts deleted file mode 100644 index 50e69548ecae..000000000000 --- a/packages/fastify/src/api.ts +++ /dev/null @@ -1,43 +0,0 @@ -import fastifyUrlData from '@fastify/url-data' -import type { FastifyInstance } from 'fastify' -import fastifyRawBody from 'fastify-raw-body' - -import type { GlobalContext } from '@redwoodjs/context' -import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' - -import { loadFastifyConfig } from './config' -import { lambdaRequestHandler, loadFunctionsFromDist } from './lambda' -import type { RedwoodFastifyAPIOptions } from './types' - -export async function redwoodFastifyAPI( - fastify: FastifyInstance, - opts: RedwoodFastifyAPIOptions -) { - if (!fastify.hasPlugin('@fastify/url-data')) { - await fastify.register(fastifyUrlData) - } - await fastify.register(fastifyRawBody) - - // TODO: This should be refactored to only be defined once and it might not live here - // Ensure that each request has a unique global context - fastify.addHook('onRequest', (_req, _reply, done) => { - getAsyncStoreInstance().run(new Map(), done) - }) - - fastify.addContentTypeParser( - ['application/x-www-form-urlencoded', 'multipart/form-data'], - { parseAs: 'string' }, - fastify.defaultTextParser - ) - - // NOTE: Deprecate this when we move to a `server.ts` file. - const { configureFastify } = loadFastifyConfig() - if (configureFastify) { - await configureFastify(fastify, { side: 'api', ...opts }) - } - - const apiRootPath = opts.redwood?.apiRootPath || '/' - fastify.all(`${apiRootPath}:routeName`, lambdaRequestHandler) - fastify.all(`${apiRootPath}:routeName/*`, lambdaRequestHandler) - await loadFunctionsFromDist() -} diff --git a/packages/fastify/src/config.ts b/packages/fastify/src/config.ts deleted file mode 100644 index f9c6e59796c8..000000000000 --- a/packages/fastify/src/config.ts +++ /dev/null @@ -1,77 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' - -import type { FastifyInstance, FastifyServerOptions } from 'fastify' - -import { getPaths, getConfig } from '@redwoodjs/project-config' - -import type { FastifySideConfigFn, FastifySideConfigFnOptions } from './types' - -export const DEFAULT_REDWOOD_FASTIFY_CONFIG: FastifyServerOptions = { - requestTimeout: 15_000, - logger: { - // Note: If running locally using `yarn rw serve` you may want to adust - // the default non-development level to `info` - level: - process.env.LOG_LEVEL ?? process.env.NODE_ENV === 'development' - ? 'debug' - : 'warn', - }, -} - -let isServerConfigLoaded = false - -let serverConfigFile: { - config: FastifyServerOptions - configureFastify: FastifySideConfigFn -} = { - config: DEFAULT_REDWOOD_FASTIFY_CONFIG, - configureFastify: async (fastify, options) => { - fastify.log.trace( - options, - `In configureFastify hook for side: ${options?.side}` - ) - return fastify - }, -} - -export function loadFastifyConfig() { - const serverTsFileExists = fs.existsSync( - path.join(getPaths().api.src, 'server.ts') - ) - const serverJsFileExists = - !serverTsFileExists && - fs.existsSync(path.join(getPaths().api.src, 'server.js')) - - if (serverTsFileExists || serverJsFileExists) { - const ext = serverTsFileExists ? 'ts' : 'js' - console.log(`Ignoring Fastify config inside 'api/src/server.config.${ext}`) - - return { - config: {}, - configureFastify: async ( - fastify: FastifyInstance, - _options: FastifySideConfigFnOptions - ) => fastify, - } - } - - // TODO: Use `require.resolve` to find the config file. Do we need to babel first? - const serverConfigPath = path.join( - getPaths().base, - getConfig().api.serverConfig - ) - - // If a server.config.js is not found, use the default options. - if (!fs.existsSync(serverConfigPath)) { - return serverConfigFile - } - - if (!isServerConfigLoaded) { - console.log(`Loading server config from ${serverConfigPath}`) - serverConfigFile = { ...require(serverConfigPath) } - isServerConfigLoaded = true - } - - return serverConfigFile -} diff --git a/packages/fastify/src/index.ts b/packages/fastify/src/index.ts deleted file mode 100644 index 3df9716916ad..000000000000 --- a/packages/fastify/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { FastifyServerOptions } from 'fastify' -import Fastify from 'fastify' - -import { loadFastifyConfig, DEFAULT_REDWOOD_FASTIFY_CONFIG } from './config.js' - -// NOTE: Needed for backwards compatibility in the CLI. -export function createFastifyInstance(options?: FastifyServerOptions) { - const { config } = loadFastifyConfig() - return Fastify(options || config || DEFAULT_REDWOOD_FASTIFY_CONFIG) -} - -export { redwoodFastifyAPI } from './api.js' - -export type * from './types.js' - -export { DEFAULT_REDWOOD_FASTIFY_CONFIG } from './config.js' diff --git a/packages/fastify/src/lambda/index.ts b/packages/fastify/src/lambda/index.ts deleted file mode 100644 index 1a02b6448ea2..000000000000 --- a/packages/fastify/src/lambda/index.ts +++ /dev/null @@ -1,266 +0,0 @@ -import path from 'node:path' - -import c from 'ansi-colors' -import type { - APIGatewayProxyResult, - APIGatewayProxyEvent, - Handler, -} from 'aws-lambda' -import fg from 'fast-glob' -import type { - FastifyReply, - FastifyRequest, - RequestGenericInterface, -} from 'fastify' -import { escape } from 'lodash' -import qs from 'qs' - -import { getPaths } from '@redwoodjs/project-config' - -// NOTE: Copied from @redwoodjs/internal/dist/files to avoid depending on @redwoodjs/internal. -// import { findApiDistFunctions } from '@redwoodjs/internal/dist/files' -function findApiDistFunctions(cwd: string = getPaths().api.base) { - return fg.sync('dist/functions/**/*.{ts,js}', { - cwd, - deep: 2, // We don't support deeply nested api functions, to maximise compatibility with deployment providers - absolute: true, - }) -} - -export type Lambdas = Record -export const LAMBDA_FUNCTIONS: Lambdas = {} - -/** - * Imports the API functions and add them to the LAMBDA_FUNCTIONS object. - */ -export const setLambdaFunctions = async (foundFunctions: string[]) => { - const tsImport = Date.now() - console.log(c.italic(c.dim('Importing Server Functions... '))) - const imports = foundFunctions.map((fnPath) => { - return new Promise((resolve) => { - const ts = Date.now() - const routeName = path.basename(fnPath).replace('.js', '') - - const { handler } = require(fnPath) - LAMBDA_FUNCTIONS[routeName] = handler - if (!handler) { - console.warn( - routeName, - 'at', - fnPath, - 'does not have a function called handler defined.' - ) - } - console.log( - c.magenta('/' + routeName), - c.italic(c.dim(Date.now() - ts + ' ms')) - ) - return resolve(true) - }) - }) - - Promise.all(imports).then((_results) => { - console.log( - c.italic(c.dim('...Done importing in ' + (Date.now() - tsImport) + ' ms')) - ) - }) -} - -// TODO: Use v8 caching to load these crazy fast. -export const loadFunctionsFromDist = async () => { - const serverFunctions = findApiDistFunctions() - // Place `GraphQL` serverless function at the start. - const i = serverFunctions.findIndex((x) => x.indexOf('graphql') !== -1) - if (i >= 0) { - const graphQLFn = serverFunctions.splice(i, 1)[0] - serverFunctions.unshift(graphQLFn) - } - await setLambdaFunctions(serverFunctions) -} - -interface LambdaHandlerRequest extends RequestGenericInterface { - Params: { - routeName: string - } -} - -/** - * This converts a Fastify request to a lambdaEvent, and passes it to the the appropriate handler for the routeName. - * At this point, the LAMBDA_FUNCTIONS lookup has been populated. - **/ -export async function lambdaRequestHandler( - req: FastifyRequest, - reply: FastifyReply -) { - const { routeName } = req.params - - if (!LAMBDA_FUNCTIONS[routeName]) { - const errorMessage = `Function "${routeName}" was not found.` - req.log.error(errorMessage) - reply.status(404) - - if (process.env.NODE_ENV === 'development') { - const devError = { - error: errorMessage, - availableFunctions: Object.keys(LAMBDA_FUNCTIONS), - } - reply.send(devError) - } else { - reply.send(escape(errorMessage)) - } - - return - } - return requestHandler(req, reply, LAMBDA_FUNCTIONS[routeName]) -} - -export function lambdaEventForFastifyRequest( - request: FastifyRequest -): APIGatewayProxyEvent { - return { - httpMethod: request.method, - headers: request.headers, - path: request.urlData('path'), - queryStringParameters: qs.parse(request.url.split(/\?(.+)/)[1]), - requestContext: { - requestId: request.id, - identity: { - sourceIp: request.ip, - }, - }, - ...parseBody(request.rawBody || ''), // adds `body` and `isBase64Encoded` - } as APIGatewayProxyEvent -} - -function fastifyResponseForLambdaResult( - reply: FastifyReply, - lambdaResult: APIGatewayProxyResult -) { - const { - statusCode = 200, - headers, - body = '', - multiValueHeaders, - } = lambdaResult - const mergedHeaders = mergeMultiValueHeaders(headers, multiValueHeaders) - Object.entries(mergedHeaders).forEach(([name, values]) => - values.forEach((value) => reply.header(name, value)) - ) - reply.status(statusCode) - - if (lambdaResult.isBase64Encoded) { - // Correctly handle base 64 encoded binary data. See - // https://aws.amazon.com/blogs/compute/handling-binary-data-using-amazon-api-gateway-http-apis - return reply.send(Buffer.from(body, 'base64')) - } else { - return reply.send(body) - } -} - -const fastifyResponseForLambdaError = ( - req: FastifyRequest, - reply: FastifyReply, - error: Error -) => { - req.log.error(error) - reply.status(500).send() -} - -export async function requestHandler( - req: FastifyRequest, - reply: FastifyReply, - handler: Handler -) { - // We take the fastify request object and convert it into a lambda function event. - const event = lambdaEventForFastifyRequest(req) - - const handlerCallback = - (reply: FastifyReply) => - (error: Error, lambdaResult: APIGatewayProxyResult) => { - if (error) { - fastifyResponseForLambdaError(req, reply, error) - return - } - - fastifyResponseForLambdaResult(reply, lambdaResult) - } - - // Execute the lambda function. - // https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html - const handlerPromise = handler( - event, - // @ts-expect-error - Add support for context: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0bb210867d16170c4a08d9ce5d132817651a0f80/types/aws-lambda/index.d.ts#L443-L467 - {}, - handlerCallback(reply) - ) - - // In this case the handlerCallback should not be called. - if (handlerPromise && typeof handlerPromise.then === 'function') { - try { - const lambdaResponse = await handlerPromise - - return fastifyResponseForLambdaResult(reply, lambdaResponse) - } catch (error: any) { - return fastifyResponseForLambdaError(req, reply, error) - } - } -} - -type ParseBodyResult = { - body: string - isBase64Encoded: boolean -} - -type FastifyHeaderValue = string | number | boolean - -type FastifyMergedHeaders = { [name: string]: FastifyHeaderValue[] } - -type FastifyRequestHeader = { [header: string]: FastifyHeaderValue } - -type FastifyLambdaHeaders = FastifyRequestHeader | undefined - -type FastifyLambdaMultiValueHeaders = FastifyMergedHeaders | undefined - -export function parseBody(rawBody: string | Buffer): ParseBodyResult { - if (typeof rawBody === 'string') { - return { body: rawBody, isBase64Encoded: false } - } - if (rawBody instanceof Buffer) { - return { body: rawBody.toString('base64'), isBase64Encoded: true } - } - return { body: '', isBase64Encoded: false } -} - -/** - * `headers` and `multiValueHeaders` are merged into a single object where the - * key is the header name in lower-case and the value is a list of values for - * that header. Most multi-values are merged into a single value separated by a - * semi-colon. The only exception is set-cookie. set-cookie headers should not - * be merged, they should be set individually by multiple calls to - * reply.header(). See - * https://www.fastify.io/docs/latest/Reference/Reply/#set-cookie - */ -export function mergeMultiValueHeaders( - headers: FastifyLambdaHeaders, - multiValueHeaders: FastifyLambdaMultiValueHeaders -) { - const mergedHeaders = Object.entries( - headers || {} - ).reduce((acc, [name, value]) => { - acc[name.toLowerCase()] = [value] - - return acc - }, {}) - - Object.entries(multiValueHeaders || {}).forEach(([headerName, values]) => { - const name = headerName.toLowerCase() - - if (name.toLowerCase() === 'set-cookie') { - mergedHeaders['set-cookie'] = values - } else { - mergedHeaders[name] = [values.join('; ')] - } - }) - - return mergedHeaders -} diff --git a/packages/fastify/src/types.ts b/packages/fastify/src/types.ts deleted file mode 100644 index 32a56536e83a..000000000000 --- a/packages/fastify/src/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { FastifyInstance } from 'fastify' - -export interface RedwoodFastifyAPIOptions { - redwood?: { - apiRootPath?: string - } -} - -// Types for using server.config.js -export type FastifySideConfigFnOptions = { - side: SupportedSides -} & RedwoodFastifyAPIOptions - -export type SupportedSides = 'api' | 'web' -export type FastifySideConfigFn = ( - fastify: FastifyInstance, - options?: FastifySideConfigFnOptions -) => Promise | void diff --git a/packages/fastify/tsconfig.json b/packages/fastify/tsconfig.json deleted file mode 100644 index e4431bdae5ae..000000000000 --- a/packages/fastify/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.compilerOption.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist" - }, - "include": ["src"], - "references": [ - { "path": "../project-config" }] -} diff --git a/packages/internal/src/generate/watch.ts b/packages/internal/src/generate/watch.ts index 7a564046825a..b98f85ac60d9 100644 --- a/packages/internal/src/generate/watch.ts +++ b/packages/internal/src/generate/watch.ts @@ -3,7 +3,7 @@ import fs from 'fs' import path from 'path' -import c from 'ansi-colors' +import chalk from 'chalk' import chokidar from 'chokidar' import { getPaths } from '@redwoodjs/project-config' @@ -82,7 +82,9 @@ watcher } }) .on('all', async (eventName, p) => { - cliLogger.trace(`File system change: ${c.magenta(eventName)} ${c.dim(p)}`) + cliLogger.trace( + `File system change: ${chalk.magenta(eventName)} ${chalk.dim(p)}` + ) if (!['add', 'change', 'unlink'].includes(eventName)) { return } @@ -95,8 +97,8 @@ watcher cliLogger.debug( action[eventTigger], type + ':', - c.dim(p), - c.italic(c.dim(Date.now() - start + ' ms')) + chalk.dim(p), + chalk.dim.italic(Date.now() - start + ' ms') ) if (absPath.indexOf('Cell') !== -1 && isCellFile(absPath)) { diff --git a/packages/project-config/src/__tests__/config.test.ts b/packages/project-config/src/__tests__/config.test.ts index c2e458178f20..c075ac437191 100644 --- a/packages/project-config/src/__tests__/config.test.ts +++ b/packages/project-config/src/__tests__/config.test.ts @@ -33,7 +33,6 @@ describe('getConfig', () => { { "api": { "debugPort": 18911, - "host": "localhost", "path": "./api", "port": 8911, "schemaPath": "./api/db/schema.prisma", @@ -102,7 +101,6 @@ describe('getConfig', () => { "apiUrl": "/.redwood/functions", "bundler": "vite", "fastRefresh": true, - "host": "localhost", "includeEnvironmentVariables": [], "path": "./web", "port": 8910, diff --git a/packages/project-config/src/config.ts b/packages/project-config/src/config.ts index 4c226ac3ccde..34f326d625be 100644 --- a/packages/project-config/src/config.ts +++ b/packages/project-config/src/config.ts @@ -21,7 +21,7 @@ export enum BundlerEnum { export interface NodeTargetConfig { title: string name?: string - host: string + host?: string port: number path: string target: TargetEnum.NODE @@ -33,7 +33,7 @@ export interface NodeTargetConfig { interface BrowserTargetConfig { title: string name?: string - host: string + host?: string port: number path: string target: TargetEnum.BROWSER @@ -132,7 +132,6 @@ export interface CLIPlugin { const DEFAULT_CONFIG: Config = { web: { title: 'Redwood App', - host: 'localhost', port: 8910, path: './web', target: TargetEnum.BROWSER, @@ -145,7 +144,6 @@ const DEFAULT_CONFIG: Config = { }, api: { title: 'Redwood App', - host: 'localhost', port: 8911, path: './api', target: TargetEnum.NODE, diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index e636a7166df1..4de0a64da24e 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -120,6 +120,17 @@ export default function redwoodPluginVite(): PluginOption[] { // ---------- End Bundle injection ---------- config: (options: UserConfig, env: ConfigEnv): UserConfig => { + let apiHost = process.env.REDWOOD_API_HOST + apiHost ??= rwConfig.api.host + apiHost ??= process.env.NODE_ENV === 'production' ? '0.0.0.0' : '[::]' + + let apiPort + if (process.env.REDWOOD_API_PORT) { + apiPort = parseInt(process.env.REDWOOD_API_PORT) + } else { + apiPort = rwConfig.api.port + } + return { root: rwPaths.web.src, // Disabling for now, let babel handle this for consistency @@ -194,7 +205,7 @@ export default function redwoodPluginVite(): PluginOption[] { host: true, // Listen to all hosts proxy: { [rwConfig.web.apiUrl]: { - target: `http://${rwConfig.api.host}:${rwConfig.api.port}`, + target: `http://${apiHost}:${apiPort}`, changeOrigin: false, // Remove the `.redwood/functions` part, but leave the `/graphql` rewrite: (path) => path.replace(rwConfig.web.apiUrl, ''), diff --git a/packages/vite/src/streaming/registerGlobals.ts b/packages/vite/src/streaming/registerGlobals.ts index 75d034636579..d23f4c7a65a5 100644 --- a/packages/vite/src/streaming/registerGlobals.ts +++ b/packages/vite/src/streaming/registerGlobals.ts @@ -33,10 +33,21 @@ export const registerFwGlobals = () => { if (/^[a-zA-Z][a-zA-Z\d+\-.]*?:/.test(apiPath)) { return apiPath } else { + let webHost = process.env.REDWOOD_WEB_HOST + webHost ??= rwConfig.web.host + webHost ??= process.env.NODE_ENV === 'production' ? '0.0.0.0' : '[::]' + + let webPort + if (process.env.REDWOOD_WEB_PORT) { + webPort = parseInt(process.env.REDWOOD_WEB_PORT) + } else { + webPort = rwConfig.web.port + } + // NOTE: rwConfig.web.host defaults to "localhost", which is // When running in production, the api server does not listen on localhost const proxiedApiUrl = swapLocalhostFor127( - 'http://' + rwConfig.web.host + ':' + rwConfig.web.port + apiPath + 'http://' + webHost + ':' + webPort + apiPath ) if ( diff --git a/packages/web-server/README.md b/packages/web-server/README.md index 228b3125ef49..f14acb674e8e 100644 --- a/packages/web-server/README.md +++ b/packages/web-server/README.md @@ -1,31 +1,6 @@ # Redwood's server for the Web side -## About +This package contains code for Redwood's web server. -This package contains code for Redwood's Fastify Web side server: -- Used when running `yarn rw serve web` -- Used directly when doing Docker-based deploys - -## package.json Server Binaries - -Used to run the Redwood Fastify server for the Web side programmatically - -From package.json -``` -"bin": { - "rw-web-server": "./dist/server.js" -}, -``` - -### `rw-web-server` -Intended for dev and Docker-based deploys. - -Not optimized for production use at scale on its own. Recommended to use CDN or -Nginx as performant alternatives. Or, at least along with a tool like PM2 - -- Runs web on redwood.toml web.port (default 8910) -- GraphQL endpoint is set to redwood.toml web.apiUrl/graphql -- Command Options: - - port (default 8910) - - socket (optional, overrides port if specified) - - apiHost (should point to your api-side server) +This package isn't optimized for production use at scale on it's own. +It's recommended to use a CDN or a web server like Nginx as performant alternatives. diff --git a/packages/web-server/package.json b/packages/web-server/package.json index 108ef7cfdffb..8838fe7a07c4 100644 --- a/packages/web-server/package.json +++ b/packages/web-server/package.json @@ -8,13 +8,7 @@ "directory": "packages/web-server" }, "license": "MIT", - "exports": { - ".": { - "types": "./dist/cliConfig.d.ts", - "default": "./dist/cliConfig.js" - }, - "./package.json": "./package.json" - }, + "main": "./dist/cliConfig.js", "types": "./dist/cliConfig.d.ts", "bin": { "rw-web-server": "./dist/bin.js" diff --git a/packages/web-server/src/bin.ts b/packages/web-server/src/bin.ts index 51d64b224757..36759468c8c4 100644 --- a/packages/web-server/src/bin.ts +++ b/packages/web-server/src/bin.ts @@ -1,17 +1,34 @@ +import path from 'path' + +import { config } from 'dotenv-defaults' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' +import { getPaths } from '@redwoodjs/project-config' + import { bin } from '../package.json' import { description, builder } from './cliConfig' import { handler } from './cliConfigHandler' +if (!process.env.REDWOOD_ENV_FILES_LOADED) { + config({ + path: path.join(getPaths().base, '.env'), + defaults: path.join(getPaths().base, '.env.defaults'), + multiline: true, + }) + + process.env.REDWOOD_ENV_FILES_LOADED = 'true' +} + process.env.NODE_ENV ??= 'production' const [scriptName] = Object.keys(bin) yargs(hideBin(process.argv)) .scriptName(scriptName) + .alias('h', 'help') + .alias('v', 'version') .strict() .example( 'yarn $0 --api-url=/api --api-proxy-target=https://api.redwood.horse', diff --git a/packages/web-server/src/cliConfig.ts b/packages/web-server/src/cliConfig.ts index 48a32b041ca0..a1a47b7756ce 100644 --- a/packages/web-server/src/cliConfig.ts +++ b/packages/web-server/src/cliConfig.ts @@ -1,10 +1,8 @@ import type { Argv } from 'yargs' -import { getConfig } from '@redwoodjs/project-config' - import type { ParsedOptions } from './types' -export const description = 'Start a server for serving only the web side' +export const description = 'Start a server for serving the web side' export function builder(yargs: Argv) { yargs.options({ @@ -12,34 +10,22 @@ export function builder(yargs: Argv) { description: 'The port to listen on', type: 'number', alias: 'p', - default: getConfig().web.port, }, host: { description: - "The host to listen on. Defaults to '0.0.0.0' in production, '::' in development", + "The host to listen on. Note that you most likely want this to be '0.0.0.0' in production", type: 'string', }, - - apiUrl: { - description: - 'Relative URL for proxying requests from or a fully-qualified URL to the API server', - type: 'string', - alias: 'api-url', - default: getConfig().web.apiUrl, - }, apiProxyTarget: { description: - 'Forward requests from the apiUrl to this target. apiUrl must be a relative URL', + 'Forward requests from the apiUrl (in the redwood.toml) to this target. apiUrl must be a relative URL', type: 'string', alias: 'api-proxy-target', }, // Deprecated alias of `apiProxyTarget` apiHost: { - description: - '[Deprecated; use apiProxyTarget] Forward requests from the apiUrl to this target. apiUrl must be a relative URL', - type: 'string', + hidden: true, alias: 'api-host', - deprecated: true, }, }) } diff --git a/packages/web-server/src/cliConfigHandler.ts b/packages/web-server/src/cliConfigHandler.ts index b6138b785f3c..dc4d04e3c5bd 100644 --- a/packages/web-server/src/cliConfigHandler.ts +++ b/packages/web-server/src/cliConfigHandler.ts @@ -1,23 +1,7 @@ -import path from 'path' - -import { config } from 'dotenv-defaults' - -import { getPaths } from '@redwoodjs/project-config' - import type { ParsedOptions } from './types' import { serveWeb } from './webServer' export async function handler(options: ParsedOptions) { - if (!process.env.REDWOOD_ENV_FILES_LOADED) { - config({ - path: path.join(getPaths().base, '.env'), - defaults: path.join(getPaths().base, '.env.defaults'), - multiline: true, - }) - - process.env.REDWOOD_ENV_FILES_LOADED = 'true' - } - try { // Change this to a dynamic import when we add other handlers await serveWeb(options) diff --git a/packages/web-server/src/webServer.ts b/packages/web-server/src/webServer.ts index 7698fcd13a0c..57bdf8559709 100644 --- a/packages/web-server/src/webServer.ts +++ b/packages/web-server/src/webServer.ts @@ -5,7 +5,7 @@ import Fastify from 'fastify' import fs from 'fs-extra' import { redwoodFastifyWeb } from '@redwoodjs/fastify-web' -import { getPaths } from '@redwoodjs/project-config' +import { getConfig, getPaths } from '@redwoodjs/project-config' import type { ParsedOptions } from './types' @@ -18,14 +18,22 @@ export async function serveWeb(options: ParsedOptions = {}) { ) if (!distIndexExists) { throw new Error( - 'no built files to serve; run `yarn rw build web` before serving web' + 'no built files to serve; run `yarn rw build web` before serving the web side' ) } + if (process.env.REDWOOD_WEB_PORT) { + options.port ??= parseInt(process.env.REDWOOD_WEB_PORT) + } + options.port ??= getConfig().web.port + + options.host ??= process.env.REDWOOD_WEB_HOST + options.host ??= getConfig().web.host options.host ??= process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::' + if (process.env.NODE_ENV === 'production' && options.host !== '0.0.0.0') { console.warn( - `Warning: host '${options.host}' may need to be '0.0.0.0' in production` + `Warning: host '${options.host}' may need to be '0.0.0.0' in production for containerized deployments` ) } @@ -33,21 +41,18 @@ export async function serveWeb(options: ParsedOptions = {}) { requestTimeout: 15_000, logger: { level: - process.env.LOG_LEVEL ?? process.env.NODE_ENV === 'development' - ? 'debug' - : 'warn', + process.env.LOG_LEVEL ?? + (process.env.NODE_ENV === 'development' ? 'debug' : 'warn'), }, }) - await fastify.register(redwoodFastifyWeb, { - redwood: options, - }) + fastify.register(redwoodFastifyWeb, { redwood: options }) const address = await fastify.listen({ port: options.port, host: options.host, }) - console.log(chalk.italic.dim('Took ' + (Date.now() - start) + ' ms')) + console.log(chalk.dim.italic('Took ' + (Date.now() - start) + ' ms')) console.log(`Web server listening at ${chalk.green(address)}`) } diff --git a/tasks/server-tests/__snapshots__/bothServer.test.mts.snap b/tasks/server-tests/__snapshots__/bothServer.test.mts.snap new file mode 100644 index 000000000000..1c432606f99f --- /dev/null +++ b/tasks/server-tests/__snapshots__/bothServer.test.mts.snap @@ -0,0 +1,139 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`serve both (/Users/dom/projects/redwood/redwood/packages/api-server/dist/bin.js) > errors out on unknown args 1`] = ` +"rw-server + +Start a server for serving the api and web sides + +Commands: + rw-server Start a server for serving the api and web sides [default] + rw-server api Start a server for serving the api side + rw-server web Start a server for serving the web side + +Options: + --webPort, --web-port The port for the web server to + listen on [number] + --webHost, --web-host The host for the web server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiPort, --api-port The port for the api server to + listen on [number] + --apiHost, --api-host The host for the api server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + -h, --help Show help [boolean] + -v, --version Show version number [boolean] + +Unknown arguments: foo, bar, baz +" +`; + +exports[`serve both (/Users/dom/projects/redwood/redwood/packages/api-server/dist/bin.js) > has help configured 1`] = ` +"rw-server + +Start a server for serving the api and web sides + +Commands: + rw-server Start a server for serving the api and web sides [default] + rw-server api Start a server for serving the api side + rw-server web Start a server for serving the web side + +Options: + --webPort, --web-port The port for the web server to + listen on [number] + --webHost, --web-host The host for the web server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiPort, --api-port The port for the api server to + listen on [number] + --apiHost, --api-host The host for the api server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + -h, --help Show help [boolean] + -v, --version Show version number [boolean] +" +`; + +exports[`serve both ([ '/Users/dom/projects/redwood/redwood/packages/cli/dist/index.js', 'serve' ]) > errors out on unknown args 1`] = ` +"rw serve [side] + +Start a server for serving both the api and web sides + +Commands: + rw serve Start a server for serving the api and web sides [default] + rw serve api Start a server for serving the api side + rw serve web Start a server for serving the web side + +Options: + --version Show version number [boolean] + --cwd Working directory to use (where + \`redwood.toml\` is located) + --telemetry Whether to send anonymous usage + telemetry to RedwoodJS [boolean] + --webPort, --web-port The port for the web server to + listen on [number] + --webHost, --web-host The host for the web server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiPort, --api-port The port for the api server to + listen on [number] + --apiHost, --api-host The host for the api server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + -h, --help Show help [boolean] + +Also see the Redwood CLI Reference +(​https://redwoodjs.com/docs/cli-commands#serve​) + +Unknown arguments: foo, bar, baz +" +`; + +exports[`serve both ([ '/Users/dom/projects/redwood/redwood/packages/cli/dist/index.js', 'serve' ]) > has help configured 1`] = ` +"rw serve [side] + +Start a server for serving both the api and web sides + +Commands: + rw serve Start a server for serving the api and web sides [default] + rw serve api Start a server for serving the api side + rw serve web Start a server for serving the web side + +Options: + --version Show version number [boolean] + --cwd Working directory to use (where + \`redwood.toml\` is located) + --telemetry Whether to send anonymous usage + telemetry to RedwoodJS [boolean] + --webPort, --web-port The port for the web server to + listen on [number] + --webHost, --web-host The host for the web server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiPort, --api-port The port for the api server to + listen on [number] + --apiHost, --api-host The host for the api server to + listen on. Note that you most likely + want this to be '0.0.0.0' in + production [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + -h, --help Show help [boolean] + +Also see the Redwood CLI Reference +(​https://redwoodjs.com/docs/cli-commands#serve​) +" +`; diff --git a/tasks/server-tests/__snapshots__/server.test.mts.snap b/tasks/server-tests/__snapshots__/server.test.mts.snap deleted file mode 100644 index c439d78914b3..000000000000 --- a/tasks/server-tests/__snapshots__/server.test.mts.snap +++ /dev/null @@ -1,240 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`serve web (/Users/dom/projects/redwood/redwood/packages/web-server/dist/bin.js) > errors out on unknown args 1`] = ` -"rw-web-server - -Start a server for serving only the web side - -Options: - --help Show help [boolean] - --version Show version number [boolean] - -p, --port The port to listen on - [number] [default: 8910] - --host The host to listen on. Defaults to - '0.0.0.0' in production, '::' in - development [string] - --apiUrl, --api-url Relative URL for proxying requests - from or a fully-qualified URL to the - API server - [string] [default: "/.redwood/functions"] - --apiProxyTarget, --api-proxy-target Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [string] - --apiHost, --api-host [Deprecated; use apiProxyTarget] - Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [deprecated] [string] - -Examples: - yarn rw-web-server --api-url=/api --api- Start the web server and proxy - proxy-target=https://api.redwood.horse requests made to '/api' to - 'https://api.redwood.horse' - yarn rw-web-server Start the web server send api - --api-url=https://api.redwood.horse requests to - 'https://api.redwood.horse' (make - sure to configure CORS) - -Unknown arguments: foo, bar, baz -" -`; - -exports[`serve web (/Users/dom/projects/redwood/redwood/packages/web-server/dist/bin.js) > has help configured 1`] = ` -"rw-web-server - -Start a server for serving only the web side - -Options: - --help Show help [boolean] - --version Show version number [boolean] - -p, --port The port to listen on - [number] [default: 8910] - --host The host to listen on. Defaults to - '0.0.0.0' in production, '::' in - development [string] - --apiUrl, --api-url Relative URL for proxying requests - from or a fully-qualified URL to the - API server - [string] [default: "/.redwood/functions"] - --apiProxyTarget, --api-proxy-target Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [string] - --apiHost, --api-host [Deprecated; use apiProxyTarget] - Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [deprecated] [string] - -Examples: - yarn rw-web-server --api-url=/api --api- Start the web server and proxy - proxy-target=https://api.redwood.horse requests made to '/api' to - 'https://api.redwood.horse' - yarn rw-web-server Start the web server send api - --api-url=https://api.redwood.horse requests to - 'https://api.redwood.horse' (make - sure to configure CORS) -" -`; - -exports[`serve web (/Users/dom/projects/redwood/redwood/packages/web-server/dist/bin.js) > works by default; registers a warning at apiUrl 1`] = ` -{ - "data": null, - "errors": [ - { - "extensions": { - "code": "BAD_GATEWAY", - "httpStatus": 502, - }, - "message": "Bad Gateway: you may have misconfigured apiUrl and apiProxyTarget. If apiUrl is a relative URL, you must provide apiProxyTarget.", - }, - ], -} -`; - -exports[`serve web ([ '/Users/dom/projects/redwood/redwood/packages/api-server/dist/index.js', 'web' ]) > errors out on unknown args 1`] = ` -"rw-server web - -Start a server for serving only the web side - -Options: - --help Show help [boolean] - --version Show version number [boolean] - -p, --port The port to listen on - [number] [default: 8910] - --host The host to listen on. Defaults to - '0.0.0.0' in production, '::' in - development [string] - --apiUrl, --api-url Relative URL for proxying requests - from or a fully-qualified URL to the - API server - [string] [default: "/.redwood/functions"] - --apiProxyTarget, --api-proxy-target Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [string] - --apiHost, --api-host [Deprecated; use apiProxyTarget] - Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [deprecated] [string] - -Unknown arguments: foo, bar, baz -" -`; - -exports[`serve web ([ '/Users/dom/projects/redwood/redwood/packages/api-server/dist/index.js', 'web' ]) > has help configured 1`] = ` -"rw-server web - -Start a server for serving only the web side - -Options: - --help Show help [boolean] - --version Show version number [boolean] - -p, --port The port to listen on - [number] [default: 8910] - --host The host to listen on. Defaults to - '0.0.0.0' in production, '::' in - development [string] - --apiUrl, --api-url Relative URL for proxying requests - from or a fully-qualified URL to the - API server - [string] [default: "/.redwood/functions"] - --apiProxyTarget, --api-proxy-target Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [string] - --apiHost, --api-host [Deprecated; use apiProxyTarget] - Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [deprecated] [string] -" -`; - -exports[`serve web ([ '/Users/dom/projects/redwood/redwood/packages/api-server/dist/index.js', 'web' ]) > works by default; registers a warning at apiUrl 1`] = ` -{ - "data": null, - "errors": [ - { - "extensions": { - "code": "BAD_GATEWAY", - "httpStatus": 502, - }, - "message": "Bad Gateway: you may have misconfigured apiUrl and apiProxyTarget. If apiUrl is a relative URL, you must provide apiProxyTarget.", - }, - ], -} -`; - -exports[`serve web ([ '/Users/dom/projects/redwood/redwood/packages/cli/dist/index.js', 'serve', 'web' ]) > errors out on unknown args 1`] = ` -"rw serve web - -Start a server for serving only the web side - -Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where - \`redwood.toml\` is located) - --telemetry Whether to send anonymous usage - telemetry to RedwoodJS [boolean] - -p, --port The port to listen on - [number] [default: 8910] - --host The host to listen on. Defaults to - '0.0.0.0' in production, '::' in - development [string] - --apiUrl, --api-url Relative URL for proxying requests - from or a fully-qualified URL to the - API server - [string] [default: "/.redwood/functions"] - --apiProxyTarget, --api-proxy-target Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [string] - --apiHost, --api-host [Deprecated; use apiProxyTarget] - Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [deprecated] [string] - -Unknown arguments: foo, bar, baz -" -`; - -exports[`serve web ([ '/Users/dom/projects/redwood/redwood/packages/cli/dist/index.js', 'serve', 'web' ]) > has help configured 1`] = ` -"rw serve web - -Start a server for serving only the web side - -Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where - \`redwood.toml\` is located) - --telemetry Whether to send anonymous usage - telemetry to RedwoodJS [boolean] - -p, --port The port to listen on - [number] [default: 8910] - --host The host to listen on. Defaults to - '0.0.0.0' in production, '::' in - development [string] - --apiUrl, --api-url Relative URL for proxying requests - from or a fully-qualified URL to the - API server - [string] [default: "/.redwood/functions"] - --apiProxyTarget, --api-proxy-target Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [string] - --apiHost, --api-host [Deprecated; use apiProxyTarget] - Forward requests from the apiUrl to - this target. apiUrl must be a - relative URL [deprecated] [string] -" -`; - -exports[`serve web ([ '/Users/dom/projects/redwood/redwood/packages/cli/dist/index.js', 'serve', 'web' ]) > works by default; registers a warning at apiUrl 1`] = ` -{ - "data": null, - "errors": [ - { - "extensions": { - "code": "BAD_GATEWAY", - "httpStatus": 502, - }, - "message": "Bad Gateway: you may have misconfigured apiUrl and apiProxyTarget. If apiUrl is a relative URL, you must provide apiProxyTarget.", - }, - ], -} -`; diff --git a/tasks/server-tests/bothServer.test.mts b/tasks/server-tests/bothServer.test.mts new file mode 100644 index 000000000000..e55aa0b60b07 --- /dev/null +++ b/tasks/server-tests/bothServer.test.mts @@ -0,0 +1,191 @@ +import { fileURLToPath } from 'node:url' + +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' +import { fs, path, $ } from 'zx' + +import { getConfig } from '@redwoodjs/project-config' +import { sleep } from './helpers.mjs' + +////////// +// Setup +////////// + +$.verbose = !!process.env.VERBOSE + +const __dirname = fileURLToPath(new URL('./', import.meta.url)) +const FIXTURE_PATH = fileURLToPath( + new URL('./fixtures/redwood-app', import.meta.url) +) + +// @redwoodjs/cli (yarn rw) +const rw = path.resolve(__dirname, '../../packages/cli/dist/index.js') +// @redwoodjs/api-server (yarn rw-server) +const rwServer = path.resolve( + __dirname, + '../../packages/api-server/dist/bin.js' +) +// @redwoodjs/web-server (yarn rw-web-server) +const rwWebServer = path.resolve( + __dirname, + '../../packages/web-server/dist/bin.js' +) + +let original_RWJS_CWD +let projectConfig +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + process.env.RWJS_CWD = FIXTURE_PATH + projectConfig = getConfig() + console.log([ + 'These tests use the following command to run the server:', + `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rw} serve`, + `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rwServer}`, + `• RWJS_CWD=${process.env.RWJS_CWD} yarn node ${rwWebServer}`, + ].join('\n')) +}) +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +// Clean up the child process after each test +let p +afterEach(async () => { + if (!p) { + return + } + p.kill() + // Wait for child process to terminate + try { + await p + } catch { + // Ignore + } +}) + +const TIMEOUT = 1_000 * 2 + +////////// +// Tests +////////// + +describe.each([ + [[rw, 'serve']], + [rwServer], +])('serve both (%s)', (cmd) => { + it("has help configured", async () => { + const { stdout } = await $`yarn node ${cmd} --help` + expect(stdout).toMatchSnapshot() + }) + + it('errors out on unknown args', async () => { + try { + await $`yarn node ${cmd} --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchSnapshot() + } + }) + + describe('webPort', () => { + it("`--webPort` changes the web server's port", async () => { + const webPort = 8920 + p = $`yarn node ${cmd} --webPort ${webPort}` + await sleep(TIMEOUT) + await test({ webPort }) + }) + + it("`REDWOOD_WEB_PORT` changes the web server's port", async () => { + process.env.REDWOOD_WEB_PORT = '8921' + const webPort = +process.env.REDWOOD_WEB_PORT + p = $`yarn node ${cmd}` + await sleep(TIMEOUT) + await test({ webPort }) + delete process.env.REDWOOD_WEB_PORT + }) + + it('`--webPort` takes precedence over `REDWOOD_WEB_PORT`', async () => { + const webPort = 8922 + process.env.REDWOOD_WEB_PORT = '8923' + p = $`yarn node ${cmd} --webPort ${webPort}` + await sleep(TIMEOUT) + await test({ webPort }) + delete process.env.REDWOOD_WEB_PORT + }) + + it('`[web].port` changes the port', async () => { + p = $`yarn node ${cmd}` + await sleep(TIMEOUT) + await test({ port: projectConfig.web.port }) + }) + }) + + describe('webHost', () => { + it("`--webHost` changes the web server's host", async () => { + const webHost = '127.0.0.1' + p = $`yarn node ${cmd} --webHost ${webHost}` + await sleep(TIMEOUT) + await test({ webHost }) + }) + + it("`REDWOOD_WEB_HOST` changes the web server's host", async () => { + process.env.REDWOOD_WEB_HOST = '::1' + const webHost = process.env.REDWOOD_WEB_HOST + p = $`yarn node ${cmd}` + await sleep(TIMEOUT) + await test({ webHost }) + delete process.env.REDWOOD_WEB_HOST + }) + + it('`--webHost` takes precedence over `REDWOOD_WEB_HOST`', async () => { + const webHost = '::' + process.env.REDWOOD_WEB_HOST = '0.0.0.0' + p = $`yarn node ${cmd} --webHost ${webHost}` + await sleep(TIMEOUT) + await test({ webHost }) + delete process.env.REDWOOD_WEB_HOST + }) + + it.todo('`[web].host` changes the host') + + it("defaults to '::' if `NODE_ENV` isn't production", async () => { + p = $`yarn node ${cmd}` + await sleep(TIMEOUT) + await test() + }) + + it.todo("defaults to '0.0.0.0' if `NODE_ENV` is production") + }) +}) + +async function test(options = {}) { + options.webHost ??= '::' + if (options.webHost.includes(':')) { + options.webHost = `[${options.webHost}]` + } + options.webPort ??= projectConfig.web.port + + const webRes = await fetch(`http://${options.webHost}:${options.webPort}/about`) + const webBody = await webRes.text() + + expect(webRes.status).toEqual(200) + expect(webBody).toEqual( + fs.readFileSync( + path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), + 'utf-8' + ) + ) + + options.apiHost ??= '::' + if (options.apiHost.includes(':')) { + options.apiHost = `[${options.apiHost}]` + } + options.apiPort ??= projectConfig.api.port + + const apiRes = await fetch(`http://${options.apiHost}:${options.apiPort}/hello`) + const apiBody = await apiRes.json() + + expect(apiRes.status).toEqual(200) + expect(apiBody).toEqual({ data: 'hello function' }) +} diff --git a/tasks/server-tests/helpers.mts b/tasks/server-tests/helpers.mts new file mode 100644 index 000000000000..79f302b950b4 --- /dev/null +++ b/tasks/server-tests/helpers.mts @@ -0,0 +1,3 @@ +export function sleep(time = 1_000) { + return new Promise(resolve => setTimeout(resolve, time)); +} diff --git a/tasks/server-tests/server.test.mts b/tasks/server-tests/server.test.mts deleted file mode 100644 index 0c805777b904..000000000000 --- a/tasks/server-tests/server.test.mts +++ /dev/null @@ -1,583 +0,0 @@ -/* eslint-disable camelcase */ - -import http from 'node:http' -import { fileURLToPath } from 'node:url' - -import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' -import { fs, path, $ } from 'zx' - -//////////////// -// Tests setup -//////////////// - -const __dirname = fileURLToPath(new URL('./', import.meta.url)) -const FIXTURE_PATH = fileURLToPath( - new URL('./fixtures/redwood-app', import.meta.url) -) -$.verbose = !!process.env.VERBOSE - -let original_RWJS_CWD - -beforeAll(() => { - original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = FIXTURE_PATH -}) -afterAll(() => { - process.env.RWJS_CWD = original_RWJS_CWD -}) - -// Clean up the child process after each test -let p - -afterEach(async () => { - if (!p) { - return - } - p.kill() - // Wait for child process to terminate - try { - await p - } catch { - // Ignore - } -}) - -const TIMEOUT = 1_000 * 2 - -const commandStrings = { - '@redwoodjs/cli': path.resolve(__dirname, '../../packages/cli/dist/index.js'), - '@redwoodjs/api-server': path.resolve( - __dirname, - '../../packages/api-server/dist/index.js' - ), - '@redwoodjs/web-server': path.resolve( - __dirname, - '../../packages/web-server/dist/bin.js' - ), -} - -const redwoodToml = await fs.readFile( - path.join(__dirname, './fixtures/redwood-app/redwood.toml'), - 'utf-8' -) -const match = redwoodToml.match(/apiUrl = "(?[^"]*)/) -const apiUrl = match?.groups?.apiUrl -if (!apiUrl) { - throw new Error("Couldn't find apiUrl in redwood.toml") -} - -//////////////// -// Tests start -//////////////// - -// `yarn rw serve` and variants -describe.each([ - [[commandStrings['@redwoodjs/cli'], 'serve']], - [commandStrings['@redwoodjs/api-server']], -])('serve both (%s)', (commandString) => { - it('serves both sides, using the apiRootPath in redwood.toml', async () => { - p = $`yarn node ${commandString}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch('http://localhost:8910/about') - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) - - const apiRes = await fetch(`http://localhost:8910${apiUrl}/hello`) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) - - it('--port changes the port', async () => { - const port = 8920 - - p = $`yarn node ${commandString} --port ${port}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch(`http://localhost:${port}/about`) - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) - - const apiRes = await fetch(`http://localhost:${port}${apiUrl}/hello`) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) -}) - -// `yarn rw serve api` and variants -describe.each([ - [[commandStrings['@redwoodjs/cli'], 'serve', 'api']], - [[commandStrings['@redwoodjs/api-server'], 'api']], -])('serve api (%s)', (commandString) => { - it('serves the api side', async () => { - p = $`yarn node ${commandString}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch('http://localhost:8911/hello') - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--port changes the port', async () => { - const port = 3000 - - p = $`yarn node ${commandString} --port ${port}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${port}/hello`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--apiRootPath changes the prefix', async () => { - const apiRootPath = '/api' - - p = $`yarn node ${commandString} --apiRootPath ${apiRootPath}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:8911${apiRootPath}/hello`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) -}) - -// `yarn rw serve web` and variants -describe.each([ - [[`${commandStrings['@redwoodjs/cli']}`, 'serve', 'web']], - [[`${commandStrings['@redwoodjs/api-server']}`, 'web']], - [commandStrings['@redwoodjs/web-server']], -])('serve web (%s)', (commandString) => { - it('has help configured', async () => { - const { stdout } = await $`yarn node ${commandString} --help` - expect(stdout).toMatchSnapshot() - }) - - it("works by default; registers a warning at apiUrl", async () => { - p = $`yarn node ${commandString}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - // it serves some page - const res = await fetch('http://localhost:8910/about') - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - await fs.readFile( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) - - const warningRes = await fetch('http://localhost:8910/.redwood/functions/graphql') - const warningBody = await warningRes.json() - - expect(warningRes.status).toEqual(200) - expect(warningBody).toMatchSnapshot() - }) - - it('--api-proxy-target changes the apiUrl proxy target', async () => { - const apiPort = 8916 - const apiHost = 'localhost' - - const helloData = { data: 'hello from mock server' } - - const server = http.createServer((req, res) => { - if (req.url === '/hello') { - res.end(JSON.stringify(helloData)) - } - }) - - server.listen(apiPort, apiHost) - - p = $`yarn node ${commandString} --apiHost http://${apiHost}:${apiPort}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch('http://localhost:8910/.redwood/functions/hello') - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual(helloData) - - server.close() - }) - - it('--port changes the port', async () => { - const port = 8912 - - p = $`yarn node ${commandString} --apiHost http://localhost:8916 --port ${port}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${port}/about`) - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - await fs.readFile( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) - }) - - it('errors out on unknown args', async () => { - try { - await $`yarn node ${commandString} --foo --bar --baz` - expect(true).toEqual(false) - } catch (p) { - expect(p.exitCode).toEqual(1) - expect(p.stdout).toEqual('') - expect(p.stderr).toMatchSnapshot() - } - }) -}) - -describe('@redwoodjs/cli', () => { - describe('both server CLI', () => { - it.todo('handles --socket differently') - - it('has help configured', async () => { - const { stdout } = - await $`yarn node ${commandStrings['@redwoodjs/cli']} serve --help` - - expect(stdout).toMatchInlineSnapshot(` - "usage: rw - - Commands: - rw serve Run both api and web servers [default] - rw serve api Start server for serving only the api - rw serve web Start a server for serving only the web side - - Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where \`redwood.toml\` is located) - --telemetry Whether to send anonymous usage telemetry to RedwoodJS - [boolean] - -p, --port [number] [default: 8910] - --socket [string] - - Also see the Redwood CLI Reference - (​https://redwoodjs.com/docs/cli-commands#serve​) - " - `) - }) - - it('errors out on unknown args', async () => { - try { - await $`yarn node ${commandStrings['@redwoodjs/cli']} serve --foo --bar --baz` - expect(true).toEqual(false) - } catch (p) { - expect(p.exitCode).toEqual(1) - expect(p.stdout).toEqual('') - expect(p.stderr).toMatchInlineSnapshot(` - "usage: rw - - Commands: - rw serve Run both api and web servers [default] - rw serve api Start server for serving only the api - rw serve web Start a server for serving only the web side - - Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where \`redwood.toml\` is located) - --telemetry Whether to send anonymous usage telemetry to RedwoodJS - [boolean] - -p, --port [number] [default: 8910] - --socket [string] - - Also see the Redwood CLI Reference - (​https://redwoodjs.com/docs/cli-commands#serve​) - - Unknown arguments: foo, bar, baz - " - `) - } - }) - }) - - describe('api server CLI', () => { - it.todo('handles --socket differently') - - it('loads dotenv files', async () => { - p = $`yarn node ${commandStrings['@redwoodjs/cli']} serve api` - - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:8911/env`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: '42' }) - }) - - it('has help configured', async () => { - const { stdout } = - await $`yarn node ${commandStrings['@redwoodjs/cli']} serve api --help` - - expect(stdout).toMatchInlineSnapshot(` - "rw serve api - - Start server for serving only the api - - Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where - \`redwood.toml\` is located) - --telemetry Whether to send anonymous usage - telemetry to RedwoodJS [boolean] - -p, --port [number] [default: 8911] - --socket [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - " - `) - }) - - it('errors out on unknown args', async () => { - try { - await $`yarn node ${commandStrings['@redwoodjs/cli']} serve api --foo --bar --baz` - expect(true).toEqual(false) - } catch (p) { - expect(p.exitCode).toEqual(1) - expect(p.stdout).toEqual('') - expect(p.stderr).toMatchInlineSnapshot(` - "rw serve api - - Start server for serving only the api - - Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where - \`redwood.toml\` is located) - --telemetry Whether to send anonymous usage - telemetry to RedwoodJS [boolean] - -p, --port [number] [default: 8911] - --socket [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - - Unknown arguments: foo, bar, baz - " - `) - } - }) - }) -}) - -describe('@redwoodjs/api-server', () => { - describe('both server CLI', () => { - it('--socket changes the port', async () => { - const socket = 8921 - - p = $`yarn node ${commandStrings['@redwoodjs/api-server']} --socket ${socket}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch(`http://localhost:${socket}/about`) - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) - - const apiRes = await fetch( - `http://localhost:${socket}/.redwood/functions/hello` - ) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) - - it('--socket wins out over --port', async () => { - const socket = 8922 - const port = 8923 - - p = $`yarn node ${commandStrings['@redwoodjs/api-server']} --socket ${socket} --port ${port}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch(`http://localhost:${socket}/about`) - const webBody = await webRes.text() - - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) - - const apiRes = await fetch( - `http://localhost:${socket}/.redwood/functions/hello` - ) - const apiBody = await apiRes.json() - - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) - }) - - it("doesn't have help configured", async () => { - const { stdout } = - await $`yarn node ${commandStrings['@redwoodjs/api-server']} --help` - - expect(stdout).toMatchInlineSnapshot(` - "usage: rw-server - - Commands: - rw-server Run both api and web servers [default] - rw-server api Start server for serving only the api - rw-server web Start a server for serving only the web side - - Options: - --help Show help [boolean] - --version Show version number [boolean] - -p, --port [number] [default: 8910] - --socket [string] - " - `) - }) - - it('errors out on unknown args', async () => { - try { - await $`yarn node ${commandStrings['@redwoodjs/api-server']} --foo --bar --baz` - expect(true).toEqual(false) - } catch (p) { - expect(p.exitCode).toEqual(1) - expect(p.stdout).toEqual('') - expect(p.stderr).toMatchInlineSnapshot(` - "usage: rw-server - - Commands: - rw-server Run both api and web servers [default] - rw-server api Start server for serving only the api - rw-server web Start a server for serving only the web side - - Options: - --help Show help [boolean] - --version Show version number [boolean] - -p, --port [number] [default: 8910] - --socket [string] - - Unknown arguments: foo, bar, baz - " - `) - } - }) - }) - - describe('api server CLI', () => { - it('--socket changes the port', async () => { - const socket = 3001 - - p = $`yarn node ${commandStrings['@redwoodjs/api-server']} api --socket ${socket}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${socket}/hello`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--socket wins out over --port', async () => { - const socket = 3002 - const port = 3003 - - p = $`yarn node ${commandStrings['@redwoodjs/api-server']} api --socket ${socket} --port ${port}` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${socket}/hello`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) - }) - - it('--loadEnvFiles loads dotenv files', async () => { - p = $`yarn node ${commandStrings['@redwoodjs/api-server']} api --loadEnvFiles` - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:8911/env`) - const body = await res.json() - - expect(res.status).toEqual(200) - expect(body).toEqual({ data: '42' }) - }) - - it('has help configured', async () => { - const { stdout } = - await $`yarn node ${commandStrings['@redwoodjs/api-server']} api --help` - - expect(stdout).toMatchInlineSnapshot(` - "rw-server api - - Start server for serving only the api - - Options: - --help Show help [boolean] - --version Show version number [boolean] - -p, --port [number] [default: 8911] - --socket [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - " - `) - }) - - it('errors out on unknown args', async () => { - try { - await $`yarn node ${commandStrings['@redwoodjs/api-server']} api --foo --bar --baz` - expect(true).toEqual(false) - } catch (p) { - expect(p.exitCode).toEqual(1) - expect(p.stdout).toEqual('') - expect(p.stderr).toMatchInlineSnapshot(` - "rw-server api - - Start server for serving only the api - - Options: - --help Show help [boolean] - --version Show version number [boolean] - -p, --port [number] [default: 8911] - --socket [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - - Unknown arguments: foo, bar, baz - " - `) - } - }) - }) -}) diff --git a/tasks/smoke-tests/streaming-ssr-dev/playwright.config.ts b/tasks/smoke-tests/streaming-ssr-dev/playwright.config.ts index b188c0d86434..1f0573b50806 100644 --- a/tasks/smoke-tests/streaming-ssr-dev/playwright.config.ts +++ b/tasks/smoke-tests/streaming-ssr-dev/playwright.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ webServer: { command: 'yarn redwood dev --no-generate --fwd="--no-open"', cwd: process.env.REDWOOD_TEST_PROJECT_PATH, - url: 'http://localhost:8910', + url: 'http://localhost:8911/graphql?query={redwood{version}}', reuseExistingServer: !process.env.CI, stdout: 'pipe', }, diff --git a/yarn.lock b/yarn.lock index c6ebc4f0f92e..1753081ef591 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7434,12 +7434,10 @@ __metadata: version: 0.0.0-use.local resolution: "@redwoodjs/api-server@workspace:packages/api-server" dependencies: - "@babel/cli": "npm:7.23.9" - "@babel/core": "npm:^7.22.20" - "@babel/runtime-corejs3": "npm:7.23.9" "@fastify/url-data": "npm:5.4.0" "@redwoodjs/context": "npm:6.0.7" "@redwoodjs/fastify-web": "npm:6.0.7" + "@redwoodjs/framework-tools": "npm:6.0.7" "@redwoodjs/project-config": "npm:6.0.7" "@redwoodjs/web-server": "npm:6.0.7" "@types/aws-lambda": "npm:8.10.126" @@ -7447,11 +7445,9 @@ __metadata: "@types/qs": "npm:6.9.11" "@types/split2": "npm:4.2.3" "@types/yargs": "npm:17.0.32" - ansi-colors: "npm:4.1.3" aws-lambda: "npm:1.0.7" chalk: "npm:4.1.2" chokidar: "npm:3.5.3" - core-js: "npm:3.35.1" dotenv-defaults: "npm:5.0.2" fast-glob: "npm:3.3.2" fast-json-parse: "npm:1.0.3" @@ -7474,7 +7470,7 @@ __metadata: bin: rw-api-server-watch: ./dist/watch.js rw-log-formatter: ./dist/logFormatter/bin.js - rw-server: ./dist/index.js + rw-server: ./dist/bin.js languageName: unknown linkType: soft @@ -8091,7 +8087,6 @@ __metadata: "@prisma/internals": "npm:5.9.0" "@redwoodjs/api-server": "npm:6.0.7" "@redwoodjs/cli-helpers": "npm:6.0.7" - "@redwoodjs/fastify": "npm:6.0.7" "@redwoodjs/fastify-web": "npm:6.0.7" "@redwoodjs/internal": "npm:6.0.7" "@redwoodjs/prerender": "npm:6.0.7" @@ -8318,29 +8313,6 @@ __metadata: languageName: unknown linkType: soft -"@redwoodjs/fastify@npm:6.0.7, @redwoodjs/fastify@workspace:packages/fastify": - version: 0.0.0-use.local - resolution: "@redwoodjs/fastify@workspace:packages/fastify" - dependencies: - "@fastify/http-proxy": "npm:9.3.0" - "@fastify/static": "npm:6.12.0" - "@fastify/url-data": "npm:5.4.0" - "@redwoodjs/context": "npm:6.0.7" - "@redwoodjs/framework-tools": "npm:6.0.7" - "@redwoodjs/project-config": "npm:6.0.7" - "@types/aws-lambda": "npm:8.10.126" - "@types/lodash": "npm:4.14.201" - "@types/qs": "npm:6.9.11" - ansi-colors: "npm:4.1.3" - fast-glob: "npm:3.3.2" - fastify: "npm:4.25.2" - fastify-raw-body: "npm:4.3.0" - lodash: "npm:4.17.21" - qs: "npm:6.11.2" - typescript: "npm:5.3.3" - languageName: unknown - linkType: soft - "@redwoodjs/forms@workspace:packages/forms": version: 0.0.0-use.local resolution: "@redwoodjs/forms@workspace:packages/forms" @@ -12834,7 +12806,7 @@ __metadata: languageName: node linkType: hard -"ansi-colors@npm:4.1.3, ansi-colors@npm:^4.1.1": +"ansi-colors@npm:^4.1.1": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" checksum: ec87a2f59902f74e61eada7f6e6fe20094a628dab765cfdbd03c3477599368768cffccdb5d3bb19a1b6c99126783a143b1fee31aab729b31ffe5836c7e5e28b9 @@ -28146,9 +28118,9 @@ __metadata: linkType: hard "process-warning@npm:^2.0.0": - version: 2.2.0 - resolution: "process-warning@npm:2.2.0" - checksum: 22b252ca6c1edf7fe3c6ab30c39f9a2fa240dc5af46fd0f94c4dcbc577e7570dcccfc1cbfb4510db4759906b9170cb8b18c519d581cdf2ea649e5ac6bb9a0e60 + version: 2.3.2 + resolution: "process-warning@npm:2.3.2" + checksum: 6bccf187f604dd63067ae8b5a08f658d1cc5df4948a51525691a564ad9250575802c094dd5d1b69f015934fe5df6d925f2e607d7a589918069129b07a777aa7b languageName: node linkType: hard @@ -31905,9 +31877,9 @@ __metadata: linkType: hard "tiny-lru@npm:^11.0.0": - version: 11.0.1 - resolution: "tiny-lru@npm:11.0.1" - checksum: f1b4c61dcf822747daafc2ec9f6de6722b7c8f028532d89a878315d0c82001fd9c9386916b6af96ee754ed327d3136ba7b55d319ffc1b4c108a34fdd923fd13b + version: 11.2.5 + resolution: "tiny-lru@npm:11.2.5" + checksum: bda6de074035ca108ce179ba4ceb02a3eca6aab78b5cf161736035f2af562644594435d8fa4c07f098eee96e1a483992025af72f25e6033d54a66cf270fa8372 languageName: node linkType: hard