Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(server file): add createServer #9845

Merged
merged 34 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
858cdaa
update server file to more finalized api
jtoar Oct 26, 2023
3610b75
createServer: Type `start`
Tobbe Jan 18, 2024
7329e51
make start options optional
jtoar Jan 18, 2024
1755f9f
Use Object.assign instead of spread to avoid copying object
Tobbe Jan 18, 2024
db5790f
move setup server file out of exp
jtoar Jan 18, 2024
c795c4f
remove experimental warning from dev server
jtoar Jan 18, 2024
be8178b
temporary fix for graphql
jtoar Jan 19, 2024
abf8b2a
add babel plugin
jtoar Jan 19, 2024
93d7d94
babel graphql extract plugin
Josh-Walker-GM Jan 19, 2024
a4f7f6c
revert graphql function
Josh-Walker-GM Jan 19, 2024
501b3ea
wip
Josh-Walker-GM Jan 19, 2024
56f36bc
handle apiRootPath in graphql
jtoar Jan 19, 2024
b6081bc
add filterFn to loadFunctions
jtoar Jan 19, 2024
ed74c95
fix filter fn options
jtoar Jan 19, 2024
6cf9a57
rename rw fastify plugin
jtoar Jan 19, 2024
4e59299
fix babel plugin
Josh-Walker-GM Jan 19, 2024
7c9fd71
remove unused graphql plugin from fastify package
jtoar Jan 19, 2024
97f0671
rm space from dirname lol
jtoar Jan 19, 2024
b36edeb
conditionally register graphql plugin
jtoar Jan 19, 2024
22b45e0
copy over dotenv-defaults fix
jtoar Jan 19, 2024
92a08da
get tests passing
jtoar Jan 19, 2024
8a283b5
refactor some tests
jtoar Jan 19, 2024
4168c36
move context up
jtoar Jan 19, 2024
8ccadeb
remove context from graphql
jtoar Jan 19, 2024
77f7262
pass cli args through to watch
jtoar Jan 19, 2024
fbde827
improve comments
jtoar Jan 19, 2024
cde8091
comment graphql options
jtoar Jan 20, 2024
9bdb885
improve mocks in tests
jtoar Jan 20, 2024
458a3f1
remove console log from serve api
jtoar Jan 20, 2024
8394041
style
jtoar Jan 20, 2024
4f20d4b
work on options
jtoar Jan 20, 2024
5898eae
work on entry points
jtoar Jan 20, 2024
f417191
use concurrently in serve
jtoar Jan 21, 2024
b979239
fix typo in error
jtoar Jan 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/api-server/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'dotenv-defaults'
1 change: 1 addition & 0 deletions packages/api-server/dist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('dist', () => {
"type": "string",
},
},
"createServer": [Function],
"webCliOptions": {
"apiHost": {
"alias": "api-host",
Expand Down
9 changes: 9 additions & 0 deletions packages/api-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,16 @@
"@types/yargs": "17.0.32",
"aws-lambda": "1.0.7",
"jest": "29.7.0",
"pino-abstract-transport": "1.1.0",
"typescript": "5.3.3"
},
"peerDependencies": {
"@redwoodjs/graphql-server": "6.0.7"
},
"peerDependenciesMeta": {
"@redwoodjs/graphql-server": {
"optional": true
}
},
"gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1"
}
330 changes: 330 additions & 0 deletions packages/api-server/src/__tests__/createServer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
import path from 'path'

import pino from 'pino'
import build from 'pino-abstract-transport'

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

import {
createServer,
resolveOptions,
DEFAULT_CREATE_SERVER_OPTIONS,
} from '../createServer'

// Set up RWJS_CWD.
let original_RWJS_CWD

beforeAll(() => {
original_RWJS_CWD = process.env.RWJS_CWD
process.env.RWJS_CWD = path.join(__dirname, './fixtures/redwood-app')
})

afterAll(() => {
process.env.RWJS_CWD = original_RWJS_CWD
})

describe('createServer', () => {
// Create a server for most tests. Some that test initialization create their own
let server

beforeAll(async () => {
server = await createServer()
})

afterAll(async () => {
await server?.close()
})

it('serves functions', async () => {
const res = await server.inject({
method: 'GET',
url: '/hello',
})

expect(res.json()).toEqual({ data: 'hello function' })
})

describe('warnings', () => {
let consoleWarnSpy

beforeAll(() => {
consoleWarnSpy = jest.spyOn(console, 'warn')
})

afterAll(() => {
consoleWarnSpy.mockRestore()
})

it('warns about server.config.js', async () => {
await createServer()

expect(consoleWarnSpy.mock.calls[0][0]).toMatchInlineSnapshot(`
"
Ignoring \`config\` and \`configureServer\` in api/server.config.js.
Migrate them to api/src/server.{ts,js}:

\`\`\`js title="api/src/server.{ts,js}"
// Pass your config to \`createServer\`
const server = createServer({
 fastifyServerOptions: myFastifyConfig
})

// Then inline your \`configureFastify\` logic:
server.register(myFastifyPlugin)
\`\`\`
"
`)
})
})

it('`apiRootPath` prefixes all routes', async () => {
const server = await createServer({ apiRootPath: '/api' })

const res = await server.inject({
method: 'GET',
url: '/api/hello',
})

expect(res.json()).toEqual({ data: 'hello function' })

await server.close()
})

// We use `console.log` and `.warn` to output some things.
// Meanwhile, the server gets a logger that may not output to the same place.
// The server's logger also seems to output things out of order.
//
// This should be fixed so that all logs go to the same place
describe('logs', () => {
let consoleLogSpy
let consoleWarnSpy

beforeAll(() => {
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation()
})

afterAll(() => {
consoleLogSpy.mockRestore()
consoleWarnSpy.mockRestore()
})

it("doesn't handle logs consistently", async () => {
// Here we create a logger that outputs to an array.
const loggerLogs: string[] = []
const stream = build(async (source) => {
for await (const obj of source) {
loggerLogs.push(obj)
}
})
const logger = pino(stream)

// Generate some logs.
const server = await createServer({ logger })
const res = await server.inject({
method: 'GET',
url: '/hello',
})
expect(res.json()).toEqual({ data: 'hello function' })
await server.listen({ port: 8910 })
await server.close()

// We expect console log to be called with `withFunctions` logs.
expect(consoleLogSpy.mock.calls[0][0]).toMatch(
/Importing Server Functions/
)

const lastCallIndex = consoleLogSpy.mock.calls.length - 1

expect(consoleLogSpy.mock.calls[lastCallIndex][0]).toMatch(/Listening on/)

// `console.warn` will be used if there's a `server.config.js` file.
expect(consoleWarnSpy.mock.calls[0][0]).toMatchInlineSnapshot(`
"
Ignoring \`config\` and \`configureServer\` in api/server.config.js.
Migrate them to api/src/server.{ts,js}:

\`\`\`js title="api/src/server.{ts,js}"
// Pass your config to \`createServer\`
const server = createServer({
 fastifyServerOptions: myFastifyConfig
})

// Then inline your \`configureFastify\` logic:
server.register(myFastifyPlugin)
\`\`\`
"
`)

// Finally, the logger. Notice how the request/response logs come before the "server is listening..." logs.
expect(loggerLogs[0]).toMatchObject({
reqId: 'req-1',
level: 30,
msg: 'incoming request',
req: {
hostname: 'localhost:80',
method: 'GET',
remoteAddress: '127.0.0.1',
url: '/hello',
},
})
expect(loggerLogs[1]).toMatchObject({
reqId: 'req-1',
level: 30,
msg: 'request completed',
res: {
statusCode: 200,
},
})

expect(loggerLogs[2]).toMatchObject({
level: 30,
msg: 'Server listening at http://[::1]:8910',
})
expect(loggerLogs[3]).toMatchObject({
level: 30,
msg: 'Server listening at http://127.0.0.1:8910',
})
})
})

describe('`server.start`', () => {
it('starts the server using [api].port in redwood.toml if none is specified', async () => {
const server = await createServer()
await server.start()

const address = server.server.address()

if (!address || typeof address === 'string') {
throw new Error('No address or address is a string')
}

expect(address.port).toBe(getConfig().api.port)

await server.close()
})

it('the `REDWOOD_API_PORT` env var takes precedence over [api].port', async () => {
process.env.REDWOOD_API_PORT = '8920'

const server = await createServer()
await server.start()

const address = server.server.address()

if (!address || typeof address === 'string') {
throw new Error('No address or address is a string')
}

expect(address.port).toBe(+process.env.REDWOOD_API_PORT)

await server.close()

delete process.env.REDWOOD_API_PORT
})
})
})

describe('resolveOptions', () => {
it('nothing passed', () => {
const resolvedOptions = resolveOptions()

expect(resolvedOptions).toEqual({
apiRootPath: DEFAULT_CREATE_SERVER_OPTIONS.apiRootPath,
fastifyServerOptions: {
requestTimeout:
DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout,
logger: DEFAULT_CREATE_SERVER_OPTIONS.logger,
},
port: 8911,
})
})

it('ensures `apiRootPath` has slashes', () => {
const expected = '/v1/'

expect(
resolveOptions({
apiRootPath: 'v1',
}).apiRootPath
).toEqual(expected)

expect(
resolveOptions({
apiRootPath: '/v1',
}).apiRootPath
).toEqual(expected)

expect(
resolveOptions({
apiRootPath: 'v1/',
}).apiRootPath
).toEqual(expected)
})

it('moves `logger` to `fastifyServerOptions.logger`', () => {
const resolvedOptions = resolveOptions({
logger: { level: 'info' },
})

expect(resolvedOptions).toMatchObject({
fastifyServerOptions: {
logger: { level: 'info' },
},
})
})

it('`logger` overwrites `fastifyServerOptions.logger`', () => {
const resolvedOptions = resolveOptions({
logger: false,
fastifyServerOptions: {
// @ts-expect-error this is invalid TS but valid JS
logger: true,
},
})

expect(resolvedOptions).toMatchObject({
fastifyServerOptions: {
logger: false,
},
})
})

it('`DEFAULT_CREATE_SERVER_OPTIONS` overwrites `fastifyServerOptions.logger`', () => {
const resolvedOptions = resolveOptions({
fastifyServerOptions: {
// @ts-expect-error this is invalid TS but valid JS
logger: true,
},
})

expect(resolvedOptions).toMatchObject({
fastifyServerOptions: {
logger: DEFAULT_CREATE_SERVER_OPTIONS.logger,
},
})
})

it('parses `--port`', () => {
expect(resolveOptions({}, ['--port', '8930']).port).toEqual(8930)
})

it("throws if `--port` can't be converted to an integer", () => {
expect(() => {
resolveOptions({}, ['--port', 'eight-nine-ten'])
}).toThrowErrorMatchingInlineSnapshot(`"\`port\` must be an integer"`)
})

it('parses `--apiRootPath`', () => {
expect(resolveOptions({}, ['--apiRootPath', 'foo']).apiRootPath).toEqual(
'/foo/'
)
})

it('the `--apiRootPath` flag has precedence', () => {
expect(
resolveOptions({ apiRootPath: 'foo' }, ['--apiRootPath', 'bar'])
.apiRootPath
).toEqual('/bar/')
})
})
4 changes: 3 additions & 1 deletion packages/api-server/src/cliHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export const apiServerHandler = async (options: ApiServerArgs) => {
process.stdout.write(c.dim(c.italic('Starting API Server...\n')))

if (loadEnvFiles) {
// @ts-expect-error for some reason ts can't find the types here but can find them for other packages
const { config } = await import('dotenv-defaults')

config({
Expand Down Expand Up @@ -197,3 +196,6 @@ function isFullyQualifiedUrl(url: string) {
return false
}
}

// Temporarily here till we refactor server code
export { createServer } from './createServer'
Loading
Loading