Skip to content

Commit d78c1cd

Browse files
jtoarTobbe
andauthored
fix(server): ensure consistency between CLI serve entrypoints regarding help and strict (#9809)
This PR aims to fix inconsistencies across all the CLI entry points around `--help` and strict. This PR is technically breaking but I feel like most users would consider many of these things to be bug fixes (e.g. `--help` runs the server 😂). @Tobbe we can talk in more detail about all the changes. Here's a non exhaustive list of all the changes - the `api-root-path` alias was added to `@redwoodjs/api-server` to make the options the same across `yarn rw serve` and `yarn rw-server` - refactored `@redwoodjs/api-server` entrypoint to handle `--help` and yargs strict mode and; we also moved parsing into the if block cause it was running too early (during import) - for the CLI (`yarn rw ...`) yargs strict mode wasn’t exiting with exit code 1 when unknown arguments were passed; now it does and it prints to stderr in the case there is an error - for `@redwoodjs/web-server`, passing `--help` would just run the server since we were using `yargsParser` instead of `yargs`; also added strict mode here and like the others; kept the options the same between the entrypoints - updated the server-tests tests to handle all these cases - spiked out some more tests to write to ensure we're covering all the similarities and differences before refactoring everything --------- Co-authored-by: Tobbe Lundberg <tobbe@tlundberg.com>
1 parent b759ad1 commit d78c1cd

File tree

11 files changed

+470
-298
lines changed

11 files changed

+470
-298
lines changed

packages/api-server/dist.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('dist', () => {
3737
"apiCliOptions": {
3838
"apiRootPath": {
3939
"alias": [
40+
"api-root-path",
4041
"rootPath",
4142
"root-path",
4243
],
@@ -74,7 +75,7 @@ describe('dist', () => {
7475
"webCliOptions": {
7576
"apiHost": {
7677
"alias": "api-host",
77-
"desc": "Forward requests from the apiUrl, defined in redwood.toml to this host",
78+
"desc": "Forward requests from the apiUrl, defined in redwood.toml, to this host",
7879
"type": "string",
7980
},
8081
"port": {

packages/api-server/src/cliHandlers.ts

+28-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const apiCliOptions = {
2929
port: { default: getConfig().api?.port || 8911, type: 'number', alias: 'p' },
3030
socket: { type: 'string' },
3131
apiRootPath: {
32-
alias: ['rootPath', 'root-path'],
32+
alias: ['api-root-path', 'rootPath', 'root-path'],
3333
default: '/',
3434
type: 'string',
3535
desc: 'Root path where your api functions are served',
@@ -49,7 +49,7 @@ export const webCliOptions = {
4949
apiHost: {
5050
alias: 'api-host',
5151
type: 'string',
52-
desc: 'Forward requests from the apiUrl, defined in redwood.toml to this host',
52+
desc: 'Forward requests from the apiUrl, defined in redwood.toml, to this host',
5353
},
5454
} as const
5555

@@ -128,9 +128,24 @@ export const bothServerHandler = async (options: BothServerArgs) => {
128128

129129
export const webServerHandler = async (options: WebServerArgs) => {
130130
const { port, socket, apiHost } = options
131+
const apiUrl = getConfig().web.apiUrl
132+
133+
if (!apiHost && !isFullyQualifiedUrl(apiUrl)) {
134+
console.error(
135+
`${c.red('Error')}: If you don't provide ${c.magenta(
136+
'apiHost'
137+
)}, ${c.magenta(
138+
'apiUrl'
139+
)} needs to be a fully-qualified URL. But ${c.magenta(
140+
'apiUrl'
141+
)} is ${c.yellow(apiUrl)}.`
142+
)
143+
process.exitCode = 1
144+
return
145+
}
146+
131147
const tsServer = Date.now()
132148
process.stdout.write(c.dim(c.italic('Starting Web Server...\n')))
133-
const apiUrl = getConfig().web.apiUrl
134149
// Construct the graphql url from apiUrl by default
135150
// But if apiGraphQLUrl is specified, use that instead
136151
const graphqlEndpoint = coerceRootPath(
@@ -172,3 +187,13 @@ function coerceRootPath(path: string) {
172187

173188
return `${prefix}${path}${suffix}`
174189
}
190+
191+
function isFullyQualifiedUrl(url: string) {
192+
try {
193+
// eslint-disable-next-line no-new
194+
new URL(url)
195+
return true
196+
} catch (e) {
197+
return false
198+
}
199+
}

packages/api-server/src/index.ts

+31-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env node
2+
23
import { hideBin } from 'yargs/helpers'
34
import yargs from 'yargs/yargs'
45

@@ -13,29 +14,38 @@ import {
1314

1415
export * from './types'
1516

16-
const positionalArgs = yargs(hideBin(process.argv)).parseSync()._
17-
18-
// "bin": {
19-
// "rw-api-server-watch": "./dist/watch.js",
20-
// "rw-log-formatter": "./dist/logFormatter/bin.js",
21-
// "rw-server": "./dist/index.js"
22-
// },
23-
2417
if (require.main === module) {
25-
if (positionalArgs.includes('api') && !positionalArgs.includes('web')) {
26-
apiServerHandler(
27-
yargs(hideBin(process.argv)).options(apiCliOptions).parseSync()
18+
yargs(hideBin(process.argv))
19+
.scriptName('rw-server')
20+
.usage('usage: $0 <side>')
21+
.strict()
22+
23+
.command(
24+
'$0',
25+
'Run both api and web servers',
26+
// @ts-expect-error just passing yargs though
27+
(yargs) => {
28+
yargs.options(commonOptions)
29+
},
30+
bothServerHandler
2831
)
29-
} else if (
30-
positionalArgs.includes('web') &&
31-
!positionalArgs.includes('api')
32-
) {
33-
webServerHandler(
34-
yargs(hideBin(process.argv)).options(webCliOptions).parseSync()
32+
.command(
33+
'api',
34+
'Start server for serving only the api',
35+
// @ts-expect-error just passing yargs though
36+
(yargs) => {
37+
yargs.options(apiCliOptions)
38+
},
39+
apiServerHandler
3540
)
36-
} else {
37-
bothServerHandler(
38-
yargs(hideBin(process.argv)).options(commonOptions).parseSync()
41+
.command(
42+
'web',
43+
'Start server for serving only the web side',
44+
// @ts-expect-error just passing yargs though
45+
(yargs) => {
46+
yargs.options(webCliOptions)
47+
},
48+
webServerHandler
3949
)
40-
}
50+
.parse()
4151
}

packages/cli/src/commands/serve.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export const builder = async (yargs) => {
122122
apiHost: {
123123
alias: 'api-host',
124124
type: 'string',
125-
desc: 'Forward requests from the apiUrl, defined in redwood.toml to this host',
125+
desc: 'Forward requests from the apiUrl, defined in redwood.toml, to this host',
126126
},
127127
}),
128128
handler: async (argv) => {

packages/cli/src/index.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,20 @@ async function runYargs() {
217217
await loadPlugins(yarg)
218218

219219
// Run
220-
await yarg.parse(process.argv.slice(2), {}, (_err, _argv, output) => {
220+
await yarg.parse(process.argv.slice(2), {}, (err, _argv, output) => {
221+
// Configuring yargs with `strict` makes it error on unknown args;
222+
// here we're signaling that with an exit code.
223+
if (err) {
224+
process.exitCode = 1
225+
}
226+
221227
// Show the output that yargs was going to if there was no callback provided
222228
if (output) {
223-
console.log(output)
229+
if (err) {
230+
console.error(output)
231+
} else {
232+
console.log(output)
233+
}
224234
}
225235
})
226236
}

packages/web-server/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@
3434
"dotenv-defaults": "5.0.2",
3535
"fast-glob": "3.3.2",
3636
"fastify": "4.24.3",
37-
"yargs-parser": "21.1.1"
37+
"yargs": "17.7.2"
3838
},
3939
"devDependencies": {
40-
"@types/yargs-parser": "21.0.3",
4140
"esbuild": "0.19.9",
4241
"typescript": "5.3.3"
4342
},

packages/web-server/src/server.ts

+23-21
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,14 @@ import path from 'path'
55
import chalk from 'chalk'
66
import { config } from 'dotenv-defaults'
77
import Fastify from 'fastify'
8-
import yargsParser from 'yargs-parser'
8+
import { hideBin } from 'yargs/helpers'
9+
import yargs from 'yargs/yargs'
910

1011
import { getPaths, getConfig } from '@redwoodjs/project-config'
1112

1213
import { redwoodFastifyWeb } from './web'
1314
import { withApiProxy } from './withApiProxy'
1415

15-
interface Opts {
16-
socket?: string
17-
port?: string
18-
apiHost?: string
19-
}
20-
2116
function isFullyQualifiedUrl(url: string) {
2217
try {
2318
// eslint-disable-next-line no-new
@@ -29,22 +24,29 @@ function isFullyQualifiedUrl(url: string) {
2924
}
3025

3126
async function serve() {
32-
// Parse server file args
33-
const args = yargsParser(process.argv.slice(2), {
34-
string: ['port', 'socket', 'apiHost'],
35-
alias: { apiHost: ['api-host'], port: ['p'] },
36-
})
37-
38-
const options: Opts = {
39-
socket: args.socket,
40-
port: args.port,
41-
apiHost: args.apiHost,
42-
}
27+
const options = yargs(hideBin(process.argv))
28+
.scriptName('rw-web-server')
29+
.usage('$0', 'Start server for serving only the web side')
30+
.strict()
31+
32+
.options({
33+
port: {
34+
default: getConfig().web?.port || 8910,
35+
type: 'number',
36+
alias: 'p',
37+
},
38+
socket: { type: 'string' },
39+
apiHost: {
40+
alias: 'api-host',
41+
type: 'string',
42+
desc: 'Forward requests from the apiUrl, defined in redwood.toml, to this host',
43+
},
44+
})
45+
.parseSync()
4346

4447
const redwoodProjectPaths = getPaths()
4548
const redwoodConfig = getConfig()
4649

47-
const port = options.port ? parseInt(options.port) : redwoodConfig.web.port
4850
const apiUrl = redwoodConfig.web.apiUrl
4951

5052
if (!options.apiHost && !isFullyQualifiedUrl(apiUrl)) {
@@ -110,7 +112,7 @@ async function serve() {
110112
listenOptions = { path: options.socket }
111113
} else {
112114
listenOptions = {
113-
port,
115+
port: options.port,
114116
host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::',
115117
}
116118
}
@@ -121,7 +123,7 @@ async function serve() {
121123
if (options.socket) {
122124
console.log(`Web server started on ${options.socket}`)
123125
} else {
124-
console.log(`Web server started on http://localhost:${port}`)
126+
console.log(`Web server started on http://localhost:${options.port}`)
125127
}
126128
})
127129

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`serve web (/Users/dom/projects/redwood/redwood/packages/web-server/dist/server.js) errors out on unknown args 1`] = `
4+
"rw-web-server
5+
6+
Start server for serving only the web side
7+
8+
Options:
9+
--help Show help [boolean]
10+
--version Show version number [boolean]
11+
-p, --port [number] [default: 8910]
12+
--socket [string]
13+
--apiHost, --api-host Forward requests from the apiUrl, defined in
14+
redwood.toml, to this host [string]
15+
16+
Unknown arguments: foo, bar, baz
17+
"
18+
`;
19+
20+
exports[`serve web (/Users/dom/projects/redwood/redwood/packages/web-server/dist/server.js) fails if apiHost isn't set and apiUrl isn't fully qualified 1`] = `
21+
"Error: If you don't provide apiHost, apiUrl needs to be a fully-qualified URL. But apiUrl is /.redwood/functions.
22+
"
23+
`;
24+
25+
exports[`serve web ([
26+
'/Users/dom/projects/redwood/redwood/packages/api-server/dist/index.js',
27+
'web'
28+
]) errors out on unknown args 1`] = `
29+
"rw-server web
30+
31+
Start server for serving only the web side
32+
33+
Options:
34+
--help Show help [boolean]
35+
--version Show version number [boolean]
36+
-p, --port [number] [default: 8910]
37+
--socket [string]
38+
--apiHost, --api-host Forward requests from the apiUrl, defined in
39+
redwood.toml, to this host [string]
40+
41+
Unknown arguments: foo, bar, baz
42+
"
43+
`;
44+
45+
exports[`serve web ([
46+
'/Users/dom/projects/redwood/redwood/packages/api-server/dist/index.js',
47+
'web'
48+
]) fails if apiHost isn't set and apiUrl isn't fully qualified 1`] = `
49+
"Error: If you don't provide apiHost, apiUrl needs to be a fully-qualified URL. But apiUrl is /.redwood/functions.
50+
"
51+
`;

tasks/server-tests/jest.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/** @type {import('jest').Config} */
22
const config = {
33
rootDir: '.',
4+
testMatch: ['<rootDir>/*.test.mjs'],
45
testTimeout: 5_000 * 2,
6+
transform: {},
57
}
68

79
module.exports = config

0 commit comments

Comments
 (0)